View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.png;
18  
19  import java.awt.image.BufferedImage;
20  import java.io.ByteArrayOutputStream;
21  import java.io.IOException;
22  import java.io.OutputStream;
23  import java.nio.charset.StandardCharsets;
24  import java.util.List;
25  import java.util.zip.Deflater;
26  import java.util.zip.DeflaterOutputStream;
27  
28  import org.apache.commons.imaging.ImagingException;
29  import org.apache.commons.imaging.PixelDensity;
30  import org.apache.commons.imaging.common.Allocator;
31  import org.apache.commons.imaging.internal.Debug;
32  import org.apache.commons.imaging.palette.Palette;
33  import org.apache.commons.imaging.palette.PaletteFactory;
34  
35  public class PngWriter {
36  
37      /*
38       * 1. IHDR: image header, which is the first chunk in a PNG data stream. 2. PLTE: palette table associated with indexed PNG images. 3. IDAT: image data
39       * chunks. 4. IEND: image trailer, which is the last chunk in a PNG data stream.
40       *
41       * The remaining 14 chunk types are termed ancillary chunk types, which encoders may generate and decoders may interpret.
42       *
43       * 1. Transparency information: tRNS (see 11.3.2: Transparency information). 2. Color space information: cHRM, gAMA, iCCP, sBIT, sRGB (see 11.3.3: Color
44       * space information). 3. Textual information: iTXt, tEXt, zTXt (see 11.3.4: Textual information). 4. Miscellaneous information: bKGD, hIST, pHYs, sPLT (see
45       * 11.3.5: Miscellaneous information). 5. Time information: tIME (see 11.3.6: Time stamp information).
46       */
47  
48      private static final class ImageHeader {
49          public final int width;
50          public final int height;
51          public final byte bitDepth;
52          public final PngColorType pngColorType;
53          public final byte compressionMethod;
54          public final byte filterMethod;
55          public final InterlaceMethod interlaceMethod;
56  
57          ImageHeader(final int width, final int height, final byte bitDepth, final PngColorType pngColorType, final byte compressionMethod,
58                  final byte filterMethod, final InterlaceMethod interlaceMethod) {
59              this.width = width;
60              this.height = height;
61              this.bitDepth = bitDepth;
62              this.pngColorType = pngColorType;
63              this.compressionMethod = compressionMethod;
64              this.filterMethod = filterMethod;
65              this.interlaceMethod = interlaceMethod;
66          }
67  
68      }
69  
70      private byte[] deflate(final byte[] bytes) throws IOException {
71          try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
72              try (DeflaterOutputStream dos = new DeflaterOutputStream(baos)) {
73                  dos.write(bytes);
74                  // dos.flush() doesn't work - we must close it before baos.toByteArray()
75              }
76              return baos.toByteArray();
77          }
78      }
79  
80      private byte getBitDepth(final PngColorType pngColorType, final PngImagingParameters params) {
81          final byte depth = params.getBitDepth();
82  
83          return pngColorType.isBitDepthAllowed(depth) ? depth : PngImagingParameters.DEFAULT_BIT_DEPTH;
84      }
85  
86      private boolean isValidISO_8859_1(final String s) {
87          final String roundtrip = new String(s.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.ISO_8859_1);
88          return s.equals(roundtrip);
89      }
90  
91      private void writeChunk(final OutputStream os, final ChunkType chunkType, final byte[] data) throws IOException {
92          final int dataLength = data == null ? 0 : data.length;
93          writeInt(os, dataLength);
94          os.write(chunkType.array);
95          if (data != null) {
96              os.write(data);
97          }
98  
99          final PngCrc pngCrc = new PngCrc();
100 
101         final long crc1 = pngCrc.startPartialCrc(chunkType.array, chunkType.array.length);
102         final long crc2 = data == null ? crc1 : pngCrc.continuePartialCrc(crc1, data, data.length);
103         final int crc = (int) pngCrc.finishPartialCrc(crc2);
104 
105         writeInt(os, crc);
106     }
107 
108     private void writeChunkIDAT(final OutputStream os, final byte[] bytes) throws IOException {
109         writeChunk(os, ChunkType.IDAT, bytes);
110     }
111 
112     private void writeChunkIEND(final OutputStream os) throws IOException {
113         writeChunk(os, ChunkType.IEND, null);
114     }
115 
116     private void writeChunkIHDR(final OutputStream os, final ImageHeader value) throws IOException {
117         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
118         writeInt(baos, value.width);
119         writeInt(baos, value.height);
120         baos.write(0xff & value.bitDepth);
121         baos.write(0xff & value.pngColorType.getValue());
122         baos.write(0xff & value.compressionMethod);
123         baos.write(0xff & value.filterMethod);
124         baos.write(0xff & value.interlaceMethod.ordinal());
125 
126         writeChunk(os, ChunkType.IHDR, baos.toByteArray());
127     }
128 
129     private void writeChunkiTXt(final OutputStream os, final AbstractPngText.Itxt text) throws IOException, ImagingException {
130         if (!isValidISO_8859_1(text.keyword)) {
131             throw new ImagingException("PNG tEXt chunk keyword is not ISO-8859-1: " + text.keyword);
132         }
133         if (!isValidISO_8859_1(text.languageTag)) {
134             throw new ImagingException("PNG tEXt chunk language tag is not ISO-8859-1: " + text.languageTag);
135         }
136 
137         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
138 
139         // keyword
140         baos.write(text.keyword.getBytes(StandardCharsets.ISO_8859_1));
141         baos.write(0);
142 
143         baos.write(1); // compressed flag, true
144         baos.write(PngConstants.COMPRESSION_DEFLATE_INFLATE); // compression method
145 
146         // language tag
147         baos.write(text.languageTag.getBytes(StandardCharsets.ISO_8859_1));
148         baos.write(0);
149 
150         // translated keyword
151         baos.write(text.translatedKeyword.getBytes(StandardCharsets.UTF_8));
152         baos.write(0);
153 
154         baos.write(deflate(text.text.getBytes(StandardCharsets.UTF_8)));
155 
156         writeChunk(os, ChunkType.iTXt, baos.toByteArray());
157     }
158 
159     private void writeChunkPHYS(final OutputStream os, final int xPPU, final int yPPU, final byte units) throws IOException {
160         final byte[] bytes = new byte[9];
161         bytes[0] = (byte) (0xff & xPPU >> 24);
162         bytes[1] = (byte) (0xff & xPPU >> 16);
163         bytes[2] = (byte) (0xff & xPPU >> 8);
164         bytes[3] = (byte) (0xff & xPPU >> 0);
165         bytes[4] = (byte) (0xff & yPPU >> 24);
166         bytes[5] = (byte) (0xff & yPPU >> 16);
167         bytes[6] = (byte) (0xff & yPPU >> 8);
168         bytes[7] = (byte) (0xff & yPPU >> 0);
169         bytes[8] = units;
170         writeChunk(os, ChunkType.pHYs, bytes);
171     }
172 
173     private void writeChunkPLTE(final OutputStream os, final Palette palette) throws IOException {
174         final int length = palette.length();
175         final byte[] bytes = Allocator.byteArray(length * 3);
176 
177         // Debug.debug("length", length);
178         for (int i = 0; i < length; i++) {
179             final int rgb = palette.getEntry(i);
180             final int index = i * 3;
181             // Debug.debug("index", index);
182             bytes[index + 0] = (byte) (0xff & rgb >> 16);
183             bytes[index + 1] = (byte) (0xff & rgb >> 8);
184             bytes[index + 2] = (byte) (0xff & rgb >> 0);
185         }
186 
187         writeChunk(os, ChunkType.PLTE, bytes);
188     }
189 
190     private void writeChunkSCAL(final OutputStream os, final double xUPP, final double yUPP, final byte units) throws IOException {
191         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
192 
193         // unit specifier
194         baos.write(units);
195 
196         // units per pixel, x-axis
197         baos.write(String.valueOf(xUPP).getBytes(StandardCharsets.ISO_8859_1));
198         baos.write(0);
199 
200         baos.write(String.valueOf(yUPP).getBytes(StandardCharsets.ISO_8859_1));
201 
202         writeChunk(os, ChunkType.sCAL, baos.toByteArray());
203     }
204 
205     private void writeChunktEXt(final OutputStream os, final AbstractPngText.Text text) throws IOException, ImagingException {
206         if (!isValidISO_8859_1(text.keyword)) {
207             throw new ImagingException("PNG tEXt chunk keyword is not ISO-8859-1: " + text.keyword);
208         }
209         if (!isValidISO_8859_1(text.text)) {
210             throw new ImagingException("PNG tEXt chunk text is not ISO-8859-1: " + text.text);
211         }
212 
213         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
214 
215         // keyword
216         baos.write(text.keyword.getBytes(StandardCharsets.ISO_8859_1));
217         baos.write(0);
218 
219         // text
220         baos.write(text.text.getBytes(StandardCharsets.ISO_8859_1));
221 
222         writeChunk(os, ChunkType.tEXt, baos.toByteArray());
223     }
224 
225     private void writeChunkTRNS(final OutputStream os, final Palette palette) throws IOException {
226         final byte[] bytes = Allocator.byteArray(palette.length());
227 
228         for (int i = 0; i < bytes.length; i++) {
229             bytes[i] = (byte) (0xff & palette.getEntry(i) >> 24);
230         }
231 
232         writeChunk(os, ChunkType.tRNS, bytes);
233     }
234 
235     private void writeChunkXmpiTXt(final OutputStream os, final String xmpXml) throws IOException {
236 
237         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
238 
239         // keyword
240         baos.write(PngConstants.XMP_KEYWORD.getBytes(StandardCharsets.ISO_8859_1));
241         baos.write(0);
242 
243         baos.write(1); // compressed flag, true
244         baos.write(PngConstants.COMPRESSION_DEFLATE_INFLATE); // compression method
245 
246         baos.write(0); // language tag (ignore). TODO
247 
248         // translated keyword
249         baos.write(PngConstants.XMP_KEYWORD.getBytes(StandardCharsets.UTF_8));
250         baos.write(0);
251 
252         baos.write(deflate(xmpXml.getBytes(StandardCharsets.UTF_8)));
253 
254         writeChunk(os, ChunkType.iTXt, baos.toByteArray());
255     }
256 
257     private void writeChunkzTXt(final OutputStream os, final AbstractPngText.Ztxt text) throws IOException, ImagingException {
258         if (!isValidISO_8859_1(text.keyword)) {
259             throw new ImagingException("PNG zTXt chunk keyword is not ISO-8859-1: " + text.keyword);
260         }
261         if (!isValidISO_8859_1(text.text)) {
262             throw new ImagingException("PNG zTXt chunk text is not ISO-8859-1: " + text.text);
263         }
264 
265         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
266 
267         // keyword
268         baos.write(text.keyword.getBytes(StandardCharsets.ISO_8859_1));
269         baos.write(0);
270 
271         // compression method
272         baos.write(PngConstants.COMPRESSION_DEFLATE_INFLATE);
273 
274         // text
275         baos.write(deflate(text.text.getBytes(StandardCharsets.ISO_8859_1)));
276 
277         writeChunk(os, ChunkType.zTXt, baos.toByteArray());
278     }
279 
280     /*
281      * between two chunk types indicates alternatives. Table 5.3 - Chunk ordering rules Critical chunks (shall appear in this order, except PLTE is optional)
282      * Chunk name Multiple allowed Ordering constraints IHDR No Shall be first PLTE No Before first IDAT IDAT Yes Multiple IDAT chunks shall be consecutive IEND
283      * No Shall be last Ancillary chunks (need not appear in this order) Chunk name Multiple allowed Ordering constraints cHRM No Before PLTE and IDAT gAMA No
284      * Before PLTE and IDAT iCCP No Before PLTE and IDAT. If the iCCP chunk is present, the sRGB chunk should not be present. sBIT No Before PLTE and IDAT sRGB
285      * No Before PLTE and IDAT. If the sRGB chunk is present, the iCCP chunk should not be present. bKGD No After PLTE; before IDAT hIST No After PLTE; before
286      * IDAT tRNS No After PLTE; before IDAT pHYs No Before IDAT sCAL No Before IDAT sPLT Yes Before IDAT tIME No None iTXt Yes None tEXt Yes None zTXt Yes None
287      */
288 
289     /**
290      * Writes an image to an output stream.
291      *
292      * @param src            The image to write.
293      * @param os             The output stream to write to.
294      * @param params         The parameters to use (can be {@code NULL} to use the default {@link PngImagingParameters}).
295      * @param paletteFactory The palette factory to use (can be {@code NULL} to use the default {@link PaletteFactory}).
296      * @throws ImagingException When errors are detected.
297      * @throws IOException      When IO problems occur.
298      */
299     public void writeImage(final BufferedImage src, final OutputStream os, PngImagingParameters params, PaletteFactory paletteFactory)
300             throws ImagingException, IOException {
301         if (params == null) {
302             params = new PngImagingParameters();
303         }
304         if (paletteFactory == null) {
305             paletteFactory = new PaletteFactory();
306         }
307         final int compressionLevel = Deflater.DEFAULT_COMPRESSION;
308 
309         final int width = src.getWidth();
310         final int height = src.getHeight();
311 
312         final boolean hasAlpha = paletteFactory.hasTransparency(src);
313         Debug.debug("hasAlpha: " + hasAlpha);
314         // int transparency = paletteFactory.getTransparency(src);
315 
316         boolean isGrayscale = paletteFactory.isGrayscale(src);
317         Debug.debug("isGrayscale: " + isGrayscale);
318 
319         PngColorType pngColorType;
320         {
321             final boolean forceIndexedColor = params.isForceIndexedColor();
322             final boolean forceTrueColor = params.isForceTrueColor();
323 
324             if (forceIndexedColor && forceTrueColor) {
325                 throw new ImagingException("Params: Cannot force both indexed and true color modes");
326             }
327             if (forceIndexedColor) {
328                 pngColorType = PngColorType.INDEXED_COLOR;
329             } else if (forceTrueColor) {
330                 pngColorType = hasAlpha ? PngColorType.TRUE_COLOR_WITH_ALPHA : PngColorType.TRUE_COLOR;
331                 isGrayscale = false;
332             } else {
333                 pngColorType = PngColorType.getColorType(hasAlpha, isGrayscale);
334             }
335             Debug.debug("colorType: " + pngColorType);
336         }
337 
338         final byte bitDepth = getBitDepth(pngColorType, params);
339         Debug.debug("bitDepth: " + bitDepth);
340 
341         int sampleDepth;
342         if (pngColorType == PngColorType.INDEXED_COLOR) {
343             sampleDepth = 8;
344         } else {
345             sampleDepth = bitDepth;
346         }
347         Debug.debug("sampleDepth: " + sampleDepth);
348 
349         {
350             PngConstants.PNG_SIGNATURE.writeTo(os);
351         }
352         {
353             // IHDR must be first
354 
355             final byte compressionMethod = PngConstants.COMPRESSION_TYPE_INFLATE_DEFLATE;
356             final byte filterMethod = PngConstants.FILTER_METHOD_ADAPTIVE;
357             final InterlaceMethod interlaceMethod = InterlaceMethod.NONE;
358 
359             final ImageHeader imageHeader = new ImageHeader(width, height, bitDepth, pngColorType, compressionMethod, filterMethod, interlaceMethod);
360 
361             writeChunkIHDR(os, imageHeader);
362         }
363 
364         // {
365         // sRGB No Before PLTE and IDAT. If the sRGB chunk is present, the
366         // iCCP chunk should not be present.
367 
368         // charles
369         // }
370 
371         Palette palette = null;
372         if (pngColorType == PngColorType.INDEXED_COLOR) {
373             // PLTE No Before first IDAT
374 
375             final int maxColors = 256;
376 
377             if (hasAlpha) {
378                 palette = paletteFactory.makeQuantizedRgbaPalette(src, hasAlpha, maxColors);
379                 writeChunkPLTE(os, palette);
380                 writeChunkTRNS(os, palette);
381             } else {
382                 palette = paletteFactory.makeQuantizedRgbPalette(src, maxColors);
383                 writeChunkPLTE(os, palette);
384             }
385         }
386 
387         final Object pixelDensityObj = params.getPixelDensity();
388         if (pixelDensityObj != null) {
389             final PixelDensity pixelDensity = (PixelDensity) pixelDensityObj;
390             if (pixelDensity.isUnitless()) {
391                 writeChunkPHYS(os, (int) Math.round(pixelDensity.getRawHorizontalDensity()), (int) Math.round(pixelDensity.getRawVerticalDensity()), (byte) 0);
392             } else {
393                 writeChunkPHYS(os, (int) Math.round(pixelDensity.horizontalDensityMetres()), (int) Math.round(pixelDensity.verticalDensityMetres()), (byte) 1);
394             }
395         }
396 
397         final PhysicalScale physicalScale = params.getPhysicalScale();
398         if (physicalScale != null) {
399             writeChunkSCAL(os, physicalScale.getHorizontalUnitsPerPixel(), physicalScale.getVerticalUnitsPerPixel(),
400                     physicalScale.isInMeters() ? (byte) 1 : (byte) 2);
401         }
402 
403         final String xmpXml = params.getXmpXml();
404         if (xmpXml != null) {
405             writeChunkXmpiTXt(os, xmpXml);
406         }
407 
408         final List<? extends AbstractPngText> outputTexts = params.getTextChunks();
409         if (outputTexts != null) {
410             for (final AbstractPngText text : outputTexts) {
411                 if (text instanceof AbstractPngText.Text) {
412                     writeChunktEXt(os, (AbstractPngText.Text) text);
413                 } else if (text instanceof AbstractPngText.Ztxt) {
414                     writeChunkzTXt(os, (AbstractPngText.Ztxt) text);
415                 } else if (text instanceof AbstractPngText.Itxt) {
416                     writeChunkiTXt(os, (AbstractPngText.Itxt) text);
417                 } else {
418                     throw new ImagingException("Unknown text to embed in PNG: " + text);
419                 }
420             }
421         }
422 
423         {
424             // Debug.debug("writing IDAT");
425 
426             // IDAT Yes Multiple IDAT chunks shall be consecutive
427 
428             // 28 March 2022. At this time, we only apply the predictor
429             // for non-grayscale, true-color images. This choice is made
430             // out of caution and is not necessarily required by the PNG
431             // spec. We may broaden the use of predictors in future versions.
432             final boolean usePredictor = params.isPredictorEnabled() && !isGrayscale && palette == null;
433 
434             byte[] uncompressed;
435             if (!usePredictor) {
436                 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
437 
438                 final boolean useAlpha = pngColorType == PngColorType.GREYSCALE_WITH_ALPHA || pngColorType == PngColorType.TRUE_COLOR_WITH_ALPHA;
439 
440                 final int[] row = Allocator.intArray(width);
441                 for (int y = 0; y < height; y++) {
442                     // Debug.debug("y", y + "/" + height);
443                     src.getRGB(0, y, width, 1, row, 0, width);
444 
445                     baos.write(FilterType.NONE.ordinal());
446                     for (int x = 0; x < width; x++) {
447                         final int argb = row[x];
448 
449                         if (palette != null) {
450                             final int index = palette.getPaletteIndex(argb);
451                             baos.write(0xff & index);
452                         } else {
453                             final int alpha = 0xff & argb >> 24;
454                             final int red = 0xff & argb >> 16;
455                             final int green = 0xff & argb >> 8;
456                             final int blue = 0xff & argb >> 0;
457 
458                             if (isGrayscale) {
459                                 final int gray = (red + green + blue) / 3;
460                                 // if (y == 0)
461                                 // {
462                                 // Debug.debug("gray: " + x + ", " + y +
463                                 // " argb: 0x"
464                                 // + Integer.toHexString(argb) + " gray: 0x"
465                                 // + Integer.toHexString(gray));
466                                 // // Debug.debug(x + ", " + y + " gray", gray);
467                                 // // Debug.debug(x + ", " + y + " gray", gray);
468                                 // Debug.debug(x + ", " + y + " gray", gray +
469                                 // " " + Integer.toHexString(gray));
470                                 // Debug.debug();
471                                 // }
472                                 baos.write(gray);
473                             } else {
474                                 baos.write(red);
475                                 baos.write(green);
476                                 baos.write(blue);
477                             }
478                             if (useAlpha) {
479                                 baos.write(alpha);
480                             }
481                         }
482                     }
483                 }
484                 uncompressed = baos.toByteArray();
485             } else {
486                 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
487 
488                 final boolean useAlpha = pngColorType == PngColorType.GREYSCALE_WITH_ALPHA || pngColorType == PngColorType.TRUE_COLOR_WITH_ALPHA;
489 
490                 final int[] row = Allocator.intArray(width);
491                 for (int y = 0; y < height; y++) {
492                     // Debug.debug("y", y + "/" + height);
493                     src.getRGB(0, y, width, 1, row, 0, width);
494 
495                     int priorA = 0;
496                     int priorR = 0;
497                     int priorG = 0;
498                     int priorB = 0;
499                     baos.write(FilterType.SUB.ordinal());
500                     for (int x = 0; x < width; x++) {
501                         final int argb = row[x];
502                         final int alpha = 0xff & argb >> 24;
503                         final int red = 0xff & argb >> 16;
504                         final int green = 0xff & argb >> 8;
505                         final int blue = 0xff & argb;
506 
507                         baos.write(red - priorR);
508                         baos.write(green - priorG);
509                         baos.write(blue - priorB);
510                         priorR = red;
511                         priorG = green;
512                         priorB = blue;
513 
514                         if (useAlpha) {
515                             baos.write(alpha - priorA);
516                             priorA = alpha;
517                         }
518                     }
519                 }
520                 uncompressed = baos.toByteArray();
521             }
522 
523             // Debug.debug("uncompressed", uncompressed.length);
524 
525             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
526             final int chunkSize = 256 * 1024;
527             final Deflater deflater = new Deflater(compressionLevel);
528             final DeflaterOutputStream dos = new DeflaterOutputStream(baos, deflater, chunkSize);
529 
530             for (int index = 0; index < uncompressed.length; index += chunkSize) {
531                 final int end = Math.min(uncompressed.length, index + chunkSize);
532                 final int length = end - index;
533 
534                 dos.write(uncompressed, index, length);
535                 dos.flush();
536                 baos.flush();
537 
538                 final byte[] compressed = baos.toByteArray();
539                 baos.reset();
540                 if (compressed.length > 0) {
541                     // Debug.debug("compressed", compressed.length);
542                     writeChunkIDAT(os, compressed);
543                 }
544 
545             }
546             {
547                 dos.finish();
548                 final byte[] compressed = baos.toByteArray();
549                 if (compressed.length > 0) {
550                     // Debug.debug("compressed final", compressed.length);
551                     writeChunkIDAT(os, compressed);
552                 }
553             }
554         }
555 
556         {
557             // IEND No Shall be last
558 
559             writeChunkIEND(os);
560         }
561 
562         /*
563          * Ancillary chunks (need not appear in this order) Chunk name Multiple allowed Ordering constraints cHRM No Before PLTE and IDAT gAMA No Before PLTE
564          * and IDAT iCCP No Before PLTE and IDAT. If the iCCP chunk is present, the sRGB chunk should not be present. sBIT No Before PLTE and IDAT sRGB No
565          * Before PLTE and IDAT. If the sRGB chunk is present, the iCCP chunk should not be present. bKGD No After PLTE; before IDAT hIST No After PLTE; before
566          * IDAT tRNS No After PLTE; before IDAT pHYs No Before IDAT sCAL No Before IDAT sPLT Yes Before IDAT tIME No None iTXt Yes None tEXt Yes None zTXt Yes
567          * None
568          */
569 
570         os.close();
571     } // todo: filter types
572       // proper color types
573       // srgb, etc.
574 
575     private void writeInt(final OutputStream os, final int value) throws IOException {
576         os.write(0xff & value >> 24);
577         os.write(0xff & value >> 16);
578         os.write(0xff & value >> 8);
579         os.write(0xff & value >> 0);
580     }
581 }