1 /*
2  * Copyright (c) 2000, 2020, 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 
26 package com.sun.imageio.plugins.png;
27 
28 import java.awt.Rectangle;
29 import java.awt.image.IndexColorModel;
30 import java.awt.image.Raster;
31 import java.awt.image.WritableRaster;
32 import java.awt.image.RenderedImage;
33 import java.awt.image.SampleModel;
34 import java.io.ByteArrayOutputStream;
35 import java.io.IOException;
36 import java.util.Iterator;
37 import java.util.Locale;
38 import java.util.zip.Deflater;
39 import java.util.zip.DeflaterOutputStream;
40 import javax.imageio.IIOException;
41 import javax.imageio.IIOImage;
42 import javax.imageio.ImageTypeSpecifier;
43 import javax.imageio.ImageWriteParam;
44 import javax.imageio.ImageWriter;
45 import javax.imageio.metadata.IIOMetadata;
46 import javax.imageio.spi.ImageWriterSpi;
47 import javax.imageio.stream.ImageOutputStream;
48 import javax.imageio.stream.ImageOutputStreamImpl;
49 
50 final 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 
CRC()71     CRC() {}
72 
reset()73     void reset() {
74         crc = 0xffffffff;
75     }
76 
update(byte[] data, int off, int len)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 
update(int data)85     void update(int data) {
86         crc = crcTable[(crc ^ data) & 0xff] ^ (crc >>> 8);
87     }
88 
getValue()89     int getValue() {
90         return crc ^ 0xffffffff;
91     }
92 }
93 
94 
95 final class ChunkStream extends ImageOutputStreamImpl {
96 
97     private final ImageOutputStream stream;
98     private final long startPos;
99     private final CRC crc = new CRC();
100 
ChunkStream(int type, ImageOutputStream stream)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
read()110     public int read() throws IOException {
111         throw new RuntimeException("Method not available");
112     }
113 
114     @Override
read(byte[] b, int off, int len)115     public int read(byte[] b, int off, int len) throws IOException {
116         throw new RuntimeException("Method not available");
117     }
118 
119     @Override
write(byte[] b, int off, int len)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
write(int b)126     public void write(int b) throws IOException {
127         crc.update(b);
128         stream.write(b);
129     }
130 
finish()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")
finalize()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.
155 final 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 
IDATOutputStream(ImageOutputStream stream, int chunkLength, int deflaterLevel)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 
startChunk()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 
finishChunk()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
read()219     public int read() throws IOException {
220         throw new RuntimeException("Method not available");
221     }
222 
223     @Override
read(byte[] b, int off, int len)224     public int read(byte[] b, int off, int len) throws IOException {
225         throw new RuntimeException("Method not available");
226     }
227 
228     @Override
write(byte[] b, int off, int len)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 
deflate()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
write(int b)263     public void write(int b) throws IOException {
264         wbuf1[0] = (byte)b;
265         write(wbuf1, 0, 1);
266     }
267 
finish()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")
finalize()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 
291 final 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 
PNGImageWriteParam(Locale locale)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
unsetCompression()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
isCompressionLossless()338     public boolean isCompressionLossless() {
339         return true;
340     }
341 
342     @Override
getCompressionQualityDescriptions()343     public String[] getCompressionQualityDescriptions() {
344         super.getCompressionQualityDescriptions();
345         return qualityDescs.clone();
346     }
347 
348     @Override
getCompressionQualityValues()349     public float[] getCompressionQualityValues() {
350         super.getCompressionQualityValues();
351         return qualityVals.clone();
352     }
353 }
354 
355 /**
356  */
357 public 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 
PNGImageWriter(ImageWriterSpi originatingProvider)405     public PNGImageWriter(ImageWriterSpi originatingProvider) {
406         super(originatingProvider);
407     }
408 
409     @Override
setOutput(Object output)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
getDefaultWriteParam()423     public ImageWriteParam getDefaultWriteParam() {
424         return new PNGImageWriteParam(getLocale());
425     }
426 
427     @Override
getDefaultStreamMetadata(ImageWriteParam param)428     public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
429         return null;
430     }
431 
432     @Override
getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param)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
convertStreamMetadata(IIOMetadata inData, ImageWriteParam param)441     public IIOMetadata convertStreamMetadata(IIOMetadata inData,
442                                              ImageWriteParam param) {
443         return null;
444     }
445 
446     @Override
convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param)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 
write_magic()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 
write_IHDR()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 
write_cHRM()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 
write_gAMA()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 
write_iCCP()513     private void write_iCCP() throws IOException {
514         if (metadata.iCCP_present) {
515             ChunkStream cs = new ChunkStream(PNGImageReader.iCCP_TYPE, stream);
516             if (metadata.iCCP_profileName.length() > 79) {
517                 throw new IIOException("iCCP profile name is longer than 79");
518             }
519             cs.writeBytes(metadata.iCCP_profileName);
520             cs.writeByte(0); // null terminator
521 
522             cs.writeByte(metadata.iCCP_compressionMethod);
523             cs.write(metadata.iCCP_compressedProfile);
524             cs.finish();
525         }
526     }
527 
write_sBIT()528     private void write_sBIT() throws IOException {
529         if (metadata.sBIT_present) {
530             ChunkStream cs = new ChunkStream(PNGImageReader.sBIT_TYPE, stream);
531             int colorType = metadata.IHDR_colorType;
532             if (metadata.sBIT_colorType != colorType) {
533                 processWarningOccurred(0,
534 "sBIT metadata has wrong color type.\n" +
535 "The chunk will not be written.");
536                 return;
537             }
538 
539             if (colorType == PNGImageReader.PNG_COLOR_GRAY ||
540                 colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
541                 cs.writeByte(metadata.sBIT_grayBits);
542             } else if (colorType == PNGImageReader.PNG_COLOR_RGB ||
543                        colorType == PNGImageReader.PNG_COLOR_PALETTE ||
544                        colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) {
545                 cs.writeByte(metadata.sBIT_redBits);
546                 cs.writeByte(metadata.sBIT_greenBits);
547                 cs.writeByte(metadata.sBIT_blueBits);
548             }
549 
550             if (colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA ||
551                 colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) {
552                 cs.writeByte(metadata.sBIT_alphaBits);
553             }
554             cs.finish();
555         }
556     }
557 
write_sRGB()558     private void write_sRGB() throws IOException {
559         if (metadata.sRGB_present) {
560             ChunkStream cs = new ChunkStream(PNGImageReader.sRGB_TYPE, stream);
561             cs.writeByte(metadata.sRGB_renderingIntent);
562             cs.finish();
563         }
564     }
565 
write_PLTE()566     private void write_PLTE() throws IOException {
567         if (metadata.PLTE_present) {
568             if (metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY ||
569               metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
570                 // PLTE cannot occur in a gray image
571 
572                 processWarningOccurred(0,
573 "A PLTE chunk may not appear in a gray or gray alpha image.\n" +
574 "The chunk will not be written");
575                 return;
576             }
577 
578             ChunkStream cs = new ChunkStream(PNGImageReader.PLTE_TYPE, stream);
579 
580             int numEntries = metadata.PLTE_red.length;
581             byte[] palette = new byte[numEntries*3];
582             int index = 0;
583             for (int i = 0; i < numEntries; i++) {
584                 palette[index++] = metadata.PLTE_red[i];
585                 palette[index++] = metadata.PLTE_green[i];
586                 palette[index++] = metadata.PLTE_blue[i];
587             }
588 
589             cs.write(palette);
590             cs.finish();
591         }
592     }
593 
write_hIST()594     private void write_hIST() throws IOException, IIOException {
595         if (metadata.hIST_present) {
596             ChunkStream cs = new ChunkStream(PNGImageReader.hIST_TYPE, stream);
597 
598             if (!metadata.PLTE_present) {
599                 throw new IIOException("hIST chunk without PLTE chunk!");
600             }
601 
602             cs.writeChars(metadata.hIST_histogram,
603                           0, metadata.hIST_histogram.length);
604             cs.finish();
605         }
606     }
607 
write_tRNS()608     private void write_tRNS() throws IOException, IIOException {
609         if (metadata.tRNS_present) {
610             ChunkStream cs = new ChunkStream(PNGImageReader.tRNS_TYPE, stream);
611             int colorType = metadata.IHDR_colorType;
612             int chunkType = metadata.tRNS_colorType;
613 
614             // Special case: image is RGB and chunk is Gray
615             // Promote chunk contents to RGB
616             int chunkRed = metadata.tRNS_red;
617             int chunkGreen = metadata.tRNS_green;
618             int chunkBlue = metadata.tRNS_blue;
619             if (colorType == PNGImageReader.PNG_COLOR_RGB &&
620                 chunkType == PNGImageReader.PNG_COLOR_GRAY) {
621                 chunkType = colorType;
622                 chunkRed = chunkGreen = chunkBlue =
623                     metadata.tRNS_gray;
624             }
625 
626             if (chunkType != colorType) {
627                 processWarningOccurred(0,
628 "tRNS metadata has incompatible color type.\n" +
629 "The chunk will not be written.");
630                 return;
631             }
632 
633             if (colorType == PNGImageReader.PNG_COLOR_PALETTE) {
634                 if (!metadata.PLTE_present) {
635                     throw new IIOException("tRNS chunk without PLTE chunk!");
636                 }
637                 cs.write(metadata.tRNS_alpha);
638             } else if (colorType == PNGImageReader.PNG_COLOR_GRAY) {
639                 cs.writeShort(metadata.tRNS_gray);
640             } else if (colorType == PNGImageReader.PNG_COLOR_RGB) {
641                 cs.writeShort(chunkRed);
642                 cs.writeShort(chunkGreen);
643                 cs.writeShort(chunkBlue);
644             } else {
645                 throw new IIOException("tRNS chunk for color type 4 or 6!");
646             }
647             cs.finish();
648         }
649     }
650 
write_bKGD()651     private void write_bKGD() throws IOException {
652         if (metadata.bKGD_present) {
653             ChunkStream cs = new ChunkStream(PNGImageReader.bKGD_TYPE, stream);
654             int colorType = metadata.IHDR_colorType & 0x3;
655             int chunkType = metadata.bKGD_colorType;
656 
657             int chunkRed = metadata.bKGD_red;
658             int chunkGreen = metadata.bKGD_green;
659             int chunkBlue = metadata.bKGD_blue;
660             // Special case: image is RGB(A) and chunk is Gray
661             // Promote chunk contents to RGB
662             if (colorType == PNGImageReader.PNG_COLOR_RGB &&
663                 chunkType == PNGImageReader.PNG_COLOR_GRAY) {
664                 // Make a gray bKGD chunk look like RGB
665                 chunkType = colorType;
666                 chunkRed = chunkGreen = chunkBlue =
667                     metadata.bKGD_gray;
668             }
669 
670             // Ignore status of alpha in colorType
671             if (chunkType != colorType) {
672                 processWarningOccurred(0,
673 "bKGD metadata has incompatible color type.\n" +
674 "The chunk will not be written.");
675                 return;
676             }
677 
678             if (colorType == PNGImageReader.PNG_COLOR_PALETTE) {
679                 cs.writeByte(metadata.bKGD_index);
680             } else if (colorType == PNGImageReader.PNG_COLOR_GRAY ||
681                        colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
682                 cs.writeShort(metadata.bKGD_gray);
683             } else { // colorType == PNGImageReader.PNG_COLOR_RGB ||
684                      // colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA
685                 cs.writeShort(chunkRed);
686                 cs.writeShort(chunkGreen);
687                 cs.writeShort(chunkBlue);
688             }
689             cs.finish();
690         }
691     }
692 
write_pHYs()693     private void write_pHYs() throws IOException {
694         if (metadata.pHYs_present) {
695             ChunkStream cs = new ChunkStream(PNGImageReader.pHYs_TYPE, stream);
696             cs.writeInt(metadata.pHYs_pixelsPerUnitXAxis);
697             cs.writeInt(metadata.pHYs_pixelsPerUnitYAxis);
698             cs.writeByte(metadata.pHYs_unitSpecifier);
699             cs.finish();
700         }
701     }
702 
write_sPLT()703     private void write_sPLT() throws IOException {
704         if (metadata.sPLT_present) {
705             ChunkStream cs = new ChunkStream(PNGImageReader.sPLT_TYPE, stream);
706 
707             if (metadata.sPLT_paletteName.length() > 79) {
708                 throw new IIOException("sPLT palette name is longer than 79");
709             }
710             cs.writeBytes(metadata.sPLT_paletteName);
711             cs.writeByte(0); // null terminator
712 
713             cs.writeByte(metadata.sPLT_sampleDepth);
714             int numEntries = metadata.sPLT_red.length;
715 
716             if (metadata.sPLT_sampleDepth == 8) {
717                 for (int i = 0; i < numEntries; i++) {
718                     cs.writeByte(metadata.sPLT_red[i]);
719                     cs.writeByte(metadata.sPLT_green[i]);
720                     cs.writeByte(metadata.sPLT_blue[i]);
721                     cs.writeByte(metadata.sPLT_alpha[i]);
722                     cs.writeShort(metadata.sPLT_frequency[i]);
723                 }
724             } else { // sampleDepth == 16
725                 for (int i = 0; i < numEntries; i++) {
726                     cs.writeShort(metadata.sPLT_red[i]);
727                     cs.writeShort(metadata.sPLT_green[i]);
728                     cs.writeShort(metadata.sPLT_blue[i]);
729                     cs.writeShort(metadata.sPLT_alpha[i]);
730                     cs.writeShort(metadata.sPLT_frequency[i]);
731                 }
732             }
733             cs.finish();
734         }
735     }
736 
write_tIME()737     private void write_tIME() throws IOException {
738         if (metadata.tIME_present) {
739             ChunkStream cs = new ChunkStream(PNGImageReader.tIME_TYPE, stream);
740             cs.writeShort(metadata.tIME_year);
741             cs.writeByte(metadata.tIME_month);
742             cs.writeByte(metadata.tIME_day);
743             cs.writeByte(metadata.tIME_hour);
744             cs.writeByte(metadata.tIME_minute);
745             cs.writeByte(metadata.tIME_second);
746             cs.finish();
747         }
748     }
749 
write_tEXt()750     private void write_tEXt() throws IOException {
751         Iterator<String> keywordIter = metadata.tEXt_keyword.iterator();
752         Iterator<String> textIter = metadata.tEXt_text.iterator();
753 
754         while (keywordIter.hasNext()) {
755             ChunkStream cs = new ChunkStream(PNGImageReader.tEXt_TYPE, stream);
756             String keyword = keywordIter.next();
757             if (keyword.length() > 79) {
758                 throw new IIOException("tEXt keyword is longer than 79");
759             }
760             cs.writeBytes(keyword);
761             cs.writeByte(0);
762 
763             String text = textIter.next();
764             cs.writeBytes(text);
765             cs.finish();
766         }
767     }
768 
deflate(byte[] b)769     private byte[] deflate(byte[] b) throws IOException {
770         ByteArrayOutputStream baos = new ByteArrayOutputStream();
771         DeflaterOutputStream dos = new DeflaterOutputStream(baos);
772         dos.write(b);
773         dos.close();
774         return baos.toByteArray();
775     }
776 
write_iTXt()777     private void write_iTXt() throws IOException {
778         Iterator<String> keywordIter = metadata.iTXt_keyword.iterator();
779         Iterator<Boolean> flagIter = metadata.iTXt_compressionFlag.iterator();
780         Iterator<Integer> methodIter = metadata.iTXt_compressionMethod.iterator();
781         Iterator<String> languageIter = metadata.iTXt_languageTag.iterator();
782         Iterator<String> translatedKeywordIter =
783             metadata.iTXt_translatedKeyword.iterator();
784         Iterator<String> textIter = metadata.iTXt_text.iterator();
785 
786         while (keywordIter.hasNext()) {
787             ChunkStream cs = new ChunkStream(PNGImageReader.iTXt_TYPE, stream);
788 
789             String keyword = keywordIter.next();
790             if (keyword.length() > 79) {
791                 throw new IIOException("iTXt keyword is longer than 79");
792             }
793             cs.writeBytes(keyword);
794             cs.writeByte(0);
795 
796             Boolean compressed = flagIter.next();
797             cs.writeByte(compressed ? 1 : 0);
798 
799             cs.writeByte(methodIter.next().intValue());
800 
801             cs.writeBytes(languageIter.next());
802             cs.writeByte(0);
803 
804 
805             cs.write(translatedKeywordIter.next().getBytes("UTF8"));
806             cs.writeByte(0);
807 
808             String text = textIter.next();
809             if (compressed) {
810                 cs.write(deflate(text.getBytes("UTF8")));
811             } else {
812                 cs.write(text.getBytes("UTF8"));
813             }
814             cs.finish();
815         }
816     }
817 
write_zTXt()818     private void write_zTXt() throws IOException {
819         Iterator<String> keywordIter = metadata.zTXt_keyword.iterator();
820         Iterator<Integer> methodIter = metadata.zTXt_compressionMethod.iterator();
821         Iterator<String> textIter = metadata.zTXt_text.iterator();
822 
823         while (keywordIter.hasNext()) {
824             ChunkStream cs = new ChunkStream(PNGImageReader.zTXt_TYPE, stream);
825             String keyword = keywordIter.next();
826             if (keyword.length() > 79) {
827                 throw new IIOException("zTXt keyword is longer than 79");
828             }
829             cs.writeBytes(keyword);
830             cs.writeByte(0);
831 
832             int compressionMethod = (methodIter.next()).intValue();
833             cs.writeByte(compressionMethod);
834 
835             String text = textIter.next();
836             cs.write(deflate(text.getBytes("ISO-8859-1")));
837             cs.finish();
838         }
839     }
840 
writeUnknownChunks()841     private void writeUnknownChunks() throws IOException {
842         Iterator<String> typeIter = metadata.unknownChunkType.iterator();
843         Iterator<byte[]> dataIter = metadata.unknownChunkData.iterator();
844 
845         while (typeIter.hasNext() && dataIter.hasNext()) {
846             String type = typeIter.next();
847             ChunkStream cs = new ChunkStream(chunkType(type), stream);
848             byte[] data = dataIter.next();
849             cs.write(data);
850             cs.finish();
851         }
852     }
853 
chunkType(String typeString)854     private static int chunkType(String typeString) {
855         char c0 = typeString.charAt(0);
856         char c1 = typeString.charAt(1);
857         char c2 = typeString.charAt(2);
858         char c3 = typeString.charAt(3);
859 
860         int type = (c0 << 24) | (c1 << 16) | (c2 << 8) | c3;
861         return type;
862     }
863 
encodePass(ImageOutputStream os, RenderedImage image, int xOffset, int yOffset, int xSkip, int ySkip)864     private void encodePass(ImageOutputStream os,
865                             RenderedImage image,
866                             int xOffset, int yOffset,
867                             int xSkip, int ySkip) throws IOException {
868         int minX = sourceXOffset;
869         int minY = sourceYOffset;
870         int width = sourceWidth;
871         int height = sourceHeight;
872 
873         // Adjust offsets and skips based on source subsampling factors
874         xOffset *= periodX;
875         xSkip *= periodX;
876         yOffset *= periodY;
877         ySkip *= periodY;
878 
879         // Early exit if no data for this pass
880         int hpixels = (width - xOffset + xSkip - 1)/xSkip;
881         int vpixels = (height - yOffset + ySkip - 1)/ySkip;
882         if (hpixels == 0 || vpixels == 0) {
883             return;
884         }
885 
886         // Convert X offset and skip from pixels to samples
887         xOffset *= numBands;
888         xSkip *= numBands;
889 
890         // Create row buffers
891         int samplesPerByte = 8/metadata.IHDR_bitDepth;
892         int numSamples = width*numBands;
893         int[] samples = new int[numSamples];
894 
895         int bytesPerRow = hpixels*numBands;
896         if (metadata.IHDR_bitDepth < 8) {
897             bytesPerRow = (bytesPerRow + samplesPerByte - 1)/samplesPerByte;
898         } else if (metadata.IHDR_bitDepth == 16) {
899             bytesPerRow *= 2;
900         }
901 
902         IndexColorModel icm_gray_alpha = null;
903         if (metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA &&
904             image.getColorModel() instanceof IndexColorModel)
905         {
906             // reserve space for alpha samples
907             bytesPerRow *= 2;
908 
909             // will be used to calculate alpha value for the pixel
910             icm_gray_alpha = (IndexColorModel)image.getColorModel();
911         }
912 
913         currRow = new byte[bytesPerRow + bpp];
914         prevRow = new byte[bytesPerRow + bpp];
915         filteredRows = new byte[5][bytesPerRow + bpp];
916 
917         int bitDepth = metadata.IHDR_bitDepth;
918         for (int row = minY + yOffset; row < minY + height; row += ySkip) {
919             Rectangle rect = new Rectangle(minX, row, width, 1);
920             Raster ras = image.getData(rect);
921             if (sourceBands != null) {
922                 ras = ras.createChild(minX, row, width, 1, minX, row,
923                                       sourceBands);
924             }
925 
926             ras.getPixels(minX, row, width, 1, samples);
927 
928             if (image.getColorModel().isAlphaPremultiplied()) {
929                 WritableRaster wr = ras.createCompatibleWritableRaster();
930                 wr.setPixels(wr.getMinX(), wr.getMinY(),
931                              wr.getWidth(), wr.getHeight(),
932                              samples);
933 
934                 image.getColorModel().coerceData(wr, false);
935                 wr.getPixels(wr.getMinX(), wr.getMinY(),
936                              wr.getWidth(), wr.getHeight(),
937                              samples);
938             }
939 
940             // Reorder palette data if necessary
941             int[] paletteOrder = metadata.PLTE_order;
942             if (paletteOrder != null) {
943                 for (int i = 0; i < numSamples; i++) {
944                     samples[i] = paletteOrder[samples[i]];
945                 }
946             }
947 
948             int count = bpp; // leave first 'bpp' bytes zero
949             int pos = 0;
950             int tmp = 0;
951 
952             switch (bitDepth) {
953             case 1: case 2: case 4:
954                 // Image can only have a single band
955 
956                 int mask = samplesPerByte - 1;
957                 for (int s = xOffset; s < numSamples; s += xSkip) {
958                     byte val = scale0[samples[s]];
959                     tmp = (tmp << bitDepth) | val;
960 
961                     if ((pos++ & mask) == mask) {
962                         currRow[count++] = (byte)tmp;
963                         tmp = 0;
964                         pos = 0;
965                     }
966                 }
967 
968                 // Left shift the last byte
969                 if ((pos & mask) != 0) {
970                     tmp <<= ((8/bitDepth) - pos)*bitDepth;
971                     currRow[count++] = (byte)tmp;
972                 }
973                 break;
974 
975             case 8:
976                 if (numBands == 1) {
977                     for (int s = xOffset; s < numSamples; s += xSkip) {
978                         currRow[count++] = scale0[samples[s]];
979                         if (icm_gray_alpha != null) {
980                             currRow[count++] =
981                                 scale0[icm_gray_alpha.getAlpha(0xff & samples[s])];
982                         }
983                     }
984                 } else {
985                     for (int s = xOffset; s < numSamples; s += xSkip) {
986                         for (int b = 0; b < numBands; b++) {
987                             currRow[count++] = scale[b][samples[s + b]];
988                         }
989                     }
990                 }
991                 break;
992 
993             case 16:
994                 for (int s = xOffset; s < numSamples; s += xSkip) {
995                     for (int b = 0; b < numBands; b++) {
996                         currRow[count++] = scaleh[b][samples[s + b]];
997                         currRow[count++] = scalel[b][samples[s + b]];
998                     }
999                 }
1000                 break;
1001             }
1002 
1003             // Perform filtering
1004             int filterType = rowFilter.filterRow(metadata.IHDR_colorType,
1005                                                  currRow, prevRow,
1006                                                  filteredRows,
1007                                                  bytesPerRow, bpp);
1008 
1009             os.write(filterType);
1010             os.write(filteredRows[filterType], bpp, bytesPerRow);
1011 
1012             // Swap current and previous rows
1013             byte[] swap = currRow;
1014             currRow = prevRow;
1015             prevRow = swap;
1016 
1017             pixelsDone += hpixels;
1018             processImageProgress(100.0F*pixelsDone/totalPixels);
1019 
1020             // If write has been aborted, just return;
1021             // processWriteAborted will be called later
1022             if (abortRequested()) {
1023                 return;
1024             }
1025         }
1026     }
1027 
1028     // Use sourceXOffset, etc.
write_IDAT(RenderedImage image, int deflaterLevel)1029     private void write_IDAT(RenderedImage image, int deflaterLevel)
1030         throws IOException
1031     {
1032         IDATOutputStream ios = new IDATOutputStream(stream, 32768,
1033                                                     deflaterLevel);
1034         try {
1035             if (metadata.IHDR_interlaceMethod == 1) {
1036                 for (int i = 0; i < 7; i++) {
1037                     encodePass(ios, image,
1038                                PNGImageReader.adam7XOffset[i],
1039                                PNGImageReader.adam7YOffset[i],
1040                                PNGImageReader.adam7XSubsampling[i],
1041                                PNGImageReader.adam7YSubsampling[i]);
1042                     if (abortRequested()) {
1043                         break;
1044                     }
1045                 }
1046             } else {
1047                 encodePass(ios, image, 0, 0, 1, 1);
1048             }
1049         } finally {
1050             ios.finish();
1051         }
1052     }
1053 
writeIEND()1054     private void writeIEND() throws IOException {
1055         ChunkStream cs = new ChunkStream(PNGImageReader.IEND_TYPE, stream);
1056         cs.finish();
1057     }
1058 
1059     // Check two int arrays for value equality, always returns false
1060     // if either array is null
equals(int[] s0, int[] s1)1061     private boolean equals(int[] s0, int[] s1) {
1062         if (s0 == null || s1 == null) {
1063             return false;
1064         }
1065         if (s0.length != s1.length) {
1066             return false;
1067         }
1068         for (int i = 0; i < s0.length; i++) {
1069             if (s0[i] != s1[i]) {
1070                 return false;
1071             }
1072         }
1073         return true;
1074     }
1075 
1076     // Initialize the scale/scale0 or scaleh/scalel arrays to
1077     // hold the results of scaling an input value to the desired
1078     // output bit depth
initializeScaleTables(int[] sampleSize)1079     private void initializeScaleTables(int[] sampleSize) {
1080         int bitDepth = metadata.IHDR_bitDepth;
1081 
1082         // If the existing tables are still valid, just return
1083         if (bitDepth == scalingBitDepth &&
1084             equals(sampleSize, this.sampleSize)) {
1085             return;
1086         }
1087 
1088         // Compute new tables
1089         this.sampleSize = sampleSize;
1090         this.scalingBitDepth = bitDepth;
1091         int maxOutSample = (1 << bitDepth) - 1;
1092         if (bitDepth <= 8) {
1093             scale = new byte[numBands][];
1094             for (int b = 0; b < numBands; b++) {
1095                 int maxInSample = (1 << sampleSize[b]) - 1;
1096                 int halfMaxInSample = maxInSample/2;
1097                 scale[b] = new byte[maxInSample + 1];
1098                 for (int s = 0; s <= maxInSample; s++) {
1099                     scale[b][s] =
1100                         (byte)((s*maxOutSample + halfMaxInSample)/maxInSample);
1101                 }
1102             }
1103             scale0 = scale[0];
1104             scaleh = scalel = null;
1105         } else { // bitDepth == 16
1106             // Divide scaling table into high and low bytes
1107             scaleh = new byte[numBands][];
1108             scalel = new byte[numBands][];
1109 
1110             for (int b = 0; b < numBands; b++) {
1111                 int maxInSample = (1 << sampleSize[b]) - 1;
1112                 int halfMaxInSample = maxInSample/2;
1113                 scaleh[b] = new byte[maxInSample + 1];
1114                 scalel[b] = new byte[maxInSample + 1];
1115                 for (int s = 0; s <= maxInSample; s++) {
1116                     int val = (s*maxOutSample + halfMaxInSample)/maxInSample;
1117                     scaleh[b][s] = (byte)(val >> 8);
1118                     scalel[b][s] = (byte)(val & 0xff);
1119                 }
1120             }
1121             scale = null;
1122             scale0 = null;
1123         }
1124     }
1125 
1126     @Override
write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param)1127     public void write(IIOMetadata streamMetadata,
1128                       IIOImage image,
1129                       ImageWriteParam param) throws IIOException {
1130         if (stream == null) {
1131             throw new IllegalStateException("output == null!");
1132         }
1133         if (image == null) {
1134             throw new IllegalArgumentException("image == null!");
1135         }
1136         if (image.hasRaster()) {
1137             throw new UnsupportedOperationException("image has a Raster!");
1138         }
1139 
1140         RenderedImage im = image.getRenderedImage();
1141         SampleModel sampleModel = im.getSampleModel();
1142         this.numBands = sampleModel.getNumBands();
1143 
1144         // Set source region and subsampling to default values
1145         this.sourceXOffset = im.getMinX();
1146         this.sourceYOffset = im.getMinY();
1147         this.sourceWidth = im.getWidth();
1148         this.sourceHeight = im.getHeight();
1149         this.sourceBands = null;
1150         this.periodX = 1;
1151         this.periodY = 1;
1152 
1153         if (param != null) {
1154             // Get source region and subsampling factors
1155             Rectangle sourceRegion = param.getSourceRegion();
1156             if (sourceRegion != null) {
1157                 Rectangle imageBounds = new Rectangle(im.getMinX(),
1158                                                       im.getMinY(),
1159                                                       im.getWidth(),
1160                                                       im.getHeight());
1161                 // Clip to actual image bounds
1162                 sourceRegion = sourceRegion.intersection(imageBounds);
1163                 sourceXOffset = sourceRegion.x;
1164                 sourceYOffset = sourceRegion.y;
1165                 sourceWidth = sourceRegion.width;
1166                 sourceHeight = sourceRegion.height;
1167             }
1168 
1169             // Adjust for subsampling offsets
1170             int gridX = param.getSubsamplingXOffset();
1171             int gridY = param.getSubsamplingYOffset();
1172             sourceXOffset += gridX;
1173             sourceYOffset += gridY;
1174             sourceWidth -= gridX;
1175             sourceHeight -= gridY;
1176 
1177             // Get subsampling factors
1178             periodX = param.getSourceXSubsampling();
1179             periodY = param.getSourceYSubsampling();
1180 
1181             int[] sBands = param.getSourceBands();
1182             if (sBands != null) {
1183                 sourceBands = sBands;
1184                 numBands = sourceBands.length;
1185             }
1186         }
1187 
1188         // Compute output dimensions
1189         int destWidth = (sourceWidth + periodX - 1)/periodX;
1190         int destHeight = (sourceHeight + periodY - 1)/periodY;
1191         if (destWidth <= 0 || destHeight <= 0) {
1192             throw new IllegalArgumentException("Empty source region!");
1193         }
1194 
1195         // Compute total number of pixels for progress notification
1196         this.totalPixels = destWidth*destHeight;
1197         this.pixelsDone = 0;
1198 
1199         // Create metadata
1200         IIOMetadata imd = image.getMetadata();
1201         if (imd != null) {
1202             metadata = (PNGMetadata)convertImageMetadata(imd,
1203                                ImageTypeSpecifier.createFromRenderedImage(im),
1204                                                          null);
1205         } else {
1206             metadata = new PNGMetadata();
1207         }
1208 
1209         // reset compression level to default:
1210         int deflaterLevel = DEFAULT_COMPRESSION_LEVEL;
1211 
1212         if (param != null) {
1213             switch(param.getCompressionMode()) {
1214             case ImageWriteParam.MODE_DISABLED:
1215                 deflaterLevel = Deflater.NO_COMPRESSION;
1216                 break;
1217             case ImageWriteParam.MODE_EXPLICIT:
1218                 float quality = param.getCompressionQuality();
1219                 if (quality >= 0f && quality <= 1f) {
1220                     deflaterLevel = 9 - Math.round(9f * quality);
1221                 }
1222                 break;
1223             default:
1224             }
1225 
1226             // Use Adam7 interlacing if set in write param
1227             switch (param.getProgressiveMode()) {
1228             case ImageWriteParam.MODE_DEFAULT:
1229                 metadata.IHDR_interlaceMethod = 1;
1230                 break;
1231             case ImageWriteParam.MODE_DISABLED:
1232                 metadata.IHDR_interlaceMethod = 0;
1233                 break;
1234                 // MODE_COPY_FROM_METADATA should already be taken care of
1235                 // MODE_EXPLICIT is not allowed
1236             default:
1237             }
1238         }
1239 
1240         // Initialize bitDepth and colorType
1241         metadata.initialize(new ImageTypeSpecifier(im), numBands);
1242 
1243         // Overwrite IHDR width and height values with values from image
1244         metadata.IHDR_width = destWidth;
1245         metadata.IHDR_height = destHeight;
1246 
1247         this.bpp = numBands*((metadata.IHDR_bitDepth == 16) ? 2 : 1);
1248 
1249         // Initialize scaling tables for this image
1250         initializeScaleTables(sampleModel.getSampleSize());
1251 
1252         clearAbortRequest();
1253 
1254         processImageStarted(0);
1255         if (abortRequested()) {
1256             processWriteAborted();
1257         } else {
1258             try {
1259                 write_magic();
1260                 write_IHDR();
1261 
1262                 write_cHRM();
1263                 write_gAMA();
1264                 write_iCCP();
1265                 write_sBIT();
1266                 write_sRGB();
1267 
1268                 write_PLTE();
1269 
1270                 write_hIST();
1271                 write_tRNS();
1272                 write_bKGD();
1273 
1274                 write_pHYs();
1275                 write_sPLT();
1276                 write_tIME();
1277                 write_tEXt();
1278                 write_iTXt();
1279                 write_zTXt();
1280 
1281                 writeUnknownChunks();
1282 
1283                 write_IDAT(im, deflaterLevel);
1284 
1285                 if (abortRequested()) {
1286                     processWriteAborted();
1287                 } else {
1288                     // Finish up and inform the listeners we are done
1289                     writeIEND();
1290                     processImageComplete();
1291                 }
1292             } catch (IOException e) {
1293                 throw new IIOException("I/O error writing PNG file!", e);
1294             }
1295         }
1296     }
1297 }
1298