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