001/*
002 * $RCSfile: PCXImageWriter.java,v $
003 *
004 * 
005 * Copyright (c) 2007 Sun Microsystems, Inc. All  Rights Reserved.
006 * 
007 * Redistribution and use in source and binary forms, with or without
008 * modification, are permitted provided that the following conditions
009 * are met: 
010 * 
011 * - Redistribution of source code must retain the above copyright 
012 *   notice, this  list of conditions and the following disclaimer.
013 * 
014 * - Redistribution in binary form must reproduce the above copyright
015 *   notice, this list of conditions and the following disclaimer in 
016 *   the documentation and/or other materials provided with the
017 *   distribution.
018 * 
019 * Neither the name of Sun Microsystems, Inc. or the names of 
020 * contributors may be used to endorse or promote products derived 
021 * from this software without specific prior written permission.
022 * 
023 * This software is provided "AS IS," without a warranty of any 
024 * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND 
025 * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, 
026 * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
027 * EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL 
028 * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF 
029 * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
030 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR 
031 * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,
032 * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND
033 * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
034 * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
035 * POSSIBILITY OF SUCH DAMAGES. 
036 * 
037 * You acknowledge that this software is not designed or intended for 
038 * use in the design, construction, operation or maintenance of any 
039 * nuclear facility. 
040 *
041 * $Revision: 1.2 $
042 * $Date: 2007/09/11 20:45:42 $
043 * $State: Exp $
044 */
045package com.github.jaiimageio.impl.plugins.pcx;
046
047import java.awt.Rectangle;
048import java.awt.color.ColorSpace;
049import java.awt.image.ColorModel;
050import java.awt.image.IndexColorModel;
051import java.awt.image.Raster;
052import java.awt.image.RenderedImage;
053import java.awt.image.SampleModel;
054import java.io.IOException;
055import java.nio.ByteOrder;
056
057import javax.imageio.IIOImage;
058import javax.imageio.ImageTypeSpecifier;
059import javax.imageio.ImageWriteParam;
060import javax.imageio.ImageWriter;
061import javax.imageio.metadata.IIOMetadata;
062import javax.imageio.stream.ImageOutputStream;
063
064import com.github.jaiimageio.impl.common.ImageUtil;
065
066public class PCXImageWriter extends ImageWriter implements PCXConstants {
067
068    private ImageOutputStream ios;
069    private Rectangle sourceRegion;
070    private Rectangle destinationRegion;
071    private int colorPlanes,bytesPerLine;
072    private Raster inputRaster = null;
073    private int scaleX,scaleY;
074
075    public PCXImageWriter(PCXImageWriterSpi imageWriterSpi) {
076        super(imageWriterSpi);
077    }
078
079    public void setOutput(Object output) {
080        super.setOutput(output); // validates output
081        if (output != null) {
082            if (!(output instanceof ImageOutputStream))
083                throw new IllegalArgumentException("output not instance of ImageOutputStream");
084            ios = (ImageOutputStream) output;
085            ios.setByteOrder(ByteOrder.LITTLE_ENDIAN);
086        } else
087            ios = null;
088    }
089
090    public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) {
091        if(inData instanceof PCXMetadata)
092            return inData;
093        return null;
094    }
095
096    public IIOMetadata convertStreamMetadata(IIOMetadata inData, ImageWriteParam param) {
097        return null;
098    }
099
100    public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) {
101        PCXMetadata md = new PCXMetadata();
102        md.bitsPerPixel = (byte) imageType.getSampleModel().getSampleSize()[0];
103        return md;
104    }
105
106    public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
107        return null;
108    }
109
110    public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException {
111        if (ios == null) {
112            throw new IllegalStateException("output stream is null");
113        }
114
115        if (image == null) {
116            throw new IllegalArgumentException("image is null");
117        }
118
119        clearAbortRequest();
120        processImageStarted(0);
121        if (param == null)
122            param = getDefaultWriteParam();
123
124        boolean writeRaster = image.hasRaster();
125        
126        sourceRegion = param.getSourceRegion();
127        
128        SampleModel sampleModel = null;
129        ColorModel colorModel = null;
130
131        if (writeRaster) {
132            inputRaster = image.getRaster();
133            sampleModel = inputRaster.getSampleModel();
134            colorModel = ImageUtil.createColorModel(null, sampleModel);
135            if (sourceRegion == null)
136                sourceRegion = inputRaster.getBounds();
137            else
138                sourceRegion = sourceRegion.intersection(inputRaster.getBounds());
139        } else {
140            RenderedImage input = image.getRenderedImage();
141            inputRaster = input.getData();
142            sampleModel = input.getSampleModel();
143            colorModel = input.getColorModel();
144            Rectangle rect = new Rectangle(input.getMinX(), input.getMinY(),
145                                           input.getWidth(), input.getHeight());
146            if (sourceRegion == null)
147                sourceRegion = rect;
148            else
149                sourceRegion = sourceRegion.intersection(rect);
150        }
151        
152        if (sourceRegion.isEmpty())
153            throw new IllegalArgumentException("source region is empty");
154
155        IIOMetadata imageMetadata = image.getMetadata();
156        PCXMetadata pcxImageMetadata = null;
157        
158        ImageTypeSpecifier imageType = new ImageTypeSpecifier(colorModel, sampleModel);
159        if(imageMetadata != null) {
160            // Convert metadata.
161            pcxImageMetadata = (PCXMetadata)convertImageMetadata(imageMetadata, imageType, param);
162        } else {
163            // Use default.
164            pcxImageMetadata = (PCXMetadata)getDefaultImageMetadata(imageType, param);
165        }
166
167        scaleX = param.getSourceXSubsampling();
168        scaleY = param.getSourceYSubsampling();
169        
170        int xOffset = param.getSubsamplingXOffset();
171        int yOffset = param.getSubsamplingYOffset();
172
173        // cache the data type;
174        int dataType = sampleModel.getDataType();
175
176        sourceRegion.translate(xOffset, yOffset);
177        sourceRegion.width -= xOffset;
178        sourceRegion.height -= yOffset;
179
180        int minX = sourceRegion.x / scaleX;
181        int minY = sourceRegion.y / scaleY;
182        int w = (sourceRegion.width + scaleX - 1) / scaleX;
183        int h = (sourceRegion.height + scaleY - 1) / scaleY;
184        
185        xOffset = sourceRegion.x % scaleX;
186        yOffset = sourceRegion.y % scaleY;
187
188        destinationRegion = new Rectangle(minX, minY, w, h);
189        
190        boolean noTransform = destinationRegion.equals(sourceRegion);
191
192        // Raw data can only handle bytes, everything greater must be ASCII.
193        int[] sourceBands = param.getSourceBands();
194        boolean noSubband = true;
195        int numBands = sampleModel.getNumBands();
196
197        if (sourceBands != null) {
198            sampleModel = sampleModel.createSubsetSampleModel(sourceBands);
199            colorModel = null;
200            noSubband = false;
201            numBands = sampleModel.getNumBands();
202        } else {
203            sourceBands = new int[numBands];
204            for (int i = 0; i < numBands; i++)
205                sourceBands[i] = i;
206        }
207        
208        ios.writeByte(MANUFACTURER);
209        ios.writeByte(VERSION_3_0);
210        ios.writeByte(ENCODING);
211        
212        int bitsPerPixel = sampleModel.getSampleSize(0);
213        ios.writeByte(bitsPerPixel);
214        
215        ios.writeShort(destinationRegion.x); // xmin
216        ios.writeShort(destinationRegion.y); // ymin
217        ios.writeShort(destinationRegion.x+destinationRegion.width-1); // xmax
218        ios.writeShort(destinationRegion.y+destinationRegion.height-1); // ymax
219        
220        ios.writeShort(pcxImageMetadata.hdpi);
221        ios.writeShort(pcxImageMetadata.vdpi);
222        
223        byte[] smallpalette = createSmallPalette(colorModel);
224        ios.write(smallpalette);
225        ios.writeByte(0); // reserved
226        
227        colorPlanes = sampleModel.getNumBands();
228        
229        ios.writeByte(colorPlanes);
230        
231        bytesPerLine = destinationRegion.width*bitsPerPixel/8;
232        bytesPerLine += bytesPerLine %2;
233        
234        ios.writeShort(bytesPerLine);
235        
236        if(colorModel.getColorSpace().getType()==ColorSpace.TYPE_GRAY)
237            ios.writeShort(PALETTE_GRAYSCALE);
238        else
239            ios.writeShort(PALETTE_COLOR);
240        
241        ios.writeShort(pcxImageMetadata.hsize);
242        ios.writeShort(pcxImageMetadata.vsize);
243        
244        for(int i=0;i<54;i++)
245            ios.writeByte(0);
246        
247        // write image data
248        
249        if(colorPlanes==1 && bitsPerPixel==1) {
250            write1Bit();
251        }
252        else if(colorPlanes==1 && bitsPerPixel==4) {
253            write4Bit();
254        }
255        else {
256            write8Bit();
257        }
258        
259        // write 256 color palette if needed
260        if(colorPlanes==1 && bitsPerPixel==8 &&
261           colorModel.getColorSpace().getType()!=ColorSpace.TYPE_GRAY){
262            ios.writeByte(12); // Magic number preceding VGA 256 Color Palette Information
263            ios.write(createLargePalette(colorModel));
264        }
265        
266        if (abortRequested()) {
267            processWriteAborted();
268        } else {
269            processImageComplete();
270        }
271    }
272    
273    private void write4Bit() throws IOException {
274        int[] unpacked = new int[sourceRegion.width];
275        int[] samples = new int[bytesPerLine];
276
277        for (int line = 0; line < sourceRegion.height; line += scaleY) {
278            inputRaster.getSamples(sourceRegion.x, line + sourceRegion.y, sourceRegion.width, 1, 0, unpacked);
279            
280            int val=0,dst=0;
281            for(int x=0,nibble=0;x<sourceRegion.width;x+=scaleX){
282                val = val | (unpacked[x] & 0x0F);
283                if(nibble==1) {
284                    samples[dst++]=val;
285                    nibble=0;
286                    val=0;
287                } else {
288                    nibble=1;
289                    val = val << 4;
290                }
291            }
292
293            int last = samples[0];
294            int count = 0;
295
296            for (int x = 0; x < bytesPerLine; x += scaleX) {
297                int sample = samples[x];
298                if (sample != last || count == 63) {
299                    writeRLE(last, count);
300                    count = 1;
301                    last = sample;
302                } else
303                    count++;
304            }
305            if (count >= 1) {
306                writeRLE(last, count);
307            }
308
309            processImageProgress(100.0F * line / sourceRegion.height);
310        }
311    }
312    
313    private void write1Bit() throws IOException {
314        int[] unpacked = new int[sourceRegion.width];
315        int[] samples = new int[bytesPerLine];
316
317        for (int line = 0; line < sourceRegion.height; line += scaleY) {
318            inputRaster.getSamples(sourceRegion.x, line + sourceRegion.y, sourceRegion.width, 1, 0, unpacked);
319            
320            int val=0,dst=0;
321            for(int x=0,bit=1<<7;x<sourceRegion.width;x+=scaleX){
322                if(unpacked[x]>0)
323                    val = val | bit;
324                if(bit==1) {
325                    samples[dst++]=val;
326                    bit=1<<7;
327                    val=0;
328                } else {
329                    bit = bit >> 1;
330                }
331            }
332
333            int last = samples[0];
334            int count = 0;
335
336            for (int x = 0; x < bytesPerLine; x += scaleX) {
337                int sample = samples[x];
338                if (sample != last || count == 63) {
339                    writeRLE(last, count);
340                    count = 1;
341                    last = sample;
342                } else
343                    count++;
344            }
345            if (count >= 1) {
346                writeRLE(last, count);
347            }
348
349            processImageProgress(100.0F * line / sourceRegion.height);
350        }
351    }
352    
353    private void write8Bit() throws IOException {
354        int[][] samples = new int[colorPlanes][bytesPerLine];
355        
356        for(int line=0;line<sourceRegion.height;line+=scaleY) {
357            for(int band=0;band<colorPlanes;band++) {
358                inputRaster.getSamples(sourceRegion.x, line+sourceRegion.y,sourceRegion.width,1,band,samples[band]);
359            }
360            
361            int last = samples[0][0];
362            int count=0;
363            
364            for(int band=0;band<colorPlanes;band++) {
365                for(int x=0;x<bytesPerLine;x+=scaleX) {
366                    int sample = samples[band][x];
367                    if(sample!=last || count==63) {
368                        writeRLE(last,count);
369                        count=1;
370                        last=sample;
371                    } else
372                        count++;
373                }
374            }
375            if(count>=1) {
376                writeRLE(last,count);
377            }
378
379            processImageProgress(100.0F * line / sourceRegion.height);
380        }
381    }
382    
383    private void writeRLE(int val,int count) throws IOException{
384        if(count==1 && (val & 0xC0) != 0xC0) {
385            ios.writeByte(val);
386        } else {
387            ios.writeByte(0xC0 | count);
388            ios.writeByte(val);
389        }
390    }
391    
392    private byte[] createSmallPalette(ColorModel cm){
393        byte[] palette = new byte[16*3];
394        
395        if(!(cm instanceof IndexColorModel))
396            return palette;
397        
398        IndexColorModel icm = (IndexColorModel) cm;
399        if(icm.getMapSize()>16)
400            return palette;
401        
402        for(int i=0,offset=0;i<icm.getMapSize();i++) {
403            palette[offset++] = (byte) icm.getRed(i);
404            palette[offset++] = (byte) icm.getGreen(i);
405            palette[offset++] = (byte) icm.getBlue(i);
406        }
407        
408        return palette;
409    }
410    private byte[] createLargePalette(ColorModel cm){
411        byte[] palette = new byte[256*3];
412        
413        if(!(cm instanceof IndexColorModel))
414            return palette;
415        
416        IndexColorModel icm = (IndexColorModel) cm;
417        
418        for(int i=0,offset=0;i<icm.getMapSize();i++) {
419            palette[offset++] = (byte) icm.getRed(i);
420            palette[offset++] = (byte) icm.getGreen(i);
421            palette[offset++] = (byte) icm.getBlue(i);
422        }
423        
424        return palette;
425    }
426}