001    /*
002     * PNGCodec
003     * 
004     * Copyright (c) 2003 Marco Schmidt.
005     * All rights reserved.
006     */
007    
008    package net.sourceforge.jiu.codecs;
009    
010    import java.io.BufferedInputStream;
011    import java.io.DataInputStream;
012    import java.io.DataOutput;
013    import java.io.FileInputStream;
014    import java.io.InputStream;
015    import java.io.IOException;
016    import java.util.Calendar;
017    import java.util.GregorianCalendar;
018    import java.util.SimpleTimeZone;
019    import java.util.zip.CheckedInputStream;
020    import java.util.zip.Deflater;
021    import java.util.zip.InflaterInputStream;
022    import java.util.zip.CRC32;
023    import net.sourceforge.jiu.data.BilevelImage;
024    import net.sourceforge.jiu.data.Gray16Image;
025    import net.sourceforge.jiu.data.Gray8Image;
026    import net.sourceforge.jiu.data.IntegerImage;
027    import net.sourceforge.jiu.data.MemoryBilevelImage;
028    import net.sourceforge.jiu.data.MemoryGray16Image;
029    import net.sourceforge.jiu.data.MemoryGray8Image;
030    import net.sourceforge.jiu.data.MemoryPaletted8Image;
031    import net.sourceforge.jiu.data.MemoryRGB24Image;
032    import net.sourceforge.jiu.data.MemoryRGB48Image;
033    import net.sourceforge.jiu.data.Palette;
034    import net.sourceforge.jiu.data.Paletted8Image;
035    import net.sourceforge.jiu.data.PixelImage;
036    import net.sourceforge.jiu.data.RGB24Image;
037    import net.sourceforge.jiu.data.RGB48Image;
038    import net.sourceforge.jiu.data.RGBIndex;
039    import net.sourceforge.jiu.ops.MissingParameterException;
040    import net.sourceforge.jiu.ops.OperationFailedException;
041    import net.sourceforge.jiu.util.ArrayConverter;
042    
043    /**
044     * An input stream that reads from an underlying stream of PNG
045     * IDAT chunks and skips all header information.
046     * PNG uses one or more IDAT chunks to store image data.
047     * The resulting stream looks like that:
048     * <code>IDAT [chunk size] [compressed data] [checksum] 
049     * IDAT [chunk size] [compressed data] [checksum] ...</code>
050     * This stream class expects an input stream where the first IDAT chunk name and chunk
051     * size have been read already, the stream is thus pointing to the
052     * first byte of the first [compressed data] section.
053     * The size of that section is given to the constructor.
054     * This class then returns calls to read(), counts the bytes it has given 
055     * away and, whenever a compressed data section has been consumed, it reads
056     * the IDAT chunk and stores its size, using it to determine when the
057     * next compressed data section will end.
058     * That way, for the caller the stream appears to be one large compressed
059     * section.
060     * <p>
061     * According to the PNG specs the reason for multiple IDAT chunks is as
062     * follows:
063     * <blockquote>
064     * (Multiple IDAT chunks are allowed so that encoders can work in a fixed 
065     * amount of memory; typically the chunk size will correspond to the encoder's 
066     * buffer size.)
067     * </blockquote>
068     * <a target="_top" href="http://www.w3.org/TR/PNG#C.IDAT">4.1.3. IDAT Image data</a>
069     * <p>
070     * If there is a more elegant approach to read multiple IDAT chunks, please
071     * let me know.
072     * However, reading everything into memory is not an option. 
073     * @author Marco Schmidt
074     * @since 0.12.0
075     */
076    class PngIdatInputStream extends InputStream
077    {
078            private static final int IDAT = 0x49444154;
079            private DataInputStream in;
080            private long bytesLeft;
081    
082            public PngIdatInputStream(DataInputStream input, long bytes)
083            {
084                    in = input;
085                    bytesLeft = bytes;
086            }
087    
088            public int read() throws IOException
089            {
090                    if (bytesLeft == 0)
091                    {
092                            skipHeaders();
093                    }
094                    bytesLeft--;
095                    return in.read();
096            }
097    
098            private void skipHeaders() throws IOException
099            {
100                    do
101                    {
102                            int crc = in.readInt();
103                            bytesLeft = in.readInt() & 0xffffffffL;
104                            int type = in.readInt();
105                            if (IDAT != type)
106                            {
107                                    throw new IOException("Expected IDAT chunk type, got " + 
108                                            Integer.toHexString(type));
109                            }
110                    }
111                    while (bytesLeft == 0);
112            }
113    }
114    
115    /**
116     * A codec for the Portable Network Graphics (PNG) format.
117     * Supports both loading and saving of images.
118     * <h3>Usage examples</h3> 
119     * <h4>Load an image</h4>
120     * The following example code loads an image from a PNG file.
121     * Note that you could also use {@link ImageLoader} or {@link net.sourceforge.jiu.gui.awt.ToolkitLoader}
122     * which require only a single line of code and can load all formats
123     * supported by JIU, including PNG. 
124     * <pre>  PNGCodec codec = new PNGCodec();
125     *  codec.setFile("image.png", CodecMode.LOAD);
126     *  codec.process();
127     *  PixelImage image = codec.getImage();</pre>
128     * <h4>Save an image</h4>
129     * <pre>  PNGCodec codec = new PNGCodec();
130     *  codec.setFile("out.png", CodecMode.SAVE);
131     *  codec.setImage(image);
132     *  codec.setCompressionLevel(Deflater.BEST_COMPRESSION);
133     *  codec.appendComment("Copyright (c) 1992 John Doe");
134     *  // sets last modification time to current time
135     *  codec.setModification(new GregorianCalendar(
136     *   new SimpleTimeZone(0, "UTC")));
137     *  codec.process();</pre>
138     * <h3>Supported storage order types</h3>
139     * <h4>Loading</h4>
140     * This codec reads both non-interlaced and Adam7 interlaced PNG files.
141     * <h4>Saving</h4>
142     * This codec only writes non-interlaced PNG files.
143     * <h3>Supported color types</h3>
144     * <h4>Loading</h4>
145     * <ul>
146     * <li>Grayscale 1 bit streams are loaded as {@link net.sourceforge.jiu.data.BilevelImage} objects,
147     *  2, 4 and 8 bit streams as {@link net.sourceforge.jiu.data.Gray8Image} and 16 bit as
148     *  {@link net.sourceforge.jiu.data.Gray16Image} objects.</li>
149     * <li>Indexed 1, 2, 4 and 8 bit streams are all loaded as {@link net.sourceforge.jiu.data.Paletted8Image}.</li>
150     * <li>RGB truecolor 24 bit streams are loaded as {@link net.sourceforge.jiu.data.RGB24Image},
151     *  48 bit streams as {@link net.sourceforge.jiu.data.RGB48Image} objects.</li>
152     * </ul> 
153     * <h4>Saving</h4>
154     * <ul>
155     * <li>{@link net.sourceforge.jiu.data.BilevelImage} objects are stored as grayscale 1 bit PNG streams.</li>
156     * <li>{@link net.sourceforge.jiu.data.Paletted8Image} objects are stored as indexed 8 bit PNG streams.
157     *  Images will always be stored as 8 bit files, even if the palette has only 16, 4 or 2 entries.
158     * </li>
159     * <li>{@link net.sourceforge.jiu.data.Gray8Image} objects are stored as 8 bit grayscale PNG streams.</li>
160     * <li>{@link net.sourceforge.jiu.data.Gray16Image} objects are stored as 16 bit grayscale PNG streams.</li>
161     * <li>{@link net.sourceforge.jiu.data.RGB24Image} objects are stored as 24 bit RGB truecolor PNG streams.</li>
162     * <li>{@link net.sourceforge.jiu.data.RGB48Image} objects are stored as 48 bit RGB truecolor PNG streams.</li>
163     * </ul> 
164     * <h3>Transparency information</h3>
165     * PNG allows to store different types of transparency information.
166     * Full alpha channels, transparent index values, and more.
167     * Right now, this JIU codec does not make use of this information and simply
168     * skips over it when encountered.
169     * <h3>Bounds</h3>
170     * This codec regards the bounds concept.
171     * If bounds are specified with {@link #setBounds}, the codec will only load or save
172     * part of an image.
173     * <h3>Metadata</h3>
174     * <h4>Loading</h4>
175     * <ul>
176     * <li>Physical resolution information is loaded from <code>pHYs</code> chunks.
177     *  Use {@link #getDpiX} and {@link #getDpiY} to retrieve that information.
178     *  after the call to {@link #process}.</li>
179     * <li>Textual comments are read from <code>tEXt</code> chunks and can be retrieved
180     *  with {@link #getComment} after the call to {@link #process}.</li>
181     * </ul>
182     * <h4>Saving</h4>
183     * <ul>
184     *  <li>Physical resolution information (specified with {@link #setDpi})
185     *    is stored in a <code>pHYs</code> chunk.</li>
186     *  <li>Textual comments (specified with {@link #appendComment}) are stored as <code>tEXt</code> chunks.
187     *   The keyword used is <code>Comment</code>.
188     *   Each of the {@link #getNumComments} is stored in a chunk of its own.</li>
189     *  <li>Time of modification is stored in a <code>tIME</code> chunk.
190     *   Use {@link #setModification(Calendar)} to give a point in time to this codec.</li>
191     * </ul>
192     * <h3>Implementation details</h3>
193     * This class relies heavily on the Java runtime library for decompression and 
194     * checksum creation.
195     * <h3>Background</h3>
196     * To learn more about the PNG file format, visit its 
197     * <a target="_top" href="http://www.libpng.org/pub/png/">official homepage</a>.
198     * There you can find a detailed specification, 
199     * test images and existing PNG libraries and PNG-aware applications.
200     * The book <em>PNG - The Definitive Guide</em> by Greg Roelofs, published by O'Reilly, 1999,
201     * ISBN 1-56592-542-4 is a valuable source of information on PNG.
202     * It is out of print, but it can be viewed online and downloaded for offline reading 
203     * in its entirety from the site. 
204     * @author Marco Schmidt
205     * @since 0.12.0
206     */
207    public class PNGCodec extends ImageCodec
208    {
209            private final int CHUNK_CRC32_IEND = 0xae426082;
210            private final int CHUNK_SIZE_IHDR = 0x0000000d;
211            private final int CHUNK_TYPE_IDAT = 0x49444154;
212            private final int CHUNK_TYPE_IEND = 0x49454e44;
213            private final int CHUNK_TYPE_IHDR = 0x49484452;
214            private final int CHUNK_TYPE_PHYS = 0x70485973;
215            private final int CHUNK_TYPE_PLTE = 0x504c5445;
216            private final int CHUNK_TYPE_TEXT = 0x74455874;
217            private final int CHUNK_TYPE_TIME = 0x74494d45;
218            private final int COLOR_TYPE_GRAY = 0;
219            private final int COLOR_TYPE_GRAY_ALPHA = 4;
220            private final int COLOR_TYPE_INDEXED = 3;
221            private final int COLOR_TYPE_RGB = 2;
222            private final int COLOR_TYPE_RGB_ALPHA = 6;
223            private final int COLOR_TYPE_ALPHA = 4;
224            private final int FILTER_TYPE_NONE = 0;
225            private final int FILTER_TYPE_SUB = 1;
226            private final int FILTER_TYPE_UP = 2;
227            private final int FILTER_TYPE_AVERAGE = 3;
228            private final int FILTER_TYPE_PAETH = 4;
229            private final int COMPRESSION_DEFLATE = 0;
230            private final int INTERLACING_NONE = 0;
231            private final int INTERLACING_ADAM7 = 1;
232            private final int FILTERING_ADAPTIVE = 0;
233            private final int MAX_TEXT_SIZE = 512;
234            private final int ADAM7_NUM_PASSES = 7;
235            private final int DEFAULT_ENCODING_MIN_IDAT_SIZE = 32 * 1024;
236            private final int[] ADAM7_COLUMN_INCREMENT = {8, 8, 4, 4, 2, 2, 1};
237            private final int[] ADAM7_FIRST_COLUMN = {0, 4, 0, 2, 0, 1, 0};
238            private final int[] ADAM7_FIRST_ROW = {0, 0, 4, 0, 2, 0, 1};
239            private final int[] ADAM7_ROW_INCREMENT = {8, 8, 8, 4, 4, 2, 2};
240            private final byte[] MAGIC_BYTES =
241                    {(byte)0x89, (byte)0x50, (byte)0x4e, (byte)0x47,
242                     (byte)0x0d, (byte)0x0a, (byte)0x1a, (byte)0x0a};
243    
244            private boolean alpha;
245            private byte[][] buffers;
246            private int bpp;
247            private CRC32 checksum;
248            private CheckedInputStream checkedIn;
249            private int chunkCounter;
250            private int colorType;
251            private int compressionType;
252            private int currentBufferIndex;
253            private int deflateLevel = Deflater.DEFAULT_COMPRESSION;
254            private int deflateStrategy = Deflater.DEFAULT_STRATEGY;
255            private int encodingMinIdatSize = DEFAULT_ENCODING_MIN_IDAT_SIZE;
256            private int filterType;
257            private boolean hasIhdr;
258            private int height;
259            private IntegerImage image;
260            private DataInputStream in;
261            private InflaterInputStream infl;
262            private int interlaceType;
263            private Calendar modification;
264            private int numChannels;
265            private DataOutput out;
266            private Palette palette;
267            private int precision;
268            private int previousBufferIndex;
269            private byte[] streamInBuffer;
270            private byte[] streamOutBuffer;
271            private int width;
272    
273            /**
274             * Allocates the right image to private field <code>image</code>,
275             * taking into consideration the fields width, height, precision and colorType.
276             * Assumes that an IHDR chunk has been read and the above mentioned
277             * fields have been initialized and checked for their validity.
278             */ 
279            private void allocateImage() throws InvalidFileStructureException, UnsupportedTypeException
280            {
281                    setBoundsIfNecessary(width, height);
282                    int w = getBoundsWidth();
283                    int h = getBoundsHeight();
284                    if (colorType == COLOR_TYPE_GRAY || colorType == COLOR_TYPE_GRAY_ALPHA)
285                    {
286                            if (precision == 1)
287                            {
288                                    image = new MemoryBilevelImage(w, h);
289                            }
290                            else
291                            if (precision <= 8)
292                            {
293                                    image = new MemoryGray8Image(w, h);
294                            }
295                            else
296                            if (precision == 16)
297                            {
298                                    image = new MemoryGray16Image(w, h);
299                            }
300                    }
301                    else
302                    if (colorType == COLOR_TYPE_INDEXED)
303                    {
304                            if (palette == null)
305                            {
306                                    throw new InvalidFileStructureException("No palette found when trying to load indexed image.");
307                            }
308                            image = new MemoryPaletted8Image(w, h, palette);
309                    }
310                    else
311                    if (colorType == COLOR_TYPE_RGB || colorType == COLOR_TYPE_RGB_ALPHA)
312                    {
313                            if (precision == 8)
314                            {
315                                    image = new MemoryRGB24Image(w, h);
316                            }
317                            else
318                            {
319                                    image = new MemoryRGB48Image(w, h);
320                            }
321                    }
322                    else
323                    {
324                            throw new UnsupportedTypeException("Unsupported image type encountered");
325                    }
326            }
327    
328            /**
329             * Checks values {@link #precision} and {@link #colorType}.
330             * A lot of combinations possibly found in an IHDR chunk
331             * are invalid. 
332             * Also initializes {@link #alpha} and {@link #numChannels}.
333             * @throws UnsupportedTypeException if an invalid combination 
334             *  of precision and colorType is found
335             */
336            private void checkColorTypeAndPrecision() throws UnsupportedTypeException
337            {
338                    if (colorType != COLOR_TYPE_GRAY &&
339                        colorType != COLOR_TYPE_RGB &&
340                        colorType != COLOR_TYPE_INDEXED && 
341                        colorType != COLOR_TYPE_GRAY_ALPHA && 
342                        colorType != COLOR_TYPE_RGB_ALPHA)
343                    {
344                            throw new UnsupportedTypeException("Not a valid color type: " + colorType);
345                    }
346                    if (precision != 1 && precision != 2 && precision != 4 && precision != 8 && precision != 16)
347                    {
348                            throw new UnsupportedTypeException("Invalid precision value: " + precision);
349                    }
350                    if (colorType == COLOR_TYPE_INDEXED && precision > 8)
351                    {
352                            throw new UnsupportedTypeException("More than eight bits of precision are not allowed for indexed images.");
353                    }
354                    if (colorType == COLOR_TYPE_RGB && precision < 8)
355                    {
356                            throw new UnsupportedTypeException("Less than eight bits of precision are not allowed for RGB images.");
357                    }
358                    alpha = (colorType & COLOR_TYPE_ALPHA) != 0;
359                    if (colorType == COLOR_TYPE_RGB ||
360                        colorType == COLOR_TYPE_RGB_ALPHA)
361                    {
362                            numChannels = 3;
363                    }
364                    else
365                    {
366                            numChannels = 1;
367                    }
368                    bpp = computeBytesPerRow(1);
369            }
370    
371            /**
372             * Computes a number of bytes for a given number of pixels,
373             * regarding precision and availability of an alpha channel.
374             * @param numPixels the number of pixels for which the number
375             *  of bytes necessary to store them is to be computed
376             * @return number of bytes
377             */
378            private int computeBytesPerRow(int numPixels)
379            {
380                    if (precision < 8)
381                    {
382                            return (numPixels + ((8 / precision) - 1)) / (8 / precision);
383                    }
384                    else
385                    {
386                            return (numChannels + (alpha ? 1 : 0)) * (precision / 8) * numPixels;
387                    }
388            }
389    
390            private int computeColumnsAdam7(int pass)
391            {
392                    switch(pass)
393                    {
394                            case(0): return (width + 7) / 8;
395                            case(1): return (width + 3) / 8;
396                            case(2): return (width + 3) / 4;
397                            case(3): return (width + 1) / 4;
398                            case(4): return (width + 1) / 2;
399                            case(5): return width / 2;
400                            case(6): return width;
401                            default: throw new IllegalArgumentException("Not a valid pass index: " + pass);
402                    }
403            }
404    
405            private void fillRowBuffer(int y, byte[] row, int offs)
406            {
407                    PixelImage image = getImage();
408                    int x1 = getBoundsX1();
409                    int x2 = getBoundsX2();
410                    int w = getBoundsWidth();
411                    if (image instanceof BilevelImage)
412                    {
413                            BilevelImage bilevelImage = (BilevelImage)image;
414                            bilevelImage.getPackedBytes(x1, y, w, row, offs, 0);
415                    }
416                    else
417                    if (image instanceof Gray16Image)
418                    {
419                            Gray16Image grayImage = (Gray16Image)image;
420                            while (w-- > 0)
421                            {
422                                    short sample = grayImage.getShortSample(x1++, y);
423                                    ArrayConverter.setShortBE(row, offs, sample);
424                                    offs += 2;
425                            }
426                    }
427                    else
428                    if (image instanceof Gray8Image)
429                    {
430                            Gray8Image grayImage = (Gray8Image)image;
431                            grayImage.getByteSamples(0, getBoundsX1(), y, getBoundsWidth(), 1, row, offs);
432                    }
433                    else
434                    if (image instanceof Paletted8Image)
435                    {
436                            Paletted8Image palImage = (Paletted8Image)image;
437                            palImage.getByteSamples(0, getBoundsX1(), y, getBoundsWidth(), 1, row, offs);
438                    }
439                    else
440                    if (image instanceof RGB24Image)
441                    {
442                            RGB24Image rgbImage = (RGB24Image)image;
443                            while (w-- > 0)
444                            {
445                                    row[offs++] = rgbImage.getByteSample(RGBIndex.INDEX_RED, x1, y);
446                                    row[offs++] = rgbImage.getByteSample(RGBIndex.INDEX_GREEN, x1, y);
447                                    row[offs++] = rgbImage.getByteSample(RGBIndex.INDEX_BLUE, x1, y);
448                                    x1++;
449                            }
450                    }
451                    else
452                    if (image instanceof RGB48Image)
453                    {
454                            RGB48Image rgbImage = (RGB48Image)image;
455                            while (w-- > 0)
456                            {
457                                    short sample = rgbImage.getShortSample(RGBIndex.INDEX_RED, x1, y);
458                                    ArrayConverter.setShortBE(row, offs, sample);
459                                    offs += 2;
460    
461                                    sample = rgbImage.getShortSample(RGBIndex.INDEX_GREEN, x1, y);
462                                    ArrayConverter.setShortBE(row, offs, sample);
463                                    offs += 2;
464    
465                                    sample = rgbImage.getShortSample(RGBIndex.INDEX_BLUE, x1, y);
466                                    ArrayConverter.setShortBE(row, offs, sample);
467                                    offs += 2;
468    
469                                    x1++;
470                            }
471                    }
472            }
473    
474            /**
475              * Creates a four-letter String from the parameter, an <code>int</code>
476              * value, supposed to be storing a chunk name.
477              * @return the chunk name
478              */
479            private static String getChunkName(int chunk)
480            {
481                    StringBuffer result = new StringBuffer(4);
482                    for (int i = 24; i >= 0; i -= 8)
483                    {
484                            result.append((char)((chunk >> i) & 0xff));
485                    }
486                    return result.toString();
487            }
488    
489            public String getFormatName()
490            {
491                    return "Portable Network Graphics (PNG)";
492            }
493    
494            public String[] getMimeTypes()
495            {
496                    return new String[] {"image/png"};
497            }
498    
499            private static int getPaeth(byte l, byte u, byte nw)
500            {
501            int a = l & 0xff;
502            int b = u & 0xff; 
503            int c = nw & 0xff; 
504            int p = a + b - c;
505            int pa = p - a;
506            if (pa < 0)
507            {
508                    pa = -pa;
509            }
510            int pb = p - b;
511            if (pb < 0)
512            {
513                    pb = -pb;
514            } 
515            int pc = p - c; 
516            if (pc < 0)
517            {
518                    pc = -pc;
519            } 
520            if (pa <= pb && pa <= pc)
521            {
522                    return a;
523            } 
524            if (pb <= pc)
525            {
526                    return b;
527            } 
528            return c;
529            }
530    
531            private void inflateBytes(byte[] buffer, int numBytes) throws InvalidFileStructureException, IOException
532            {
533                    int offset = 0;
534                    do
535                    {
536                            try
537                            {
538                                    int toRead = numBytes - offset;
539                                    int numRead = infl.read(buffer, offset, toRead);
540                                    if (numRead < 0)
541                                    {
542                                            throw new InvalidFileStructureException("Cannot fill buffer");
543                                    }
544                                    offset += numRead;
545                            }
546                            catch (IOException ioe)
547                            {
548                                    throw new InvalidFileStructureException("Stopped decompressing " + ioe.toString());
549                            }
550                    }
551                    while (offset != numBytes);
552            }
553    
554            public boolean isLoadingSupported()
555            {
556                    return true;
557            }
558    
559            public boolean isSavingSupported()
560            {
561                    return true;
562            }
563    
564            private void load() throws 
565                    InvalidFileStructureException,
566                    IOException,
567                    UnsupportedTypeException,
568                    WrongFileFormatException
569            {
570                    byte[] magic = new byte[MAGIC_BYTES.length];
571                    in.readFully(magic);
572                    for (int i = 0; i < MAGIC_BYTES.length; i++)
573                    {
574                            if (magic[i] != MAGIC_BYTES[i])
575                            {
576                                    throw new WrongFileFormatException("Not a valid PNG input " +
577                                            "stream, wrong magic byte sequence.");
578                            }
579                    }
580                    chunkCounter = 0;
581                    do
582                    {
583                            loadChunk();
584                            chunkCounter++;
585                    }
586                    while (image == null);
587                    close();
588                    setImage(image);
589            }
590    
591            private void loadChunk() throws InvalidFileStructureException, IOException, UnsupportedTypeException
592            {
593                    /*
594                     * read chunk size; according to the PNG specs, the size value must not be larger
595                     * than 2^31 - 1; to be safe, we treat the value as an unsigned
596                     * 32 bit value anyway 
597                     */
598                    long chunkSize = in.readInt() & 0xffffffffL;
599                    checksum.reset();
600                    int chunkName = in.readInt();
601                    // first chunk must be IHDR
602                    if (chunkCounter == 0 && chunkName != CHUNK_TYPE_IHDR)
603                    {
604                            throw new InvalidFileStructureException("First chunk was not IHDR but " + getChunkName(chunkName));
605                    }
606                    switch (chunkName)
607                    {
608                            // image data chunk
609                            case(CHUNK_TYPE_IDAT):
610                            {
611                                    loadImage(chunkSize);
612                                    break;
613                            }
614                            // end of image chunk
615                            case(CHUNK_TYPE_IEND):
616                            {
617                                    throw new InvalidFileStructureException("Reached IEND chunk but could not load image.");
618                            }
619                            case(CHUNK_TYPE_IHDR):
620                            {
621                                    if (hasIhdr)
622                                    {
623                                            throw new InvalidFileStructureException("More than one IHDR chunk found.");
624                                    }
625                                    if (chunkCounter != 0)
626                                    {
627                                            throw new InvalidFileStructureException("IHDR chunk must be first; found to be chunk #" + (chunkCounter + 1));
628                                    }
629                                    if (chunkSize != CHUNK_SIZE_IHDR)
630                                    {
631                                            throw new InvalidFileStructureException("Expected PNG " +
632                                                    "IHDR chunk length to be " + CHUNK_SIZE_IHDR + ", got " +
633                                                    chunkSize + ".");
634                                    }
635                                    hasIhdr = true;
636                                    loadImageHeader();
637                                    break;
638                            }
639                            case(CHUNK_TYPE_PHYS):
640                            {
641                                    if (chunkSize == 9)
642                                    {
643                                            byte[] phys = new byte[9];
644                                            in.readFully(phys);
645                                            int x = ArrayConverter.getIntBE(phys, 0);
646                                            int y = ArrayConverter.getIntBE(phys, 4);
647                                            if (phys[8] == 1)
648                                            {
649                                                    // unit is meters
650                                                    final double INCHES_PER_METER = 100 / 2.54;
651                                                    setDpi((int)(x / INCHES_PER_METER), (int)(y / INCHES_PER_METER));
652                                            }
653                                    }
654                                    else
655                                    {
656                                            skip(chunkSize);
657                                    }
658                                    break;
659                            }
660                            case(CHUNK_TYPE_PLTE):
661                            {
662                                    if ((chunkSize % 3) != 0)
663                                    {
664                                            throw new InvalidFileStructureException("Not a valid palette chunk size: " + chunkSize);
665                                    }
666                                    loadPalette(chunkSize / 3);
667                                    break;
668                            }
669                            case(CHUNK_TYPE_TEXT):
670                            {
671                                    if (chunkSize == 0)
672                                    {
673                                    }
674                                    else
675                                    if (chunkSize > MAX_TEXT_SIZE)
676                                    {
677                                            skip(chunkSize);
678                                    }
679                                    else
680                                    {
681                                            StringBuffer text = new StringBuffer((int)chunkSize);
682                                            int i = 0;
683                                            char c;
684                                            do
685                                            {
686                                                    c = (char)in.read();
687                                                    if (c == 0)
688                                                    {
689                                                            skip(chunkSize - i - 1);
690                                                            break;
691                                                    }
692                                                    text.append(c);
693                                                    i++;
694                                            }
695                                            while (i < chunkSize);
696                                            //System.out.println("text=\"" + text.toString() + "\"");
697                                    }
698                                    break;
699                            }
700                            default:
701                            {
702                                    skip(chunkSize);
703                            }
704                    }
705                    int createdChecksum = (int)checksum.getValue();
706                    if (image == null)
707                    {
708                            // this code doesn't work anymore if we have just read an image
709                            int chunkChecksum = in.readInt();
710                            if (createdChecksum != chunkChecksum)
711                            {
712                                    throw new InvalidFileStructureException("Checksum created on chunk " +
713                                            getChunkName(chunkName) + " " + Integer.toHexString(createdChecksum) +
714                                            " is not equal to checksum read from stream " + 
715                                            Integer.toHexString(chunkChecksum) + 
716                                            "; file is corrupted.");
717                            }
718                    }
719            }
720    
721            /**
722             * Load an image from the current position in the file.
723             * Assumes the last things read from input are an IDAT chunk type and
724             * its size, which is the sole argument of this method.
725             * @param chunkSize size of the IDAT chunk that was just read
726             * @throws InvalidFileStructureException if there are values in the PNG stream that make it invalid
727             * @throws IOException if there were I/O errors when reading
728             * @throws UnsupportedTypeException if something was encountered in the stream that is valid but not supported by this codec
729             */
730            private void loadImage(long chunkSize) throws InvalidFileStructureException, IOException, UnsupportedTypeException
731            {
732                    // allocate two byte buffers for current and previous row
733                    buffers = new byte[2][];
734                    int numBytes = computeBytesPerRow(width);
735                    currentBufferIndex = 0;
736                    previousBufferIndex = 1;
737                    buffers[currentBufferIndex] = new byte[numBytes];
738                    buffers[previousBufferIndex] = new byte[numBytes];
739                    for (int i = 0; i < buffers[previousBufferIndex].length; i++)
740                    {
741                            buffers[previousBufferIndex][i] = (byte)0;
742                    }
743                    // allocate the correct type of image object for the image type read in the IHDR chunk 
744                    allocateImage();
745                    // create a PngIdatInputStream which will skip header information when
746                    // multiple IDAT chunks are in the input stream
747                    infl = new InflaterInputStream(new PngIdatInputStream(in, chunkSize));
748                    switch(interlaceType)
749                    {
750                            case(INTERLACING_NONE):
751                            {
752                                    loadImageNonInterlaced();
753                                    break;
754                            }
755                            case(INTERLACING_ADAM7):
756                            {
757                                    loadImageInterlacedAdam7();
758                                    break;
759                            }
760                    }
761            }
762    
763            /**
764             * Reads data from an IHDR chunk and initializes private fields with it.
765             * Does a lot of checking if read values are valid and supported by this class.
766             * @throws IOException
767             * @throws InvalidFileStructureException
768             * @throws UnsupportedTypeException
769             */
770            private void loadImageHeader() throws IOException, InvalidFileStructureException, UnsupportedTypeException
771            {
772                    // WIDTH -- horizontal resolution
773                    width = in.readInt();
774                    if (width < 1)
775                    {
776                            throw new InvalidFileStructureException("Width must be larger than 0; got " + width);
777                    }
778                    // HEIGHT -- vertical resolution
779                    height = in.readInt();
780                    if (height < 1)
781                    {
782                            throw new InvalidFileStructureException("Height must be larger than 0; got " + height);
783                    }
784                    // PRECISION -- bits per sample
785                    precision = in.read();
786                    // COLOR TYPE -- indexed, paletted, grayscale, optionally alpha
787                    colorType = in.read();
788                    // check for invalid combinations of color type and precision
789                    // and initialize alpha and numChannels
790                    checkColorTypeAndPrecision();
791                    // COMPRESSION TYPE -- only Deflate is defined
792                    compressionType = in.read();
793                    if (compressionType != COMPRESSION_DEFLATE)
794                    {
795                            throw new UnsupportedTypeException("Unsupported compression type: " +
796                                    compressionType + ".");
797                    }
798                    // FILTER TYPE -- only Adaptive is defined
799                    filterType = in.read();
800                    if (filterType != FILTERING_ADAPTIVE)
801                    {
802                            throw new UnsupportedTypeException("Only 'adaptive filtering' is supported right now; got " + filterType);
803                    }
804                    // INTERLACE TYPE -- order of storage of image data
805                    interlaceType = in.read();
806                    if (interlaceType != INTERLACING_NONE &&
807                        interlaceType != INTERLACING_ADAM7)
808                    {
809                            throw new UnsupportedTypeException("Only 'no interlacing' and 'Adam7 interlacing' are supported; got " + interlaceType);
810                    }
811            }
812    
813            private void loadImageInterlacedAdam7() throws InvalidFileStructureException, IOException, UnsupportedTypeException
814            {
815                    final int TOTAL_LINES = ADAM7_NUM_PASSES * height;
816                    for (int pass = 0; pass < ADAM7_NUM_PASSES; pass++)
817                    {
818                            currentBufferIndex = 0;
819                            previousBufferIndex = 1;
820                            byte[] previousBuffer = buffers[previousBufferIndex];
821                            for (int x = 0; x < previousBuffer.length; x++)
822                            {
823                                    previousBuffer[x] = 0;
824                            }
825                            int y = ADAM7_FIRST_ROW[pass];
826                            int destY = y - getBoundsY1();
827                            int numColumns = computeColumnsAdam7(pass);
828                            if (numColumns == 0)
829                            {
830                                    // this pass contains no data; skip to next pass
831                                    setProgress((pass + 1) * height, TOTAL_LINES);
832                                    continue;
833                            }
834                            int numBytes = computeBytesPerRow(numColumns);
835                            while (y < height)
836                            {
837                                    previousBuffer = buffers[previousBufferIndex];
838                                    byte[] currentBuffer = buffers[currentBufferIndex];
839                                    int rowFilterType = readFilterType();
840                                    inflateBytes(currentBuffer, numBytes);
841                                    reverseFilter(rowFilterType, currentBuffer, previousBuffer, numBytes);
842                                    if (isRowRequired(y))
843                                    {
844                                            storeInterlacedAdam7(pass, destY, currentBuffer);
845                                    }
846                                    int progressY = y;
847                                    if (pass > 0)
848                                    {
849                                            progressY += pass * height;
850                                    }
851                                    setProgress(progressY, TOTAL_LINES);
852                                    y += ADAM7_ROW_INCREMENT[pass];
853                                    destY += ADAM7_ROW_INCREMENT[pass];
854                                    currentBufferIndex = 1 - currentBufferIndex;
855                                    previousBufferIndex = 1 - previousBufferIndex;
856                            }
857                    }
858            }
859    
860            private void loadImageNonInterlaced() throws InvalidFileStructureException, IOException, UnsupportedTypeException
861            {
862                    int linesToRead = getBoundsY2() + 1;
863                    int rowLength = computeBytesPerRow(width);
864                    for (int y = 0, destY = - getBoundsY1(); y <= getBoundsY2(); y++, destY++)
865                    {
866                            byte[] currentBuffer = buffers[currentBufferIndex];
867                            byte[] previousBuffer = buffers[previousBufferIndex];
868                            int rowFilterType = readFilterType();
869                            inflateBytes(currentBuffer, rowLength);
870                            reverseFilter(rowFilterType, currentBuffer, previousBuffer, rowLength);
871                            if (isRowRequired(y))
872                            {
873                                    storeNonInterlaced(destY, currentBuffer);
874                            }
875                            setProgress(y, linesToRead);
876                            previousBufferIndex = 1 - previousBufferIndex;
877                            currentBufferIndex = 1 - currentBufferIndex;
878                    }
879            }
880    
881            private void loadPalette(long numEntries) throws InvalidFileStructureException, IOException
882            {
883                    if (palette != null)
884                    {
885                            throw new InvalidFileStructureException("More than one palette in input stream.");
886                    }
887                    if (numEntries < 1)
888                    {
889                            throw new InvalidFileStructureException("Number of palette entries must be at least 1.");
890                    }
891                    if (numEntries > 256)
892                    {
893                            throw new InvalidFileStructureException("Number of palette entries larger than 256: " + numEntries);
894                    }
895                    palette = new Palette((int)numEntries);
896                    int index = 0;
897                    do
898                    {
899                            palette.putSample(Palette.INDEX_RED, index, in.read() & 0xff);
900                            palette.putSample(Palette.INDEX_GREEN, index, in.read() & 0xff);
901                            palette.putSample(Palette.INDEX_BLUE, index, in.read() & 0xff);
902                            index++;
903                    }
904                    while (index != numEntries);
905            }
906    
907            public static void main(String[] args) throws Exception
908            {
909                    PNGCodec codec = new PNGCodec();
910                    codec.setFile(args[0], CodecMode.LOAD);
911                    codec.process();
912                    codec.close();
913                    PixelImage image = codec.getImage();
914                    codec = new PNGCodec();
915                    codec.setFile(args[1], CodecMode.SAVE);
916                    codec.setImage(image);
917                    codec.setDpi(300, 300);
918                    codec.appendComment("Test comment #1.");
919                    codec.appendComment("And test comment #2.");
920                    codec.setModification(new GregorianCalendar(new SimpleTimeZone(0, "UTC")));
921                    codec.process();
922                    codec.close();
923            }
924    
925            public void process() throws
926                    InvalidFileStructureException,
927                    MissingParameterException,
928                    OperationFailedException,
929                    UnsupportedTypeException,
930                    WrongFileFormatException
931            {
932                    initModeFromIOObjects();
933                    if (getMode() == CodecMode.LOAD)
934                    {
935                            try
936                            {
937                                    if (getImageIndex() != 0)
938                                    {
939                                            throw new InvalidImageIndexException("PNG streams can only store one image; " + 
940                                                    "index " + getImageIndex() + " is thus not valid.");
941                                    }
942                                    InputStream input = getInputStream();
943                                    if (input == null)
944                                    {
945                                            throw new MissingParameterException("InputStream object missing.");
946                                    }
947                                    checksum = new CRC32();
948                                    checkedIn = new CheckedInputStream(input, checksum);
949                                    in = new DataInputStream(checkedIn);
950                                    load();
951                            }
952                            catch (IOException ioe)
953                            {
954                                    throw new OperationFailedException("I/O failure: " + ioe.toString());
955                            }
956                    }
957                    else
958                    if (getMode() == CodecMode.SAVE)
959                    {
960                            try
961                            {
962                                    PixelImage image = getImage(); 
963                                    if (image == null)
964                                    {
965                                            throw new MissingParameterException("Need image for saving.");
966                                    }
967                                    out = getOutputAsDataOutput();
968                                    if (out == null)
969                                    {
970                                            throw new MissingParameterException("Could not retrieve non-null DataOutput object for saving.");
971                                    }
972                                    setBoundsIfNecessary(image.getWidth(), image.getHeight());
973                                    save();
974                            }
975                            catch (IOException ioe)
976                            {
977                                    throw new OperationFailedException("I/O failure: " + ioe.toString());
978                            }
979                    }
980                    else
981                    {
982                            throw new OperationFailedException("Unknown codec mode: " + getMode());
983                    }
984            }
985    
986            private int readFilterType() throws InvalidFileStructureException, IOException
987            {
988                    int filterType = infl.read();
989                    if (filterType >= 0 && filterType <= 4)
990                    {
991                            return filterType;
992                    }
993                    else
994                    {
995                            throw new InvalidFileStructureException("Valid filter types are from 0 to 4; got " + filterType);
996                    }
997            }
998            private void reverseFilter(int rowFilterType, byte[] buffer, byte[] prev, int numBytes) throws UnsupportedTypeException
999            {
1000                    switch(rowFilterType)
1001                    {
1002                            case(FILTER_TYPE_NONE):
1003                            {
1004                                    break;
1005                            }
1006                            case(FILTER_TYPE_SUB):
1007                            {
1008                                    for (int x = 0, px = -bpp; x < numBytes; x++, px++)
1009                                    {
1010                                            byte currXMinusBpp;
1011                                            if (px < 0)
1012                                            {
1013                                                    currXMinusBpp = 0;
1014                                            }
1015                                            else
1016                                            {
1017                                                    currXMinusBpp = buffer[px];
1018                                            }
1019                                            buffer[x] = (byte)(buffer[x] + currXMinusBpp);
1020                                    }
1021                                    break;
1022                            }
1023                            case(FILTER_TYPE_UP):
1024                            {
1025                                    for (int x = 0; x < numBytes; x++)
1026                                    {
1027                                            buffer[x] = (byte)(buffer[x] + prev[x]);
1028                                    }
1029                                    break;
1030                            }
1031                            case(FILTER_TYPE_AVERAGE):
1032                            {
1033                                    for (int x = 0, px = -bpp; x < numBytes; x++, px++)
1034                                    {
1035                                            int currX = buffer[x] & 0xff;
1036                                            int currXMinus1;
1037                                            if (px < 0)
1038                                            {
1039                                                    currXMinus1 = 0;
1040                                            }
1041                                            else
1042                                            {
1043                                                    currXMinus1 = buffer[px] & 0xff;
1044                                            }
1045                                            int prevX = prev[x] & 0xff;
1046                                            int result = currX + ((currXMinus1 + prevX) / 2);
1047                                            byte byteResult = (byte)result;
1048                                            buffer[x] = byteResult;
1049                                    }
1050                                    break;
1051                            }
1052                            case(FILTER_TYPE_PAETH):
1053                            {
1054                                    for (int x = 0, px = -bpp; x < numBytes; x++, px++)
1055                                    {
1056                                            byte currXMinusBpp; 
1057                                            byte prevXMinusBpp;
1058                                            if (px < 0)
1059                                            {
1060                                                    currXMinusBpp = 0;
1061                                                    prevXMinusBpp = 0;
1062                                            }
1063                                            else
1064                                            {
1065                                                    currXMinusBpp = buffer[px];
1066                                                    prevXMinusBpp = prev[px];
1067                                            }
1068                                            buffer[x] = (byte)(buffer[x] + getPaeth(currXMinusBpp, prev[x], prevXMinusBpp));
1069                                    }
1070                                    break;
1071                            }
1072                            default:
1073                            {
1074                                    throw new UnsupportedTypeException("Unknown filter type: " + rowFilterType);
1075                            }
1076                    }
1077            }
1078    
1079            private void save() throws IOException
1080            {
1081                    // write 8 byte PNG signature
1082                    out.write(MAGIC_BYTES);
1083                    // write IHDR (image header) chunk
1084                    saveIhdrChunk();
1085                    // write pHYs chunk (physical resolution) if data is available
1086                    savePhysChunk();
1087                    // write tEXt chunks if comments are available
1088                    saveTextChunks();
1089                    // write tIME chunk if modification time was set
1090                    saveTimeChunk();
1091                    // write PLTE chunk if necessary
1092                    savePlteChunk();
1093                    // write IDAT chunk
1094                    saveImage();
1095                    // write IEND chunk
1096                    saveIendChunk();
1097                    close();                
1098            }
1099    
1100            private void saveChunk(int chunkType, int chunkSize, byte[] data) throws IOException
1101            {
1102                    // set up array with chunk size and type
1103                    byte[] intArray = new byte[8];
1104                    ArrayConverter.setIntBE(intArray, 0, chunkSize);
1105                    ArrayConverter.setIntBE(intArray, 4, chunkType);
1106                    // write chunk size, type and data
1107                    out.write(intArray, 0, 8);
1108                    out.write(data, 0, chunkSize);
1109                    // create checksum on type and data
1110                    CRC32 checksum = new CRC32();
1111                    checksum.reset();
1112                    checksum.update(intArray, 4, 4);
1113                    checksum.update(data, 0, chunkSize);
1114                    // put checksum into byte array
1115                    ArrayConverter.setIntBE(intArray, 0, (int)checksum.getValue());
1116                    // and write it to output
1117                    out.write(intArray, 0, 4);
1118            }
1119    
1120            private void saveIendChunk() throws IOException
1121            {
1122                    out.writeInt(0);
1123                    out.writeInt(CHUNK_TYPE_IEND);
1124                    out.writeInt(CHUNK_CRC32_IEND);
1125            }
1126    
1127            private void saveIhdrChunk() throws IOException
1128            {
1129                    byte[] buffer = new byte[CHUNK_SIZE_IHDR];
1130                    width = getBoundsWidth();
1131                    ArrayConverter.setIntBE(buffer, 0, width);
1132                    height = getBoundsHeight();
1133                    ArrayConverter.setIntBE(buffer, 4, height);
1134                    PixelImage image = getImage();
1135                    alpha = false;
1136                    numChannels = 1;
1137                    if (image instanceof BilevelImage)
1138                    {
1139                            precision = 1;
1140                            colorType = COLOR_TYPE_GRAY;
1141                    }
1142                    else
1143                    if (image instanceof Gray16Image)
1144                    {
1145                            precision = 16;
1146                            colorType = COLOR_TYPE_GRAY;
1147                    }
1148                    else
1149                    if (image instanceof Gray8Image)
1150                    {
1151                            precision = 8;
1152                            colorType = COLOR_TYPE_GRAY;
1153                    }
1154                    else
1155                    if (image instanceof Paletted8Image)
1156                    {
1157                            precision = 8;
1158                            colorType = COLOR_TYPE_INDEXED;
1159                    }
1160                    else
1161                    if (image instanceof RGB24Image)
1162                    {
1163                            numChannels = 3;
1164                            precision = 8;
1165                            colorType = COLOR_TYPE_RGB;
1166                    }
1167                    else
1168                    if (image instanceof RGB48Image)
1169                    {
1170                            numChannels = 3;
1171                            precision = 16;
1172                            colorType = COLOR_TYPE_RGB;
1173                    }
1174                    buffer[8] = (byte)precision;
1175                    buffer[9] = (byte)colorType;
1176                    compressionType = COMPRESSION_DEFLATE;
1177                    buffer[10] = (byte)compressionType;
1178                    filterType = FILTERING_ADAPTIVE;
1179                    buffer[11] = (byte)filterType;
1180                    interlaceType = INTERLACING_NONE;
1181                    buffer[12] = (byte)interlaceType;
1182                    saveChunk(CHUNK_TYPE_IHDR, CHUNK_SIZE_IHDR, buffer);
1183            }
1184    
1185            private void saveImage() throws IOException
1186            {
1187                    switch(interlaceType)
1188                    {
1189                            case(INTERLACING_NONE):
1190                            {
1191                                    saveImageNonInterlaced();
1192                                    break;
1193                            }
1194                    }
1195            }
1196    
1197            private void saveImageNonInterlaced() throws IOException
1198            {
1199                    PixelImage image = getImage();
1200                    int bytesPerRow = computeBytesPerRow(getBoundsWidth());
1201                    byte[] rowBuffer = new byte[bytesPerRow + 1];
1202                    byte[] outBuffer = new byte[Math.max(encodingMinIdatSize, bytesPerRow + 1)];
1203                    int outOffset = 0;
1204                    int numDeflated;
1205                    Deflater defl = new Deflater(deflateLevel);
1206                    for (int y = getBoundsY1(); y <= getBoundsY2(); y++)
1207                    {
1208                            // fill row buffer
1209                            rowBuffer[0] = 0; // row filter 'None'
1210                            fillRowBuffer(y, rowBuffer, 1);
1211                            // give it to compressor 
1212                            defl.setInput(rowBuffer);
1213                            // store compressed data in outBuffer 
1214                            do
1215                            {
1216                                    numDeflated = defl.deflate(outBuffer, outOffset, outBuffer.length - outOffset);
1217                                    outOffset += numDeflated;
1218                                    if (outOffset == outBuffer.length)
1219                                    {
1220                                            saveChunk(CHUNK_TYPE_IDAT,  outOffset, outBuffer);
1221                                            outOffset = 0;
1222                                    }
1223                            }
1224                            while (numDeflated > 0);
1225                            setProgress(y - getBoundsY1(), getBoundsHeight());
1226                    }
1227                    // tell Deflater that it got all the input
1228                    defl.finish();
1229                    // retrieve remaining compressed data from defl to outBuffer  
1230                    do
1231                    {
1232                            numDeflated = defl.deflate(outBuffer, outOffset, outBuffer.length - outOffset);
1233                            outOffset += numDeflated;
1234                            if (outOffset == outBuffer.length)
1235                            {
1236                                    saveChunk(CHUNK_TYPE_IDAT,  outOffset, outBuffer);
1237                                    outOffset = 0;
1238                            }
1239                    }
1240                    while (numDeflated > 0);
1241                    // write final IDAT chunk if necessary
1242                    if (outOffset > 0)
1243                    {
1244                            saveChunk(CHUNK_TYPE_IDAT,  outOffset, outBuffer);
1245                    }
1246            }
1247    
1248            private void savePhysChunk() throws IOException
1249            {
1250                    int dpiX = getDpiX();
1251                    int dpiY = getDpiY();
1252                    if (dpiX < 1 || dpiY < 1)
1253                    {
1254                            return;
1255                    }
1256                    byte[] data = new byte[9];
1257                    int ppuX = (int)(dpiX * (100 / 2.54));
1258                    int ppuY = (int)(dpiY * (100 / 2.54));
1259                    ArrayConverter.setIntBE(data, 0, ppuX);
1260                    ArrayConverter.setIntBE(data, 4, ppuY);
1261                    data[8] = 1; // unit is the meter
1262                    saveChunk(CHUNK_TYPE_PHYS, data.length, data);
1263            }
1264    
1265            private void savePlteChunk() throws IOException
1266            {
1267                    if (colorType != COLOR_TYPE_INDEXED)
1268                    {
1269                            return;
1270                    }
1271                    Paletted8Image image = (Paletted8Image)getImage();
1272                    Palette pal = image.getPalette();
1273                    int numEntries = pal.getNumEntries();
1274                    byte[] data = new byte[numEntries * 3];
1275                    for (int i = 0, j = 0; i < numEntries; i++, j += 3)
1276                    {
1277                            data[j] = (byte)pal.getSample(RGBIndex.INDEX_RED, i);
1278                            data[j + 1] = (byte)pal.getSample(RGBIndex.INDEX_GREEN, i);
1279                            data[j + 2] = (byte)pal.getSample(RGBIndex.INDEX_BLUE, i);
1280                    }
1281                    saveChunk(CHUNK_TYPE_PLTE, data.length, data);
1282            }
1283    
1284            private void saveTextChunks() throws IOException
1285            {
1286                    int index = 0;
1287                    while (index < getNumComments())
1288                    {
1289                            String comment = getComment(index++);
1290                            comment = "Comment\000" + comment;
1291                            byte[] data = comment.getBytes("ISO-8859-1");
1292                            saveChunk(CHUNK_TYPE_TEXT, data.length, data);
1293                    }
1294            }
1295    
1296            private void saveTimeChunk() throws IOException
1297            {
1298                    if (modification == null)
1299                    {
1300                            return;
1301                    }
1302                    byte[] data = new byte[7];
1303                    ArrayConverter.setShortBE(data, 0, (short)modification.get(Calendar.YEAR));
1304                    data[2] = (byte)(modification.get(Calendar.MONTH) + 1);
1305                    data[3] = (byte)modification.get(Calendar.DAY_OF_MONTH);
1306                    data[4] = (byte)modification.get(Calendar.HOUR_OF_DAY);
1307                    data[5] = (byte)modification.get(Calendar.MINUTE);
1308                    data[6] = (byte)modification.get(Calendar.SECOND);
1309                    saveChunk(CHUNK_TYPE_TIME, data.length, data);
1310            }
1311    
1312            /**
1313             * Sets the compression level to be used with the underlying
1314             * {@link java.util.zip.Deflater} object which does the compression.
1315             * If no value is specified, {@link java.util.zip.Deflater#DEFAULT_COMPRESSION}
1316             * is used.  
1317             * @param newLevel compression level, from 0 to 9, 0 being fastest 
1318             *  and compressing worst and 9 offering highest compression and taking
1319             *  the most time 
1320             */
1321            public void setCompressionLevel(int newLevel)
1322            {
1323                    if (newLevel >= 0 && newLevel <= 9)
1324                    {
1325                            deflateLevel = newLevel;
1326                    }
1327                    else
1328                    {
1329                            throw new IllegalArgumentException("Compression level must be from 0..9; got " + newLevel);
1330                    }
1331            }
1332    
1333            /**
1334             * Sets the compression strategy to be used with the underlying
1335             * {@link java.util.zip.Deflater} object which does the compression.
1336             * If no value is specified, {@link java.util.zip.Deflater#DEFAULT_STRATEGY}
1337             * is used.  
1338             * @param newStrategy one of Deflater's strategy values: 
1339             *  {@link java.util.zip.Deflater#DEFAULT_STRATEGY},
1340             *  {@link java.util.zip.Deflater#FILTERED},
1341             *  {@link java.util.zip.Deflater#HUFFMAN_ONLY}
1342             */
1343            public void setCompressionStrategy(int newStrategy)
1344            {
1345                    if (newStrategy == Deflater.FILTERED ||
1346                        newStrategy == Deflater.DEFAULT_STRATEGY ||
1347                        newStrategy == Deflater.HUFFMAN_ONLY)
1348                    {
1349                            deflateStrategy = newStrategy;
1350                    }
1351                    else
1352                    {
1353                            throw new IllegalArgumentException("Unknown compression strategy: " + newStrategy);
1354                    }
1355            }
1356    
1357            /**
1358             * Sets the size of IDAT chunks generated when encoding.
1359             * If this method is never called, a default value of 32768 bytes (32 KB) is used.
1360             * Note that a byte array of the size of the value you specify here is allocated,
1361             * so make sure that you keep the value small enough to stay within a
1362             * system's memory.
1363             * <p>
1364             * Compressed image data is spread over several IDAT chunks by this codec.
1365             * The length of the compressed data of a complete image is known only after the complete image 
1366             * has been encoded.
1367             * With PNG, that length value has to be stored before the compressed data as a chunk size value.
1368             * This codec is supposed to work with {@link java.io.OutputStream} objects,
1369             * so seeking back to adjust the chunk size value of an IDAT chunk is not
1370             * possible.
1371             * That's why all data of a chunk is compressed into a memory buffer.
1372             * Whenever the buffer gets full, it is written to output as an IDAT chunk.
1373             * <p>
1374             * Note that the last IDAT chunk may be smaller than the size defined here.
1375             * @param newSize size of encoding compressed data buffer
1376             */
1377            public void setEncodingIdatSize(int newSize)
1378            {
1379                    if (newSize < 1)
1380                    {
1381                            throw new IllegalArgumentException("Minimum IDAT chunk size must be 1 or larger.");
1382                    }
1383                    encodingMinIdatSize = newSize;
1384            }
1385    
1386            public void setFile(String fileName, CodecMode codecMode) throws IOException, UnsupportedCodecModeException
1387            {
1388                    if (codecMode == CodecMode.LOAD)
1389                    {
1390                            setInputStream(new BufferedInputStream(new FileInputStream(fileName)));
1391                    }
1392                    else
1393                    {
1394                            super.setFile(fileName, codecMode);
1395                    }
1396            }
1397    
1398            /**
1399             * Sets date and time of last modification of the image to be stored in a PNG stream
1400             * when saving.
1401             * Make sure the argument object has UTC as time zone
1402             * (<a target="_top" href="http://www.w3.org/TR/PNG#C.tIME">as
1403             * demanded by the PNG specs)</a>.
1404             * If you want the current time and date, use 
1405             * <code>new GregorianCalendar(new SimpleTimeZone(0, "UTC"))</code>
1406             * as parameter for this method.
1407             * @param time time of last modification of the image
1408             */
1409            public void setModification(Calendar time)
1410            {
1411                    modification = time;
1412            }
1413    
1414            /**
1415             * Skips a number of bytes in the input stream.
1416             * @param num number of bytes to be skipped
1417             * @throws IOException if there were I/O errors
1418             */
1419            private void skip(long num) throws IOException
1420            {
1421                    while (num > 0)
1422                    {
1423                            long numSkipped = in.skip(num);
1424                            if (numSkipped > 0)
1425                            {
1426                                    num -= numSkipped;
1427                            }
1428                    }
1429            }
1430    
1431            private void storeInterlacedAdam7(int pass, int y, byte[] buffer)
1432            {
1433                    switch(colorType)
1434                    {
1435                            case(COLOR_TYPE_GRAY):
1436                            {
1437                                    storeInterlacedAdam7Gray(pass, y, buffer);
1438                                    break;
1439                            }
1440                            case(COLOR_TYPE_RGB):
1441                            {
1442                                    storeInterlacedAdam7Rgb(pass, y, buffer);
1443                                    break;
1444                            }
1445                            case(COLOR_TYPE_RGB_ALPHA):
1446                            {
1447                                    storeInterlacedAdam7RgbAlpha(pass, y, buffer);
1448                                    break;
1449                            }
1450                            case(COLOR_TYPE_GRAY_ALPHA):
1451                            {
1452                                    storeInterlacedAdam7GrayAlpha(pass, y, buffer);
1453                                    break;
1454                            }
1455                            case(COLOR_TYPE_INDEXED):
1456                            {
1457                                    storeInterlacedAdam7Indexed(pass, y, buffer);
1458                                    break;
1459                            }
1460                    }
1461            }
1462    
1463            private void storeInterlacedAdam7Gray(int pass, int y, byte[] buffer)
1464            {
1465                    int x = ADAM7_FIRST_COLUMN[pass];
1466                    final int incr = ADAM7_COLUMN_INCREMENT[pass];
1467                    final int x1 = getBoundsX1();
1468                    final int x2 = getBoundsX2();
1469                    int offset = 0;
1470                    int numColumns = computeColumnsAdam7(pass);
1471                    int numPackedBytes = computeBytesPerRow(numColumns);
1472                    byte[] dest = new byte[numColumns + 7];
1473                    switch(precision)
1474                    {
1475                            case(1):
1476                            {
1477                                    BilevelImage bilevelImage = (BilevelImage)image;
1478                                    ArrayConverter.decodePacked1Bit(buffer, 0, dest, 0, numPackedBytes);
1479                                    while (x <= x2)
1480                                    {
1481                                            if (x >= x1)
1482                                            {
1483                                                    if (dest[offset] == 0)
1484                                                    {
1485                                                            bilevelImage.putBlack(x - x1, y);
1486                                                    }
1487                                                    else
1488                                                    {
1489                                                            bilevelImage.putWhite(x - x1, y);
1490                                                    }
1491                                            }
1492                                            x += incr;
1493                                            offset++;
1494                                    }
1495                                    break;
1496                            }
1497                            case(2):
1498                            {
1499                                    Gray8Image grayImage = (Gray8Image)image;
1500                                    ArrayConverter.convertPacked2BitIntensityTo8Bit(buffer, 0, dest, 0, numPackedBytes);
1501                                    while (x <= x2)
1502                                    {
1503                                            if (x >= x1)
1504                                            {
1505                                                    grayImage.putByteSample(x - x1, y, dest[offset]);
1506                                            }
1507                                            x += incr;
1508                                            offset++;
1509                                    }
1510                                    break;
1511                            }
1512                            case(4):
1513                            {
1514                                    Gray8Image grayImage = (Gray8Image)image;
1515                                    ArrayConverter.convertPacked4BitIntensityTo8Bit(buffer, 0, dest, 0, numPackedBytes);
1516                                    while (x <= x2)
1517                                    {
1518                                            if (x >= x1)
1519                                            {
1520                                                    grayImage.putByteSample(x - x1, y, dest[offset]);
1521                                            }
1522                                            x += incr;
1523                                            offset++;
1524                                    }
1525                                    break;
1526                            }
1527                            case(8):
1528                            {
1529                                    Gray8Image grayImage = (Gray8Image)image;
1530                                    while (x <= x2)
1531                                    {
1532                                            if (x >= x1)
1533                                            {
1534                                                    grayImage.putSample(x - x1, y, buffer[offset]);
1535                                            }
1536                                            x += incr;
1537                                            offset++;
1538                                    }
1539                                    break;
1540                            }
1541                            case(16):
1542                            {
1543                                    Gray16Image grayImage = (Gray16Image)image;
1544                                    while (x <= x2)
1545                                    {
1546                                            if (x >= x1)
1547                                            {
1548                                                    int sample = (buffer[offset] & 0xff) << 8;
1549                                                    sample |= (buffer[offset + 1] & 0xff);
1550                                                    grayImage.putSample(x, y, sample);
1551                                            }
1552                                            x += incr;
1553                                            offset += 2;
1554                                    }
1555                                    break;
1556                            }
1557                    }
1558            }
1559    
1560            private void storeInterlacedAdam7GrayAlpha(int pass, int y, byte[] buffer)
1561            {
1562                    int x = ADAM7_FIRST_COLUMN[pass];
1563                    final int incr = ADAM7_COLUMN_INCREMENT[pass];
1564                    final int x1 = getBoundsX1();
1565                    final int x2 = getBoundsX2();
1566                    int offset = 0;
1567                    switch(precision)
1568                    {
1569                            case(8):
1570                            {
1571                                    Gray8Image grayImage = (Gray8Image)image;
1572                                    while (x <= x2)
1573                                    {
1574                                            if (x >= x1)
1575                                            {
1576                                                    grayImage.putSample(x - x1, y, buffer[offset]);
1577                                                    // alpha
1578                                            }
1579                                            x += incr;
1580                                            offset += 2;
1581                                    }
1582                                    break;
1583                            }
1584                            case(16):
1585                            {
1586                                    Gray16Image grayImage = (Gray16Image)image;
1587                                    while (x <= x2)
1588                                    {
1589                                            if (x >= x1)
1590                                            {
1591                                                    int sample = (buffer[offset] & 0xff) << 8;
1592                                                    sample |= (buffer[offset + 1] & 0xff);
1593                                                    grayImage.putSample(x, y, sample);
1594                                                    // store alpha
1595                                            }
1596                                            x += incr;
1597                                            offset += 4;
1598                                    }
1599                                    break;
1600                            }
1601                    }
1602            }
1603    
1604            private void storeInterlacedAdam7Indexed(int pass, int y, byte[] buffer)
1605            {
1606                    Paletted8Image palImage = (Paletted8Image)image;
1607                    int x = ADAM7_FIRST_COLUMN[pass];
1608                    final int incr = ADAM7_COLUMN_INCREMENT[pass];
1609                    final int x1 = getBoundsX1();
1610                    final int x2 = getBoundsX2();
1611                    int offset = 0;
1612                    int numColumns = computeColumnsAdam7(pass);
1613                    int numPackedBytes = computeBytesPerRow(numColumns);
1614                    byte[] dest = new byte[numColumns + 7];
1615                    switch(precision)
1616                    {
1617                            case(1):
1618                            {
1619                                    ArrayConverter.decodePacked1Bit(buffer, 0, dest, 0, numPackedBytes);
1620                                    while (x <= x2)
1621                                    {
1622                                            if (x >= x1)
1623                                            {
1624                                                    palImage.putByteSample(x - x1, y, dest[offset]);
1625                                            }
1626                                            x += incr;
1627                                            offset++;
1628                                    }
1629                                    break;
1630                            }
1631                            case(2):
1632                            {
1633                                    ArrayConverter.decodePacked2Bit(buffer, 0, dest, 0, numPackedBytes);
1634                                    while (x <= x2)
1635                                    {
1636                                            if (x >= x1)
1637                                            {
1638                                                    palImage.putByteSample(x - x1, y, dest[offset]);
1639                                            }
1640                                            x += incr;
1641                                            offset++;
1642                                    }
1643                                    break;
1644                            }
1645                            case(4):
1646                            {
1647                                    ArrayConverter.decodePacked4Bit(buffer, 0, dest, 0, numPackedBytes);
1648                                    while (x <= x2)
1649                                    {
1650                                            if (x >= x1)
1651                                            {
1652                                                    palImage.putByteSample(x - x1, y, dest[offset]);
1653                                            }
1654                                            x += incr;
1655                                            offset++;
1656                                    }
1657                                    break;
1658                            }
1659                            case(8):
1660                            {
1661                                    while (x <= x2)
1662                                    {
1663                                            if (x >= x1)
1664                                            {
1665                                                    palImage.putSample(x - x1, y, buffer[offset]);
1666                                            }
1667                                            x += incr;
1668                                            offset++;
1669                                    }
1670                                    break;
1671                            }
1672                    }
1673            }
1674    
1675            private void storeInterlacedAdam7Rgb(int pass, int y, byte[] buffer)
1676            {
1677                    int x = ADAM7_FIRST_COLUMN[pass];
1678                    final int x1 = getBoundsX1();
1679                    final int x2 = getBoundsX2();
1680                    final int incr = ADAM7_COLUMN_INCREMENT[pass];
1681                    int offset = 0;
1682                    if (precision == 8)
1683                    {
1684                            RGB24Image rgbImage = (RGB24Image)image;
1685                            while (x <= x2)
1686                            {
1687                                    if (x >= x1)
1688                                    {
1689                                            rgbImage.putSample(RGB24Image.INDEX_RED, x, y, buffer[offset]);
1690                                            rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, buffer[offset + 1]);
1691                                            rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, buffer[offset + 2]);
1692                                    }
1693                                    x += incr;
1694                                    offset += 3;
1695                            }
1696                    }
1697                    else
1698                    if (precision == 16)
1699                    {
1700                            RGB48Image rgbImage = (RGB48Image)image;
1701                            while (x <= x2)
1702                            {
1703                                    if (x >= x1)
1704                                    {
1705                                            int red = (buffer[offset] & 0xff) << 8;
1706                                            red |= buffer[offset + 1] & 0xff;
1707                                            rgbImage.putSample(RGB24Image.INDEX_RED, x, y, red);
1708            
1709                                            int green = (buffer[offset + 2] & 0xff) << 8;
1710                                            green |= buffer[offset + 3] & 0xff;
1711                                            rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, green);
1712                    
1713                                            int blue = (buffer[offset + 4] & 0xff) << 8;
1714                                            blue |= buffer[offset + 5] & 0xff;
1715                                            rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, blue);
1716                                    }
1717                                    x += incr;
1718                                    offset += 6;
1719                            }
1720                    }
1721            }
1722    
1723            private void storeInterlacedAdam7RgbAlpha(int pass, int y, byte[] buffer)
1724            {
1725                    int x = ADAM7_FIRST_COLUMN[pass];
1726                    final int x1 = getBoundsX1();
1727                    final int x2 = getBoundsX2();
1728                    final int incr = ADAM7_COLUMN_INCREMENT[pass];
1729                    int offset = 0;
1730                    if (precision == 8)
1731                    {
1732                            RGB24Image rgbImage = (RGB24Image)image;
1733                            while (x <= x2)
1734                            {
1735                                    if (x >= x1)
1736                                    {
1737                                            rgbImage.putSample(RGB24Image.INDEX_RED, x, y, buffer[offset]);
1738                                            rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, buffer[offset + 1]);
1739                                            rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, buffer[offset + 2]);
1740                                            // store alpha
1741                                    }
1742                                    x += incr;
1743                                    offset += 4;
1744                            }
1745                    }
1746                    else
1747                    if (precision == 16)
1748                    {
1749                            RGB48Image rgbImage = (RGB48Image)image;
1750                            while (x <= x2)
1751                            {
1752                                    if (x >= x1)
1753                                    {
1754                                            int red = (buffer[offset] & 0xff) << 8;
1755                                            red |= buffer[offset + 1] & 0xff;
1756                                            rgbImage.putSample(RGB24Image.INDEX_RED, x, y, red);
1757            
1758                                            int green = (buffer[offset + 2] & 0xff) << 8;
1759                                            green |= buffer[offset + 3] & 0xff;
1760                                            rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, green);
1761                    
1762                                            int blue = (buffer[offset + 4] & 0xff) << 8;
1763                                            blue |= buffer[offset + 5] & 0xff;
1764                                            rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, blue);
1765                                            
1766                                            // store alpha
1767                                    }
1768                                    x += incr;
1769                                    offset += 8;
1770                            }
1771                    }
1772            }
1773    
1774    
1775            private void storeNonInterlaced(int y, byte[] buffer)
1776            {
1777                    switch(colorType)
1778                    {
1779                            case(COLOR_TYPE_GRAY):
1780                            {
1781                                    storeNonInterlacedGray(y, buffer);
1782                                    break;
1783                            }
1784                            case(COLOR_TYPE_GRAY_ALPHA):
1785                            {
1786                                    storeNonInterlacedGrayAlpha(y, buffer);
1787                                    break;
1788                            }
1789                            case(COLOR_TYPE_INDEXED):
1790                            {
1791                                    storeNonInterlacedIndexed(y, buffer);
1792                                    break;
1793                            }
1794                            case(COLOR_TYPE_RGB):
1795                            {
1796                                    storeNonInterlacedRgb(y, buffer);
1797                                    break;
1798                            }
1799                            case(COLOR_TYPE_RGB_ALPHA):
1800                            {
1801                                    storeNonInterlacedRgbAlpha(y, buffer);
1802                                    break;
1803                            }
1804                    }
1805            }
1806    
1807            private void storeNonInterlacedGray(int y, byte[] buffer)
1808            {
1809                    switch(precision)
1810                    {
1811                            case(1):
1812                            {
1813                                    BilevelImage bilevelImage = (BilevelImage)image;
1814                                    int x1 = getBoundsX1();
1815                                    bilevelImage.putPackedBytes(0, y, getBoundsWidth(), buffer, x1 / 8, x1 % 8);
1816                                    break;
1817                            }
1818                            case(2):
1819                            {
1820                                    Gray8Image grayImage = (Gray8Image)image;
1821                                    byte[] dest = new byte[width + 3];
1822                                    ArrayConverter.convertPacked2BitIntensityTo8Bit(buffer, 0, dest, 0, buffer.length);
1823                                    grayImage.putByteSamples(0, 0, y, getBoundsWidth(), 1, dest, getBoundsX1());
1824                                    break;
1825                            }
1826                            case(4):
1827                            {
1828                                    Gray8Image grayImage = (Gray8Image)image;
1829                                    byte[] dest = new byte[width + 1];
1830                                    ArrayConverter.convertPacked4BitIntensityTo8Bit(buffer, 0, dest, 0, buffer.length);
1831                                    grayImage.putByteSamples(0, 0, y, getBoundsWidth(), 1, dest, getBoundsX1());
1832                                    break;
1833                            }
1834                            case(8):
1835                            {
1836                                    Gray8Image grayImage = (Gray8Image)image;
1837                                    int offset = getBoundsX1();
1838                                    int x = 0;
1839                                    int k = getBoundsWidth();
1840                                    while (k > 0)
1841                                    {
1842                                            grayImage.putSample(0, x++, y, buffer[offset++]);
1843                                            k--;
1844                                    }
1845                                    break;
1846                            }
1847                            case(16):
1848                            {
1849                                    Gray16Image grayImage = (Gray16Image)image;
1850                                    int offset = getBoundsX1();
1851                                    int x = 0;
1852                                    int k = getBoundsWidth();
1853                                    while (k > 0)
1854                                    {
1855                                            int sample = (buffer[offset++] & 0xff) << 8;
1856                                            sample |= (buffer[offset++] & 0xff);
1857                                            grayImage.putSample(x++, y, sample);
1858                                            k--;
1859                                    }
1860                                    break;
1861                            }
1862                    }
1863            }
1864    
1865            private void storeNonInterlacedGrayAlpha(int y, byte[] buffer)
1866            {
1867                    switch(precision)
1868                    {
1869                            case(8):
1870                            {
1871                                    Gray8Image grayImage = (Gray8Image)image;
1872                                    int offset = getBoundsX1();
1873                                    int x = 0;
1874                                    int k = getBoundsWidth();
1875                                    while (k > 0)
1876                                    {
1877                                            grayImage.putSample(0, x++, y, buffer[offset++]);
1878                                            offset++; // skip alpha; should be stored in a TransparencyInformation object
1879                                            k--;
1880                                    }
1881                                    break;
1882                            }
1883                            case(16):
1884                            {
1885                                    Gray16Image grayImage = (Gray16Image)image;
1886                                    int offset = getBoundsX1();
1887                                    int x = 0;
1888                                    int k = getBoundsWidth();
1889                                    while (k > 0)
1890                                    {
1891                                            int sample = (buffer[offset++] & 0xff) << 8;
1892                                            sample |= (buffer[offset++] & 0xff);
1893                                            grayImage.putSample(x++, y, sample);
1894                                            offset += 2; // skip alpha;  TODO: store in TransparencyInformation object
1895                                            k--;
1896                                    }
1897                                    break;
1898                            }
1899                    }
1900            }
1901    
1902            private void storeNonInterlacedIndexed(int y, byte[] buffer)
1903            {
1904                    Paletted8Image palImage = (Paletted8Image)image;
1905                    switch(precision)
1906                    {
1907                            case(1):
1908                            {
1909                                    byte[] dest = new byte[width + 7];
1910                                    ArrayConverter.decodePacked1Bit(buffer, 0, dest, 0, buffer.length);
1911                                    palImage.putByteSamples(0, 0, y, getBoundsWidth(), 1, dest, getBoundsX1());
1912                                    break;
1913                            }
1914                            case(2):
1915                            {
1916                                    byte[] dest = new byte[width + 3];
1917                                    ArrayConverter.decodePacked2Bit(buffer, 0, dest, 0, buffer.length);
1918                                    palImage.putByteSamples(0, 0, y, getBoundsWidth(), 1, dest, getBoundsX1());
1919                                    break;
1920                            }
1921                            case(4):
1922                            {
1923                                    byte[] dest = new byte[width + 1];
1924                                    ArrayConverter.decodePacked4Bit(buffer, 0, dest, 0, buffer.length);
1925                                    palImage.putByteSamples(0, 0, y, getBoundsWidth(), 1, dest, getBoundsX1());
1926                                    break;
1927                            }
1928                            case(8):
1929                            {
1930                                    int offset = getBoundsX1();
1931                                    int x = 0;
1932                                    int k = getBoundsWidth();
1933                                    while (k > 0)
1934                                    {
1935                                            palImage.putSample(0, x++, y, buffer[offset++]);
1936                                            k--;
1937                                    }
1938                                    break;
1939                            }
1940                    }
1941            }
1942    
1943            private void storeNonInterlacedRgb(int y, byte[] buffer)
1944            {
1945                    if (precision == 8)
1946                    {
1947                            RGB24Image rgbImage = (RGB24Image)image;
1948                            int offset = getBoundsX1() * 3;
1949                            int x = 0;
1950                            int k = getBoundsWidth();
1951                            while (k > 0)
1952                            {
1953                                    rgbImage.putSample(RGB24Image.INDEX_RED, x, y, buffer[offset++]);
1954                                    rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, buffer[offset++]);
1955                                    rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, buffer[offset++]);
1956                                    x++;
1957                                    k--;
1958                            }
1959                    }
1960                    else
1961                    if (precision == 16)
1962                    {
1963                            RGB48Image rgbImage = (RGB48Image)image;
1964                            int offset = getBoundsX1() * 6;
1965                            int x = 0;
1966                            int k = getBoundsWidth();
1967                            while (k > 0)
1968                            {
1969                                    int red = (buffer[offset++] & 0xff) << 8;
1970                                    red |= buffer[offset++] & 0xff;
1971                                    rgbImage.putSample(RGB24Image.INDEX_RED, x, y, red);
1972    
1973                                    int green = (buffer[offset++] & 0xff) << 8;
1974                                    green |= buffer[offset++] & 0xff;
1975                                    rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, green);
1976            
1977                                    int blue = (buffer[offset++] & 0xff) << 8;
1978                                    blue |= buffer[offset++] & 0xff;
1979                                    rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, blue);
1980            
1981                                    x++;
1982                                    k--;
1983                            }
1984                    }
1985            }
1986    
1987            private void storeNonInterlacedRgbAlpha(int y, byte[] buffer)
1988            {
1989                    switch(precision)
1990                    {
1991                            case(8):
1992                            {
1993                                    RGB24Image rgbImage = (RGB24Image)image;
1994                                    int offset = getBoundsX1() * 3;
1995                                    int x = 0;
1996                                    int k = getBoundsWidth();
1997                                    while (k > 0)
1998                                    {
1999                                            rgbImage.putSample(RGB24Image.INDEX_RED, x, y, buffer[offset++]);
2000                                            rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, buffer[offset++]);
2001                                            rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, buffer[offset++]);
2002                                            offset++; // skip alpha; TODO: store in TransparencyInformation object
2003                                            x++;
2004                                            k--;
2005                                    }
2006                                    break;
2007                            }
2008                            case(16):
2009                            {
2010                                    RGB48Image rgbImage = (RGB48Image)image;
2011                                    int offset = getBoundsX1() * 8;
2012                                    int x = 0;
2013                                    int k = getBoundsWidth();
2014                                    while (k > 0)
2015                                    {
2016                                            int red = (buffer[offset++] & 0xff) << 8;
2017                                            red |= buffer[offset++] & 0xff;
2018                                            rgbImage.putSample(RGB24Image.INDEX_RED, x, y, red);
2019                    
2020                                            int green = (buffer[offset++] & 0xff) << 8;
2021                                            green |= buffer[offset++] & 0xff;
2022                                            rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, green);
2023                    
2024                                            int blue = (buffer[offset++] & 0xff) << 8;
2025                                            blue |= buffer[offset++] & 0xff;
2026                                            rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, blue);
2027                    
2028                                            offset += 2; // skip alpha; TODO: store in TransparencyInformation object
2029                                            x++;
2030                                            k--;
2031                                    }
2032                                    break;
2033                            }
2034                    }
2035            }
2036    
2037            public String suggestFileExtension(PixelImage image)
2038            {
2039                    return ".png";
2040            }
2041    }