001/*
002 * $RCSfile: PCXImageReader.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.3 $
042 * $Date: 2007/09/07 19:13:02 $
043 * $State: Exp $
044 */
045package com.github.jaiimageio.impl.plugins.pcx;
046
047import java.awt.Point;
048import java.awt.Rectangle;
049import java.awt.Transparency;
050import java.awt.color.ColorSpace;
051import java.awt.image.BufferedImage;
052import java.awt.image.ColorModel;
053import java.awt.image.ComponentColorModel;
054import java.awt.image.ComponentSampleModel;
055import java.awt.image.DataBuffer;
056import java.awt.image.DataBufferByte;
057import java.awt.image.IndexColorModel;
058import java.awt.image.MultiPixelPackedSampleModel;
059import java.awt.image.Raster;
060import java.awt.image.SampleModel;
061import java.awt.image.WritableRaster;
062import java.io.EOFException;
063import java.io.IOException;
064import java.nio.ByteOrder;
065import java.util.Collections;
066import java.util.Iterator;
067
068import javax.imageio.ImageReadParam;
069import javax.imageio.ImageReader;
070import javax.imageio.ImageTypeSpecifier;
071import javax.imageio.metadata.IIOMetadata;
072import javax.imageio.stream.ImageInputStream;
073
074public class PCXImageReader extends ImageReader implements PCXConstants {
075
076    private ImageInputStream iis;
077    private int width, height;
078    private boolean gotHeader = false;
079    private byte manufacturer;
080    private byte encoding;
081    private short xmax, ymax;
082    private byte[] smallPalette = new byte[3 * 16];
083    private byte[] largePalette = new byte[3 * 256];
084    private byte colorPlanes;
085    private short bytesPerLine;
086    private short paletteType;
087
088    private PCXMetadata metadata;
089
090    private SampleModel sampleModel, originalSampleModel;
091    private ColorModel colorModel, originalColorModel;
092
093    /** The destination region. */
094    private Rectangle destinationRegion;
095
096    /** The source region. */
097    private Rectangle sourceRegion;
098
099    /** The destination image. */
100    private BufferedImage bi;
101
102    /** Indicates whether subsampled, subregion is required, and offset is
103     *  defined
104     */
105    private boolean noTransform = true;
106
107    /** Indicates whether subband is selected. */
108    private boolean seleBand = false;
109
110    /** The scaling factors. */
111    private int scaleX, scaleY;
112
113    /** source and destination bands. */
114    private int[] sourceBands, destBands;
115
116    public PCXImageReader(PCXImageReaderSpi imageReaderSpi) {
117        super(imageReaderSpi);
118    }
119
120    public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) {
121        super.setInput(input, seekForwardOnly, ignoreMetadata);
122        iis = (ImageInputStream) input; // Always works
123        if (iis != null)
124            iis.setByteOrder(ByteOrder.LITTLE_ENDIAN);
125        gotHeader = false;
126    }
127
128    public int getHeight(int imageIndex) throws IOException {
129        checkIndex(imageIndex);
130        readHeader();
131        return height;
132    }
133
134    public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
135        checkIndex(imageIndex);
136        readHeader();
137        return metadata;
138    }
139
140    public Iterator getImageTypes(int imageIndex) throws IOException {
141        checkIndex(imageIndex);
142        readHeader();
143        return Collections.singletonList(new ImageTypeSpecifier(originalColorModel, originalSampleModel)).iterator();
144    }
145
146    public int getNumImages(boolean allowSearch) throws IOException {
147        if (iis == null) {
148            throw new IllegalStateException("input is null");
149        }
150        if (seekForwardOnly && allowSearch) {
151            throw new IllegalStateException("cannot search with forward only input");
152        }
153        return 1;
154    }
155
156    public IIOMetadata getStreamMetadata() throws IOException {
157        return null;
158    }
159
160    public int getWidth(int imageIndex) throws IOException {
161        checkIndex(imageIndex);
162        readHeader();
163        return width;
164    }
165
166    public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
167        checkIndex(imageIndex);
168        readHeader();
169
170        if (iis == null)
171            throw new IllegalStateException("input is null");
172
173        BufferedImage img;
174
175        clearAbortRequest();
176        processImageStarted(imageIndex);
177
178        if (param == null)
179            param = getDefaultReadParam();
180
181        sourceRegion = new Rectangle(0, 0, 0, 0);
182        destinationRegion = new Rectangle(0, 0, 0, 0);
183
184        computeRegions(param, this.width, this.height, param.getDestination(), sourceRegion, destinationRegion);
185
186        scaleX = param.getSourceXSubsampling();
187        scaleY = param.getSourceYSubsampling();
188
189        // If the destination band is set used it
190        sourceBands = param.getSourceBands();
191        destBands = param.getDestinationBands();
192
193        seleBand = (sourceBands != null) && (destBands != null);
194        noTransform = destinationRegion.equals(new Rectangle(0, 0, width, height)) || seleBand;
195
196        if (!seleBand) {
197            sourceBands = new int[colorPlanes];
198            destBands = new int[colorPlanes];
199            for (int i = 0; i < colorPlanes; i++)
200                destBands[i] = sourceBands[i] = i;
201        }
202
203        // If the destination is provided, then use it.  Otherwise, create new one
204        bi = param.getDestination();
205
206        // Get the image data.
207        WritableRaster raster = null;
208
209        if (bi == null) {
210            if (sampleModel != null && colorModel != null) {
211                sampleModel = sampleModel.createCompatibleSampleModel(destinationRegion.width + destinationRegion.x, destinationRegion.height
212                        + destinationRegion.y);
213                if (seleBand)
214                    sampleModel = sampleModel.createSubsetSampleModel(sourceBands);
215                raster = Raster.createWritableRaster(sampleModel, new Point(0, 0));
216                bi = new BufferedImage(colorModel, raster, false, null);
217            }
218        } else {
219            raster = bi.getWritableTile(0, 0);
220            sampleModel = bi.getSampleModel();
221            colorModel = bi.getColorModel();
222
223            noTransform &= destinationRegion.equals(raster.getBounds());
224        }
225
226        byte bdata[] = null; // buffer for byte data
227
228        if (sampleModel.getDataType() == DataBuffer.TYPE_BYTE)
229            bdata = (byte[]) ((DataBufferByte) raster.getDataBuffer()).getData();
230
231        readImage(bdata);
232
233        if (abortRequested())
234            processReadAborted();
235        else
236            processImageComplete();
237
238        return bi;
239    }
240
241    private void readImage(byte[] data) throws IOException {
242
243        byte[] scanline = new byte[bytesPerLine*colorPlanes];
244        
245        if (noTransform) {
246            try {
247                int offset = 0;
248                int nbytes = (width * metadata.bitsPerPixel + 8 - metadata.bitsPerPixel) / 8;
249                for (int line = 0; line < height; line++) {
250                    readScanLine(scanline);
251                    for (int band = 0; band < colorPlanes; band++) {
252                        System.arraycopy(scanline, bytesPerLine * band, data, offset, nbytes);
253                        offset += nbytes;
254                    }
255                    processImageProgress(100.0F * line / height);
256                }
257            } catch (EOFException e) {
258            }
259        } else {
260            if (metadata.bitsPerPixel == 1)
261                read1Bit(data);
262            else if (metadata.bitsPerPixel == 4)
263                read4Bit(data);
264            else
265                read8Bit(data);
266        }
267    }
268
269    private void read1Bit(byte[] data) throws IOException {
270        byte[] scanline = new byte[bytesPerLine];
271        
272        try {
273            // skip until source region
274            for (int line = 0; line < sourceRegion.y; line++) {
275                readScanLine(scanline);
276            }
277            int lineStride =
278                ((MultiPixelPackedSampleModel)sampleModel).getScanlineStride();
279
280            // cache the values to avoid duplicated computation
281            int[] srcOff = new int[destinationRegion.width];
282            int[] destOff = new int[destinationRegion.width];
283            int[] srcPos = new int[destinationRegion.width];
284            int[] destPos = new int[destinationRegion.width];
285
286            for (int i = destinationRegion.x, x = sourceRegion.x, j = 0; i < destinationRegion.x + destinationRegion.width; i++, j++, x += scaleX) {
287                srcPos[j] = x >> 3;
288                srcOff[j] = 7 - (x & 7);
289                destPos[j] = i >> 3;
290                destOff[j] = 7 - (i & 7);
291            }
292            
293            int k = destinationRegion.y * lineStride;
294
295            for (int line = 0; line < sourceRegion.height; line++) {
296                readScanLine(scanline);
297                if (line % scaleY == 0) {
298                    for (int i = 0; i < destinationRegion.width; i++) {
299                        //get the bit and assign to the data buffer of the raster
300                        int v = (scanline[srcPos[i]] >> srcOff[i]) & 1;
301                        data[k + destPos[i]] |= v << destOff[i];
302                    }
303                    k += lineStride;
304                }
305                processImageProgress(100.0F * line / sourceRegion.height);
306            }
307        } catch (EOFException e) {
308        }
309    }
310    
311    private void read4Bit(byte[] data) throws IOException {
312        byte[] scanline = new byte[bytesPerLine];
313        try {
314            int lineStride =
315                ((MultiPixelPackedSampleModel)sampleModel).getScanlineStride();
316
317            // cache the values to avoid duplicated computation
318            int[] srcOff = new int[destinationRegion.width];
319            int[] destOff = new int[destinationRegion.width];
320            int[] srcPos = new int[destinationRegion.width];
321            int[] destPos = new int[destinationRegion.width];
322
323            for (int i = destinationRegion.x, x = sourceRegion.x, j = 0;
324                 i < destinationRegion.x + destinationRegion.width;
325                 i++, j++, x += scaleX) {
326                srcPos[j] = x >> 1;
327                srcOff[j] = (1 - (x & 1)) << 2;
328                destPos[j] = i >> 1;
329                destOff[j] = (1 - (i & 1)) << 2;
330            }
331
332            int k = destinationRegion.y * lineStride;
333
334            for (int line = 0; line < sourceRegion.height; line++) {
335                readScanLine(scanline);
336
337                if (abortRequested())
338                    break;
339                if (line % scaleY == 0) {
340                        for (int i = 0; i < destinationRegion.width; i++) {
341                            //get the bit and assign to the data buffer of the raster
342                            int v = (scanline[srcPos[i]] >> srcOff[i]) & 0x0F;
343                            data[k + destPos[i]] |= v << destOff[i];
344                        }
345                    k += lineStride;
346                }
347                processImageProgress(100.0F * line / sourceRegion.height);
348            }
349        }catch(EOFException e){
350        }
351    }
352
353    /* also handles 24 bit (three 8 bit planes) */
354    private void read8Bit(byte[] data) throws IOException {
355        byte[] scanline = new byte[colorPlanes * bytesPerLine];
356        try {
357            // skip until source region
358            for (int line = 0; line < sourceRegion.y; line++) {
359                readScanLine(scanline);
360            }
361            int dstOffset = destinationRegion.y * (destinationRegion.x + destinationRegion.width) * colorPlanes;
362            for (int line = 0; line < sourceRegion.height; line++) {
363                readScanLine(scanline);
364                if (line % scaleY == 0) {
365                    int srcOffset = sourceRegion.x;
366                    for (int band = 0; band < colorPlanes; band++) {
367                        dstOffset += destinationRegion.x;
368                        for (int x = 0; x < destinationRegion.width; x += scaleX) {
369                            data[dstOffset++] = scanline[srcOffset + x];
370                        }
371                        srcOffset += bytesPerLine;
372                    }
373                }
374                processImageProgress(100.0F * line / sourceRegion.height);
375            }
376        } catch (EOFException e) {
377        }
378    }
379
380    private void readScanLine(byte[] buffer) throws IOException {
381        int max = bytesPerLine * colorPlanes;
382        for (int j = 0; j < max;) {
383            int val = iis.readUnsignedByte();
384
385            if ((val & 0xC0) == 0xC0) {
386                int count = val & ~0xC0;
387                val = iis.readUnsignedByte();
388                for (int k = 0; k < count && j < max; k++) {
389                    buffer[j++] = (byte) (val & 0xFF);
390                }
391            } else {
392                buffer[j++] = (byte) (val & 0xFF);
393            }
394        }
395    }
396
397    private void checkIndex(int imageIndex) {
398        if (imageIndex != 0) {
399            throw new IndexOutOfBoundsException("only one image exists in the stream");
400        }
401    }
402
403    private void readHeader() throws IOException {
404        if (gotHeader) {
405            iis.seek(128);
406            return;
407        }
408
409        metadata = new PCXMetadata();
410
411        manufacturer = iis.readByte(); // manufacturer
412        if (manufacturer != MANUFACTURER)
413            throw new IllegalStateException("image is not a PCX file");
414        metadata.version = iis.readByte(); // version
415        encoding = iis.readByte(); // encoding
416        if (encoding != ENCODING)
417            throw new IllegalStateException("image is not a PCX file, invalid encoding " + encoding);
418
419        metadata.bitsPerPixel = iis.readByte();
420
421        metadata.xmin = iis.readShort();
422        metadata.ymin = iis.readShort();
423        xmax = iis.readShort();
424        ymax = iis.readShort();
425
426        metadata.hdpi = iis.readShort();
427        metadata.vdpi = iis.readShort();
428
429        iis.readFully(smallPalette);
430
431        iis.readByte(); // reserved
432
433        colorPlanes = iis.readByte();
434        bytesPerLine = iis.readShort();
435        paletteType = iis.readShort();
436
437        metadata.hsize = iis.readShort();
438        metadata.vsize = iis.readShort();
439
440        iis.skipBytes(54); // skip filler
441
442        width = xmax - metadata.xmin + 1;
443        height = ymax - metadata.ymin + 1;
444
445        if (colorPlanes == 1) {
446            if (paletteType == PALETTE_GRAYSCALE) {
447                ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
448                int[] nBits = { 8 };
449                colorModel = new ComponentColorModel(cs, nBits, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
450                sampleModel = new ComponentSampleModel(DataBuffer.TYPE_BYTE, width, height, 1, width, new int[] { 0 });
451            } else {
452                if (metadata.bitsPerPixel == 8) {
453                    // read palette from end of file, then reset back to image data
454                    iis.mark();
455
456                    if (iis.length() == -1) {
457                        // read until eof, and work backwards
458                        while (iis.read() != -1)
459                            ;
460                        iis.seek(iis.getStreamPosition() - 256 * 3 - 1);
461                    } else {
462                        iis.seek(iis.length() - 256 * 3 - 1);
463                    }
464
465                    int palletteMagic = iis.read();
466                    if(palletteMagic != 12)
467                        processWarningOccurred("Expected palette magic number 12; instead read "+
468                                               palletteMagic+" from this image.");
469
470                    iis.readFully(largePalette);
471                    iis.reset();
472
473                    colorModel = new IndexColorModel(metadata.bitsPerPixel, 256, largePalette, 0, false);
474                    sampleModel = colorModel.createCompatibleSampleModel(width, height);
475                } else {
476                    int msize = metadata.bitsPerPixel == 1 ? 2 : 16;
477                    colorModel = new IndexColorModel(metadata.bitsPerPixel, msize, smallPalette, 0, false);
478                    sampleModel = colorModel.createCompatibleSampleModel(width, height);
479                }
480            }
481        } else {
482            ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
483            int[] nBits = { 8, 8, 8 };
484            colorModel = new ComponentColorModel(cs, nBits, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
485            sampleModel = new ComponentSampleModel(DataBuffer.TYPE_BYTE, width, height, 1, width * colorPlanes, new int[] { 0, width, width * 2 });
486        }
487
488        originalSampleModel = sampleModel;
489        originalColorModel = colorModel;
490
491        gotHeader = true;
492    }
493}