1/*
2 * Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package com.sun.imageio.plugins.png;
27
28import java.awt.Rectangle;
29import java.awt.image.IndexColorModel;
30import java.awt.image.Raster;
31import java.awt.image.WritableRaster;
32import java.awt.image.RenderedImage;
33import java.awt.image.SampleModel;
34import java.io.ByteArrayOutputStream;
35import java.io.IOException;
36import java.util.Iterator;
37import java.util.Locale;
38import java.util.zip.Deflater;
39import java.util.zip.DeflaterOutputStream;
40import javax.imageio.IIOException;
41import javax.imageio.IIOImage;
42import javax.imageio.ImageTypeSpecifier;
43import javax.imageio.ImageWriteParam;
44import javax.imageio.ImageWriter;
45import javax.imageio.metadata.IIOMetadata;
46import javax.imageio.spi.ImageWriterSpi;
47import javax.imageio.stream.ImageOutputStream;
48import javax.imageio.stream.ImageOutputStreamImpl;
49
50final class CRC {
51
52    private static final int[] crcTable = new int[256];
53    private int crc = 0xffffffff;
54
55    static {
56        // Initialize CRC table
57        for (int n = 0; n < 256; n++) {
58            int c = n;
59            for (int k = 0; k < 8; k++) {
60                if ((c & 1) == 1) {
61                    c = 0xedb88320 ^ (c >>> 1);
62                } else {
63                    c >>>= 1;
64                }
65
66                crcTable[n] = c;
67            }
68        }
69    }
70
71    CRC() {}
72
73    void reset() {
74        crc = 0xffffffff;
75    }
76
77    void update(byte[] data, int off, int len) {
78        int c = crc;
79        for (int n = 0; n < len; n++) {
80            c = crcTable[(c ^ data[off + n]) & 0xff] ^ (c >>> 8);
81        }
82        crc = c;
83    }
84
85    void update(int data) {
86        crc = crcTable[(crc ^ data) & 0xff] ^ (crc >>> 8);
87    }
88
89    int getValue() {
90        return crc ^ 0xffffffff;
91    }
92}
93
94
95final class ChunkStream extends ImageOutputStreamImpl {
96
97    private final ImageOutputStream stream;
98    private final long startPos;
99    private final CRC crc = new CRC();
100
101    ChunkStream(int type, ImageOutputStream stream) throws IOException {
102        this.stream = stream;
103        this.startPos = stream.getStreamPosition();
104
105        stream.writeInt(-1); // length, will backpatch
106        writeInt(type);
107    }
108
109    @Override
110    public int read() throws IOException {
111        throw new RuntimeException("Method not available");
112    }
113
114    @Override
115    public int read(byte[] b, int off, int len) throws IOException {
116        throw new RuntimeException("Method not available");
117    }
118
119    @Override
120    public void write(byte[] b, int off, int len) throws IOException {
121        crc.update(b, off, len);
122        stream.write(b, off, len);
123    }
124
125    @Override
126    public void write(int b) throws IOException {
127        crc.update(b);
128        stream.write(b);
129    }
130
131    void finish() throws IOException {
132        // Write CRC
133        stream.writeInt(crc.getValue());
134
135        // Write length
136        long pos = stream.getStreamPosition();
137        stream.seek(startPos);
138        stream.writeInt((int)(pos - startPos) - 12);
139
140        // Return to end of chunk and flush to minimize buffering
141        stream.seek(pos);
142        stream.flushBefore(pos);
143    }
144
145    @Override
146    @SuppressWarnings("deprecation")
147    protected void finalize() throws Throwable {
148        // Empty finalizer (for improved performance; no need to call
149        // super.finalize() in this case)
150    }
151}
152
153// Compress output and write as a series of 'IDAT' chunks of
154// fixed length.
155final class IDATOutputStream extends ImageOutputStreamImpl {
156
157    private static final byte[] chunkType = {
158        (byte)'I', (byte)'D', (byte)'A', (byte)'T'
159    };
160
161    private final ImageOutputStream stream;
162    private final int chunkLength;
163    private long startPos;
164    private final CRC crc = new CRC();
165
166    private final Deflater def;
167    private final byte[] buf = new byte[512];
168    // reused 1 byte[] array:
169    private final byte[] wbuf1 = new byte[1];
170
171    private int bytesRemaining;
172
173    IDATOutputStream(ImageOutputStream stream, int chunkLength,
174                            int deflaterLevel) throws IOException
175    {
176        this.stream = stream;
177        this.chunkLength = chunkLength;
178        this.def = new Deflater(deflaterLevel);
179
180        startChunk();
181    }
182
183    private void startChunk() throws IOException {
184        crc.reset();
185        this.startPos = stream.getStreamPosition();
186        stream.writeInt(-1); // length, will backpatch
187
188        crc.update(chunkType, 0, 4);
189        stream.write(chunkType, 0, 4);
190
191        this.bytesRemaining = chunkLength;
192    }
193
194    private void finishChunk() throws IOException {
195        // Write CRC
196        stream.writeInt(crc.getValue());
197
198        // Write length
199        long pos = stream.getStreamPosition();
200        stream.seek(startPos);
201        stream.writeInt((int)(pos - startPos) - 12);
202
203        // Return to end of chunk and flush to minimize buffering
204        stream.seek(pos);
205        try {
206            stream.flushBefore(pos);
207        } catch (IOException e) {
208            /*
209             * If flushBefore() fails we try to access startPos in finally
210             * block of write_IDAT(). We should update startPos to avoid
211             * IndexOutOfBoundException while seek() is happening.
212             */
213            this.startPos = stream.getStreamPosition();
214            throw e;
215        }
216    }
217
218    @Override
219    public int read() throws IOException {
220        throw new RuntimeException("Method not available");
221    }
222
223    @Override
224    public int read(byte[] b, int off, int len) throws IOException {
225        throw new RuntimeException("Method not available");
226    }
227
228    @Override
229    public void write(byte[] b, int off, int len) throws IOException {
230        if (len == 0) {
231            return;
232        }
233
234        if (!def.finished()) {
235            def.setInput(b, off, len);
236            while (!def.needsInput()) {
237                deflate();
238            }
239        }
240    }
241
242    void deflate() throws IOException {
243        int len = def.deflate(buf, 0, buf.length);
244        int off = 0;
245
246        while (len > 0) {
247            if (bytesRemaining == 0) {
248                finishChunk();
249                startChunk();
250            }
251
252            int nbytes = Math.min(len, bytesRemaining);
253            crc.update(buf, off, nbytes);
254            stream.write(buf, off, nbytes);
255
256            off += nbytes;
257            len -= nbytes;
258            bytesRemaining -= nbytes;
259        }
260    }
261
262    @Override
263    public void write(int b) throws IOException {
264        wbuf1[0] = (byte)b;
265        write(wbuf1, 0, 1);
266    }
267
268    void finish() throws IOException {
269        try {
270            if (!def.finished()) {
271                def.finish();
272                while (!def.finished()) {
273                    deflate();
274                }
275            }
276            finishChunk();
277        } finally {
278            def.end();
279        }
280    }
281
282    @Override
283    @SuppressWarnings("deprecation")
284    protected void finalize() throws Throwable {
285        // Empty finalizer (for improved performance; no need to call
286        // super.finalize() in this case)
287    }
288}
289
290
291final class PNGImageWriteParam extends ImageWriteParam {
292
293    /** Default quality level = 0.5 ie medium compression */
294    private static final float DEFAULT_QUALITY = 0.5f;
295
296    private static final String[] compressionNames = {"Deflate"};
297    private static final float[] qualityVals = { 0.00F, 0.30F, 0.75F, 1.00F };
298    private static final String[] qualityDescs = {
299        "High compression",   // 0.00 -> 0.30
300        "Medium compression", // 0.30 -> 0.75
301        "Low compression"     // 0.75 -> 1.00
302    };
303
304    PNGImageWriteParam(Locale locale) {
305        super();
306        this.canWriteProgressive = true;
307        this.locale = locale;
308        this.canWriteCompressed = true;
309        this.compressionTypes = compressionNames;
310        this.compressionType = compressionTypes[0];
311        this.compressionMode = MODE_DEFAULT;
312        this.compressionQuality = DEFAULT_QUALITY;
313    }
314
315    /**
316     * Removes any previous compression quality setting.
317     *
318     * <p> The default implementation resets the compression quality
319     * to <code>0.5F</code>.
320     *
321     * @exception IllegalStateException if the compression mode is not
322     * <code>MODE_EXPLICIT</code>.
323     */
324    @Override
325    public void unsetCompression() {
326        super.unsetCompression();
327        this.compressionType = compressionTypes[0];
328        this.compressionQuality = DEFAULT_QUALITY;
329    }
330
331    /**
332     * Returns <code>true</code> since the PNG plug-in only supports
333     * lossless compression.
334     *
335     * @return <code>true</code>.
336     */
337    @Override
338    public boolean isCompressionLossless() {
339        return true;
340    }
341
342    @Override
343    public String[] getCompressionQualityDescriptions() {
344        super.getCompressionQualityDescriptions();
345        return qualityDescs.clone();
346    }
347
348    @Override
349    public float[] getCompressionQualityValues() {
350        super.getCompressionQualityValues();
351        return qualityVals.clone();
352    }
353}
354
355/**
356 */
357public final class PNGImageWriter extends ImageWriter {
358
359    /** Default compression level = 4 ie medium compression */
360    private static final int DEFAULT_COMPRESSION_LEVEL = 4;
361
362    ImageOutputStream stream = null;
363
364    PNGMetadata metadata = null;
365
366    // Factors from the ImageWriteParam
367    int sourceXOffset = 0;
368    int sourceYOffset = 0;
369    int sourceWidth = 0;
370    int sourceHeight = 0;
371    int[] sourceBands = null;
372    int periodX = 1;
373    int periodY = 1;
374
375    int numBands;
376    int bpp;
377
378    RowFilter rowFilter = new RowFilter();
379    byte[] prevRow = null;
380    byte[] currRow = null;
381    byte[][] filteredRows = null;
382
383    // Per-band scaling tables
384    //
385    // After the first call to initializeScaleTables, either scale and scale0
386    // will be valid, or scaleh and scalel will be valid, but not both.
387    //
388    // The tables will be designed for use with a set of input but depths
389    // given by sampleSize, and an output bit depth given by scalingBitDepth.
390    //
391    int[] sampleSize = null; // Sample size per band, in bits
392    int scalingBitDepth = -1; // Output bit depth of the scaling tables
393
394    // Tables for 1, 2, 4, or 8 bit output
395    byte[][] scale = null; // 8 bit table
396    byte[] scale0 = null; // equivalent to scale[0]
397
398    // Tables for 16 bit output
399    byte[][] scaleh = null; // High bytes of output
400    byte[][] scalel = null; // Low bytes of output
401
402    int totalPixels; // Total number of pixels to be written by write_IDAT
403    int pixelsDone; // Running count of pixels written by write_IDAT
404
405    public PNGImageWriter(ImageWriterSpi originatingProvider) {
406        super(originatingProvider);
407    }
408
409    @Override
410    public void setOutput(Object output) {
411        super.setOutput(output);
412        if (output != null) {
413            if (!(output instanceof ImageOutputStream)) {
414                throw new IllegalArgumentException("output not an ImageOutputStream!");
415            }
416            this.stream = (ImageOutputStream)output;
417        } else {
418            this.stream = null;
419        }
420    }
421
422    @Override
423    public ImageWriteParam getDefaultWriteParam() {
424        return new PNGImageWriteParam(getLocale());
425    }
426
427    @Override
428    public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
429        return null;
430    }
431
432    @Override
433    public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType,
434                                               ImageWriteParam param) {
435        PNGMetadata m = new PNGMetadata();
436        m.initialize(imageType, imageType.getSampleModel().getNumBands());
437        return m;
438    }
439
440    @Override
441    public IIOMetadata convertStreamMetadata(IIOMetadata inData,
442                                             ImageWriteParam param) {
443        return null;
444    }
445
446    @Override
447    public IIOMetadata convertImageMetadata(IIOMetadata inData,
448                                            ImageTypeSpecifier imageType,
449                                            ImageWriteParam param) {
450        // TODO - deal with imageType
451        if (inData instanceof PNGMetadata) {
452            return (PNGMetadata)((PNGMetadata)inData).clone();
453        } else {
454            return new PNGMetadata(inData);
455        }
456    }
457
458    private void write_magic() throws IOException {
459        // Write signature
460        byte[] magic = { (byte)137, 80, 78, 71, 13, 10, 26, 10 };
461        stream.write(magic);
462    }
463
464    private void write_IHDR() throws IOException {
465        // Write IHDR chunk
466        ChunkStream cs = new ChunkStream(PNGImageReader.IHDR_TYPE, stream);
467        cs.writeInt(metadata.IHDR_width);
468        cs.writeInt(metadata.IHDR_height);
469        cs.writeByte(metadata.IHDR_bitDepth);
470        cs.writeByte(metadata.IHDR_colorType);
471        if (metadata.IHDR_compressionMethod != 0) {
472            throw new IIOException(
473"Only compression method 0 is defined in PNG 1.1");
474        }
475        cs.writeByte(metadata.IHDR_compressionMethod);
476        if (metadata.IHDR_filterMethod != 0) {
477            throw new IIOException(
478"Only filter method 0 is defined in PNG 1.1");
479        }
480        cs.writeByte(metadata.IHDR_filterMethod);
481        if (metadata.IHDR_interlaceMethod < 0 ||
482            metadata.IHDR_interlaceMethod > 1) {
483            throw new IIOException(
484"Only interlace methods 0 (node) and 1 (adam7) are defined in PNG 1.1");
485        }
486        cs.writeByte(metadata.IHDR_interlaceMethod);
487        cs.finish();
488    }
489
490    private void write_cHRM() throws IOException {
491        if (metadata.cHRM_present) {
492            ChunkStream cs = new ChunkStream(PNGImageReader.cHRM_TYPE, stream);
493            cs.writeInt(metadata.cHRM_whitePointX);
494            cs.writeInt(metadata.cHRM_whitePointY);
495            cs.writeInt(metadata.cHRM_redX);
496            cs.writeInt(metadata.cHRM_redY);
497            cs.writeInt(metadata.cHRM_greenX);
498            cs.writeInt(metadata.cHRM_greenY);
499            cs.writeInt(metadata.cHRM_blueX);
500            cs.writeInt(metadata.cHRM_blueY);
501            cs.finish();
502        }
503    }
504
505    private void write_gAMA() throws IOException {
506        if (metadata.gAMA_present) {
507            ChunkStream cs = new ChunkStream(PNGImageReader.gAMA_TYPE, stream);
508            cs.writeInt(metadata.gAMA_gamma);
509            cs.finish();
510        }
511    }
512
513    private void write_iCCP() throws IOException {
514        if (metadata.iCCP_present) {
515            ChunkStream cs = new ChunkStream(PNGImageReader.iCCP_TYPE, stream);
516            cs.writeBytes(metadata.iCCP_profileName);
517            cs.writeByte(0); // null terminator
518
519            cs.writeByte(metadata.iCCP_compressionMethod);
520            cs.write(metadata.iCCP_compressedProfile);
521            cs.finish();
522        }
523    }
524
525    private void write_sBIT() throws IOException {
526        if (metadata.sBIT_present) {
527            ChunkStream cs = new ChunkStream(PNGImageReader.sBIT_TYPE, stream);
528            int colorType = metadata.IHDR_colorType;
529            if (metadata.sBIT_colorType != colorType) {
530                processWarningOccurred(0,
531"sBIT metadata has wrong color type.\n" +
532"The chunk will not be written.");
533                return;
534            }
535
536            if (colorType == PNGImageReader.PNG_COLOR_GRAY ||
537                colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
538                cs.writeByte(metadata.sBIT_grayBits);
539            } else if (colorType == PNGImageReader.PNG_COLOR_RGB ||
540                       colorType == PNGImageReader.PNG_COLOR_PALETTE ||
541                       colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) {
542                cs.writeByte(metadata.sBIT_redBits);
543                cs.writeByte(metadata.sBIT_greenBits);
544                cs.writeByte(metadata.sBIT_blueBits);
545            }
546
547            if (colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA ||
548                colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) {
549                cs.writeByte(metadata.sBIT_alphaBits);
550            }
551            cs.finish();
552        }
553    }
554
555    private void write_sRGB() throws IOException {
556        if (metadata.sRGB_present) {
557            ChunkStream cs = new ChunkStream(PNGImageReader.sRGB_TYPE, stream);
558            cs.writeByte(metadata.sRGB_renderingIntent);
559            cs.finish();
560        }
561    }
562
563    private void write_PLTE() throws IOException {
564        if (metadata.PLTE_present) {
565            if (metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY ||
566              metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
567                // PLTE cannot occur in a gray image
568
569                processWarningOccurred(0,
570"A PLTE chunk may not appear in a gray or gray alpha image.\n" +
571"The chunk will not be written");
572                return;
573            }
574
575            ChunkStream cs = new ChunkStream(PNGImageReader.PLTE_TYPE, stream);
576
577            int numEntries = metadata.PLTE_red.length;
578            byte[] palette = new byte[numEntries*3];
579            int index = 0;
580            for (int i = 0; i < numEntries; i++) {
581                palette[index++] = metadata.PLTE_red[i];
582                palette[index++] = metadata.PLTE_green[i];
583                palette[index++] = metadata.PLTE_blue[i];
584            }
585
586            cs.write(palette);
587            cs.finish();
588        }
589    }
590
591    private void write_hIST() throws IOException, IIOException {
592        if (metadata.hIST_present) {
593            ChunkStream cs = new ChunkStream(PNGImageReader.hIST_TYPE, stream);
594
595            if (!metadata.PLTE_present) {
596                throw new IIOException("hIST chunk without PLTE chunk!");
597            }
598
599            cs.writeChars(metadata.hIST_histogram,
600                          0, metadata.hIST_histogram.length);
601            cs.finish();
602        }
603    }
604
605    private void write_tRNS() throws IOException, IIOException {
606        if (metadata.tRNS_present) {
607            ChunkStream cs = new ChunkStream(PNGImageReader.tRNS_TYPE, stream);
608            int colorType = metadata.IHDR_colorType;
609            int chunkType = metadata.tRNS_colorType;
610
611            // Special case: image is RGB and chunk is Gray
612            // Promote chunk contents to RGB
613            int chunkRed = metadata.tRNS_red;
614            int chunkGreen = metadata.tRNS_green;
615            int chunkBlue = metadata.tRNS_blue;
616            if (colorType == PNGImageReader.PNG_COLOR_RGB &&
617                chunkType == PNGImageReader.PNG_COLOR_GRAY) {
618                chunkType = colorType;
619                chunkRed = chunkGreen = chunkBlue =
620                    metadata.tRNS_gray;
621            }
622
623            if (chunkType != colorType) {
624                processWarningOccurred(0,
625"tRNS metadata has incompatible color type.\n" +
626"The chunk will not be written.");
627                return;
628            }
629
630            if (colorType == PNGImageReader.PNG_COLOR_PALETTE) {
631                if (!metadata.PLTE_present) {
632                    throw new IIOException("tRNS chunk without PLTE chunk!");
633                }
634                cs.write(metadata.tRNS_alpha);
635            } else if (colorType == PNGImageReader.PNG_COLOR_GRAY) {
636                cs.writeShort(metadata.tRNS_gray);
637            } else if (colorType == PNGImageReader.PNG_COLOR_RGB) {
638                cs.writeShort(chunkRed);
639                cs.writeShort(chunkGreen);
640                cs.writeShort(chunkBlue);
641            } else {
642                throw new IIOException("tRNS chunk for color type 4 or 6!");
643            }
644            cs.finish();
645        }
646    }
647
648    private void write_bKGD() throws IOException {
649        if (metadata.bKGD_present) {
650            ChunkStream cs = new ChunkStream(PNGImageReader.bKGD_TYPE, stream);
651            int colorType = metadata.IHDR_colorType & 0x3;
652            int chunkType = metadata.bKGD_colorType;
653
654            // Special case: image is RGB(A) and chunk is Gray
655            // Promote chunk contents to RGB
656            int chunkRed = metadata.bKGD_red;
657            int chunkGreen = metadata.bKGD_red;
658            int chunkBlue = metadata.bKGD_red;
659            if (colorType == PNGImageReader.PNG_COLOR_RGB &&
660                chunkType == PNGImageReader.PNG_COLOR_GRAY) {
661                // Make a gray bKGD chunk look like RGB
662                chunkType = colorType;
663                chunkRed = chunkGreen = chunkBlue =
664                    metadata.bKGD_gray;
665            }
666
667            // Ignore status of alpha in colorType
668            if (chunkType != colorType) {
669                processWarningOccurred(0,
670"bKGD metadata has incompatible color type.\n" +
671"The chunk will not be written.");
672                return;
673            }
674
675            if (colorType == PNGImageReader.PNG_COLOR_PALETTE) {
676                cs.writeByte(metadata.bKGD_index);
677            } else if (colorType == PNGImageReader.PNG_COLOR_GRAY ||
678                       colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
679                cs.writeShort(metadata.bKGD_gray);
680            } else { // colorType == PNGImageReader.PNG_COLOR_RGB ||
681                     // colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA
682                cs.writeShort(chunkRed);
683                cs.writeShort(chunkGreen);
684                cs.writeShort(chunkBlue);
685            }
686            cs.finish();
687        }
688    }
689
690    private void write_pHYs() throws IOException {
691        if (metadata.pHYs_present) {
692            ChunkStream cs = new ChunkStream(PNGImageReader.pHYs_TYPE, stream);
693            cs.writeInt(metadata.pHYs_pixelsPerUnitXAxis);
694            cs.writeInt(metadata.pHYs_pixelsPerUnitYAxis);
695            cs.writeByte(metadata.pHYs_unitSpecifier);
696            cs.finish();
697        }
698    }
699
700    private void write_sPLT() throws IOException {
701        if (metadata.sPLT_present) {
702            ChunkStream cs = new ChunkStream(PNGImageReader.sPLT_TYPE, stream);
703
704            cs.writeBytes(metadata.sPLT_paletteName);
705            cs.writeByte(0); // null terminator
706
707            cs.writeByte(metadata.sPLT_sampleDepth);
708            int numEntries = metadata.sPLT_red.length;
709
710            if (metadata.sPLT_sampleDepth == 8) {
711                for (int i = 0; i < numEntries; i++) {
712                    cs.writeByte(metadata.sPLT_red[i]);
713                    cs.writeByte(metadata.sPLT_green[i]);
714                    cs.writeByte(metadata.sPLT_blue[i]);
715                    cs.writeByte(metadata.sPLT_alpha[i]);
716                    cs.writeShort(metadata.sPLT_frequency[i]);
717                }
718            } else { // sampleDepth == 16
719                for (int i = 0; i < numEntries; i++) {
720                    cs.writeShort(metadata.sPLT_red[i]);
721                    cs.writeShort(metadata.sPLT_green[i]);
722                    cs.writeShort(metadata.sPLT_blue[i]);
723                    cs.writeShort(metadata.sPLT_alpha[i]);
724                    cs.writeShort(metadata.sPLT_frequency[i]);
725                }
726            }
727            cs.finish();
728        }
729    }
730
731    private void write_tIME() throws IOException {
732        if (metadata.tIME_present) {
733            ChunkStream cs = new ChunkStream(PNGImageReader.tIME_TYPE, stream);
734            cs.writeShort(metadata.tIME_year);
735            cs.writeByte(metadata.tIME_month);
736            cs.writeByte(metadata.tIME_day);
737            cs.writeByte(metadata.tIME_hour);
738            cs.writeByte(metadata.tIME_minute);
739            cs.writeByte(metadata.tIME_second);
740            cs.finish();
741        }
742    }
743
744    private void write_tEXt() throws IOException {
745        Iterator<String> keywordIter = metadata.tEXt_keyword.iterator();
746        Iterator<String> textIter = metadata.tEXt_text.iterator();
747
748        while (keywordIter.hasNext()) {
749            ChunkStream cs = new ChunkStream(PNGImageReader.tEXt_TYPE, stream);
750            String keyword = keywordIter.next();
751            cs.writeBytes(keyword);
752            cs.writeByte(0);
753
754            String text = textIter.next();
755            cs.writeBytes(text);
756            cs.finish();
757        }
758    }
759
760    private byte[] deflate(byte[] b) throws IOException {
761        ByteArrayOutputStream baos = new ByteArrayOutputStream();
762        DeflaterOutputStream dos = new DeflaterOutputStream(baos);
763        dos.write(b);
764        dos.close();
765        return baos.toByteArray();
766    }
767
768    private void write_iTXt() throws IOException {
769        Iterator<String> keywordIter = metadata.iTXt_keyword.iterator();
770        Iterator<Boolean> flagIter = metadata.iTXt_compressionFlag.iterator();
771        Iterator<Integer> methodIter = metadata.iTXt_compressionMethod.iterator();
772        Iterator<String> languageIter = metadata.iTXt_languageTag.iterator();
773        Iterator<String> translatedKeywordIter =
774            metadata.iTXt_translatedKeyword.iterator();
775        Iterator<String> textIter = metadata.iTXt_text.iterator();
776
777        while (keywordIter.hasNext()) {
778            ChunkStream cs = new ChunkStream(PNGImageReader.iTXt_TYPE, stream);
779
780            cs.writeBytes(keywordIter.next());
781            cs.writeByte(0);
782
783            Boolean compressed = flagIter.next();
784            cs.writeByte(compressed ? 1 : 0);
785
786            cs.writeByte(methodIter.next().intValue());
787
788            cs.writeBytes(languageIter.next());
789            cs.writeByte(0);
790
791
792            cs.write(translatedKeywordIter.next().getBytes("UTF8"));
793            cs.writeByte(0);
794
795            String text = textIter.next();
796            if (compressed) {
797                cs.write(deflate(text.getBytes("UTF8")));
798            } else {
799                cs.write(text.getBytes("UTF8"));
800            }
801            cs.finish();
802        }
803    }
804
805    private void write_zTXt() throws IOException {
806        Iterator<String> keywordIter = metadata.zTXt_keyword.iterator();
807        Iterator<Integer> methodIter = metadata.zTXt_compressionMethod.iterator();
808        Iterator<String> textIter = metadata.zTXt_text.iterator();
809
810        while (keywordIter.hasNext()) {
811            ChunkStream cs = new ChunkStream(PNGImageReader.zTXt_TYPE, stream);
812            String keyword = keywordIter.next();
813            cs.writeBytes(keyword);
814            cs.writeByte(0);
815
816            int compressionMethod = (methodIter.next()).intValue();
817            cs.writeByte(compressionMethod);
818
819            String text = textIter.next();
820            cs.write(deflate(text.getBytes("ISO-8859-1")));
821            cs.finish();
822        }
823    }
824
825    private void writeUnknownChunks() throws IOException {
826        Iterator<String> typeIter = metadata.unknownChunkType.iterator();
827        Iterator<byte[]> dataIter = metadata.unknownChunkData.iterator();
828
829        while (typeIter.hasNext() && dataIter.hasNext()) {
830            String type = typeIter.next();
831            ChunkStream cs = new ChunkStream(chunkType(type), stream);
832            byte[] data = dataIter.next();
833            cs.write(data);
834            cs.finish();
835        }
836    }
837
838    private static int chunkType(String typeString) {
839        char c0 = typeString.charAt(0);
840        char c1 = typeString.charAt(1);
841        char c2 = typeString.charAt(2);
842        char c3 = typeString.charAt(3);
843
844        int type = (c0 << 24) | (c1 << 16) | (c2 << 8) | c3;
845        return type;
846    }
847
848    private void encodePass(ImageOutputStream os,
849                            RenderedImage image,
850                            int xOffset, int yOffset,
851                            int xSkip, int ySkip) throws IOException {
852        int minX = sourceXOffset;
853        int minY = sourceYOffset;
854        int width = sourceWidth;
855        int height = sourceHeight;
856
857        // Adjust offsets and skips based on source subsampling factors
858        xOffset *= periodX;
859        xSkip *= periodX;
860        yOffset *= periodY;
861        ySkip *= periodY;
862
863        // Early exit if no data for this pass
864        int hpixels = (width - xOffset + xSkip - 1)/xSkip;
865        int vpixels = (height - yOffset + ySkip - 1)/ySkip;
866        if (hpixels == 0 || vpixels == 0) {
867            return;
868        }
869
870        // Convert X offset and skip from pixels to samples
871        xOffset *= numBands;
872        xSkip *= numBands;
873
874        // Create row buffers
875        int samplesPerByte = 8/metadata.IHDR_bitDepth;
876        int numSamples = width*numBands;
877        int[] samples = new int[numSamples];
878
879        int bytesPerRow = hpixels*numBands;
880        if (metadata.IHDR_bitDepth < 8) {
881            bytesPerRow = (bytesPerRow + samplesPerByte - 1)/samplesPerByte;
882        } else if (metadata.IHDR_bitDepth == 16) {
883            bytesPerRow *= 2;
884        }
885
886        IndexColorModel icm_gray_alpha = null;
887        if (metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA &&
888            image.getColorModel() instanceof IndexColorModel)
889        {
890            // reserve space for alpha samples
891            bytesPerRow *= 2;
892
893            // will be used to calculate alpha value for the pixel
894            icm_gray_alpha = (IndexColorModel)image.getColorModel();
895        }
896
897        currRow = new byte[bytesPerRow + bpp];
898        prevRow = new byte[bytesPerRow + bpp];
899        filteredRows = new byte[5][bytesPerRow + bpp];
900
901        int bitDepth = metadata.IHDR_bitDepth;
902        for (int row = minY + yOffset; row < minY + height; row += ySkip) {
903            Rectangle rect = new Rectangle(minX, row, width, 1);
904            Raster ras = image.getData(rect);
905            if (sourceBands != null) {
906                ras = ras.createChild(minX, row, width, 1, minX, row,
907                                      sourceBands);
908            }
909
910            ras.getPixels(minX, row, width, 1, samples);
911
912            if (image.getColorModel().isAlphaPremultiplied()) {
913                WritableRaster wr = ras.createCompatibleWritableRaster();
914                wr.setPixels(wr.getMinX(), wr.getMinY(),
915                             wr.getWidth(), wr.getHeight(),
916                             samples);
917
918                image.getColorModel().coerceData(wr, false);
919                wr.getPixels(wr.getMinX(), wr.getMinY(),
920                             wr.getWidth(), wr.getHeight(),
921                             samples);
922            }
923
924            // Reorder palette data if necessary
925            int[] paletteOrder = metadata.PLTE_order;
926            if (paletteOrder != null) {
927                for (int i = 0; i < numSamples; i++) {
928                    samples[i] = paletteOrder[samples[i]];
929                }
930            }
931
932            int count = bpp; // leave first 'bpp' bytes zero
933            int pos = 0;
934            int tmp = 0;
935
936            switch (bitDepth) {
937            case 1: case 2: case 4:
938                // Image can only have a single band
939
940                int mask = samplesPerByte - 1;
941                for (int s = xOffset; s < numSamples; s += xSkip) {
942                    byte val = scale0[samples[s]];
943                    tmp = (tmp << bitDepth) | val;
944
945                    if ((pos++ & mask) == mask) {
946                        currRow[count++] = (byte)tmp;
947                        tmp = 0;
948                        pos = 0;
949                    }
950                }
951
952                // Left shift the last byte
953                if ((pos & mask) != 0) {
954                    tmp <<= ((8/bitDepth) - pos)*bitDepth;
955                    currRow[count++] = (byte)tmp;
956                }
957                break;
958
959            case 8:
960                if (numBands == 1) {
961                    for (int s = xOffset; s < numSamples; s += xSkip) {
962                        currRow[count++] = scale0[samples[s]];
963                        if (icm_gray_alpha != null) {
964                            currRow[count++] =
965                                scale0[icm_gray_alpha.getAlpha(0xff & samples[s])];
966                        }
967                    }
968                } else {
969                    for (int s = xOffset; s < numSamples; s += xSkip) {
970                        for (int b = 0; b < numBands; b++) {
971                            currRow[count++] = scale[b][samples[s + b]];
972                        }
973                    }
974                }
975                break;
976
977            case 16:
978                for (int s = xOffset; s < numSamples; s += xSkip) {
979                    for (int b = 0; b < numBands; b++) {
980                        currRow[count++] = scaleh[b][samples[s + b]];
981                        currRow[count++] = scalel[b][samples[s + b]];
982                    }
983                }
984                break;
985            }
986
987            // Perform filtering
988            int filterType = rowFilter.filterRow(metadata.IHDR_colorType,
989                                                 currRow, prevRow,
990                                                 filteredRows,
991                                                 bytesPerRow, bpp);
992
993            os.write(filterType);
994            os.write(filteredRows[filterType], bpp, bytesPerRow);
995
996            // Swap current and previous rows
997            byte[] swap = currRow;
998            currRow = prevRow;
999            prevRow = swap;
1000
1001            pixelsDone += hpixels;
1002            processImageProgress(100.0F*pixelsDone/totalPixels);
1003
1004            // If write has been aborted, just return;
1005            // processWriteAborted will be called later
1006            if (abortRequested()) {
1007                return;
1008            }
1009        }
1010    }
1011
1012    // Use sourceXOffset, etc.
1013    private void write_IDAT(RenderedImage image, int deflaterLevel)
1014        throws IOException
1015    {
1016        IDATOutputStream ios = new IDATOutputStream(stream, 32768,
1017                                                    deflaterLevel);
1018        try {
1019            if (metadata.IHDR_interlaceMethod == 1) {
1020                for (int i = 0; i < 7; i++) {
1021                    encodePass(ios, image,
1022                               PNGImageReader.adam7XOffset[i],
1023                               PNGImageReader.adam7YOffset[i],
1024                               PNGImageReader.adam7XSubsampling[i],
1025                               PNGImageReader.adam7YSubsampling[i]);
1026                    if (abortRequested()) {
1027                        break;
1028                    }
1029                }
1030            } else {
1031                encodePass(ios, image, 0, 0, 1, 1);
1032            }
1033        } finally {
1034            ios.finish();
1035        }
1036    }
1037
1038    private void writeIEND() throws IOException {
1039        ChunkStream cs = new ChunkStream(PNGImageReader.IEND_TYPE, stream);
1040        cs.finish();
1041    }
1042
1043    // Check two int arrays for value equality, always returns false
1044    // if either array is null
1045    private boolean equals(int[] s0, int[] s1) {
1046        if (s0 == null || s1 == null) {
1047            return false;
1048        }
1049        if (s0.length != s1.length) {
1050            return false;
1051        }
1052        for (int i = 0; i < s0.length; i++) {
1053            if (s0[i] != s1[i]) {
1054                return false;
1055            }
1056        }
1057        return true;
1058    }
1059
1060    // Initialize the scale/scale0 or scaleh/scalel arrays to
1061    // hold the results of scaling an input value to the desired
1062    // output bit depth
1063    private void initializeScaleTables(int[] sampleSize) {
1064        int bitDepth = metadata.IHDR_bitDepth;
1065
1066        // If the existing tables are still valid, just return
1067        if (bitDepth == scalingBitDepth &&
1068            equals(sampleSize, this.sampleSize)) {
1069            return;
1070        }
1071
1072        // Compute new tables
1073        this.sampleSize = sampleSize;
1074        this.scalingBitDepth = bitDepth;
1075        int maxOutSample = (1 << bitDepth) - 1;
1076        if (bitDepth <= 8) {
1077            scale = new byte[numBands][];
1078            for (int b = 0; b < numBands; b++) {
1079                int maxInSample = (1 << sampleSize[b]) - 1;
1080                int halfMaxInSample = maxInSample/2;
1081                scale[b] = new byte[maxInSample + 1];
1082                for (int s = 0; s <= maxInSample; s++) {
1083                    scale[b][s] =
1084                        (byte)((s*maxOutSample + halfMaxInSample)/maxInSample);
1085                }
1086            }
1087            scale0 = scale[0];
1088            scaleh = scalel = null;
1089        } else { // bitDepth == 16
1090            // Divide scaling table into high and low bytes
1091            scaleh = new byte[numBands][];
1092            scalel = new byte[numBands][];
1093
1094            for (int b = 0; b < numBands; b++) {
1095                int maxInSample = (1 << sampleSize[b]) - 1;
1096                int halfMaxInSample = maxInSample/2;
1097                scaleh[b] = new byte[maxInSample + 1];
1098                scalel[b] = new byte[maxInSample + 1];
1099                for (int s = 0; s <= maxInSample; s++) {
1100                    int val = (s*maxOutSample + halfMaxInSample)/maxInSample;
1101                    scaleh[b][s] = (byte)(val >> 8);
1102                    scalel[b][s] = (byte)(val & 0xff);
1103                }
1104            }
1105            scale = null;
1106            scale0 = null;
1107        }
1108    }
1109
1110    @Override
1111    public void write(IIOMetadata streamMetadata,
1112                      IIOImage image,
1113                      ImageWriteParam param) throws IIOException {
1114        if (stream == null) {
1115            throw new IllegalStateException("output == null!");
1116        }
1117        if (image == null) {
1118            throw new IllegalArgumentException("image == null!");
1119        }
1120        if (image.hasRaster()) {
1121            throw new UnsupportedOperationException("image has a Raster!");
1122        }
1123
1124        RenderedImage im = image.getRenderedImage();
1125        SampleModel sampleModel = im.getSampleModel();
1126        this.numBands = sampleModel.getNumBands();
1127
1128        // Set source region and subsampling to default values
1129        this.sourceXOffset = im.getMinX();
1130        this.sourceYOffset = im.getMinY();
1131        this.sourceWidth = im.getWidth();
1132        this.sourceHeight = im.getHeight();
1133        this.sourceBands = null;
1134        this.periodX = 1;
1135        this.periodY = 1;
1136
1137        if (param != null) {
1138            // Get source region and subsampling factors
1139            Rectangle sourceRegion = param.getSourceRegion();
1140            if (sourceRegion != null) {
1141                Rectangle imageBounds = new Rectangle(im.getMinX(),
1142                                                      im.getMinY(),
1143                                                      im.getWidth(),
1144                                                      im.getHeight());
1145                // Clip to actual image bounds
1146                sourceRegion = sourceRegion.intersection(imageBounds);
1147                sourceXOffset = sourceRegion.x;
1148                sourceYOffset = sourceRegion.y;
1149                sourceWidth = sourceRegion.width;
1150                sourceHeight = sourceRegion.height;
1151            }
1152
1153            // Adjust for subsampling offsets
1154            int gridX = param.getSubsamplingXOffset();
1155            int gridY = param.getSubsamplingYOffset();
1156            sourceXOffset += gridX;
1157            sourceYOffset += gridY;
1158            sourceWidth -= gridX;
1159            sourceHeight -= gridY;
1160
1161            // Get subsampling factors
1162            periodX = param.getSourceXSubsampling();
1163            periodY = param.getSourceYSubsampling();
1164
1165            int[] sBands = param.getSourceBands();
1166            if (sBands != null) {
1167                sourceBands = sBands;
1168                numBands = sourceBands.length;
1169            }
1170        }
1171
1172        // Compute output dimensions
1173        int destWidth = (sourceWidth + periodX - 1)/periodX;
1174        int destHeight = (sourceHeight + periodY - 1)/periodY;
1175        if (destWidth <= 0 || destHeight <= 0) {
1176            throw new IllegalArgumentException("Empty source region!");
1177        }
1178
1179        // Compute total number of pixels for progress notification
1180        this.totalPixels = destWidth*destHeight;
1181        this.pixelsDone = 0;
1182
1183        // Create metadata
1184        IIOMetadata imd = image.getMetadata();
1185        if (imd != null) {
1186            metadata = (PNGMetadata)convertImageMetadata(imd,
1187                               ImageTypeSpecifier.createFromRenderedImage(im),
1188                                                         null);
1189        } else {
1190            metadata = new PNGMetadata();
1191        }
1192
1193        // reset compression level to default:
1194        int deflaterLevel = DEFAULT_COMPRESSION_LEVEL;
1195
1196        if (param != null) {
1197            switch(param.getCompressionMode()) {
1198            case ImageWriteParam.MODE_DISABLED:
1199                deflaterLevel = Deflater.NO_COMPRESSION;
1200                break;
1201            case ImageWriteParam.MODE_EXPLICIT:
1202                float quality = param.getCompressionQuality();
1203                if (quality >= 0f && quality <= 1f) {
1204                    deflaterLevel = 9 - Math.round(9f * quality);
1205                }
1206                break;
1207            default:
1208            }
1209
1210            // Use Adam7 interlacing if set in write param
1211            switch (param.getProgressiveMode()) {
1212            case ImageWriteParam.MODE_DEFAULT:
1213                metadata.IHDR_interlaceMethod = 1;
1214                break;
1215            case ImageWriteParam.MODE_DISABLED:
1216                metadata.IHDR_interlaceMethod = 0;
1217                break;
1218                // MODE_COPY_FROM_METADATA should already be taken care of
1219                // MODE_EXPLICIT is not allowed
1220            default:
1221            }
1222        }
1223
1224        // Initialize bitDepth and colorType
1225        metadata.initialize(new ImageTypeSpecifier(im), numBands);
1226
1227        // Overwrite IHDR width and height values with values from image
1228        metadata.IHDR_width = destWidth;
1229        metadata.IHDR_height = destHeight;
1230
1231        this.bpp = numBands*((metadata.IHDR_bitDepth == 16) ? 2 : 1);
1232
1233        // Initialize scaling tables for this image
1234        initializeScaleTables(sampleModel.getSampleSize());
1235
1236        clearAbortRequest();
1237
1238        processImageStarted(0);
1239        if (abortRequested()) {
1240            processWriteAborted();
1241        } else {
1242            try {
1243                write_magic();
1244                write_IHDR();
1245
1246                write_cHRM();
1247                write_gAMA();
1248                write_iCCP();
1249                write_sBIT();
1250                write_sRGB();
1251
1252                write_PLTE();
1253
1254                write_hIST();
1255                write_tRNS();
1256                write_bKGD();
1257
1258                write_pHYs();
1259                write_sPLT();
1260                write_tIME();
1261                write_tEXt();
1262                write_iTXt();
1263                write_zTXt();
1264
1265                writeUnknownChunks();
1266
1267                write_IDAT(im, deflaterLevel);
1268
1269                if (abortRequested()) {
1270                    processWriteAborted();
1271                } else {
1272                    // Finish up and inform the listeners we are done
1273                    writeIEND();
1274                    processImageComplete();
1275                }
1276            } catch (IOException e) {
1277                throw new IIOException("I/O error writing PNG file!", e);
1278            }
1279        }
1280    }
1281}
1282