    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.imaging.formats.png;
    018
    019import java.awt.image.BufferedImage;
    020import java.io.ByteArrayOutputStream;
    021import java.io.IOException;
    022import java.io.OutputStream;
    023import java.nio.charset.StandardCharsets;
    024import java.util.List;
    025import java.util.zip.Deflater;
    026import java.util.zip.DeflaterOutputStream;
    027
    028import org.apache.commons.imaging.ImagingException;
    029import org.apache.commons.imaging.PixelDensity;
    030import org.apache.commons.imaging.common.Allocator;
    031import org.apache.commons.imaging.internal.Debug;
    032import org.apache.commons.imaging.palette.Palette;
    033import org.apache.commons.imaging.palette.PaletteFactory;
    034
    035public class PngWriter {
    036
    037    /*
    038     * 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
    039     * chunks. 4. IEND: image trailer, which is the last chunk in a PNG data stream.
    040     *
    041     * The remaining 14 chunk types are termed ancillary chunk types, which encoders may generate and decoders may interpret.
    042     *
    043     * 1. Transparency information: tRNS (see 11.3.2: Transparency information). 2. Color space information: cHRM, gAMA, iCCP, sBIT, sRGB (see 11.3.3: Color
    044     * space information). 3. Textual information: iTXt, tEXt, zTXt (see 11.3.4: Textual information). 4. Miscellaneous information: bKGD, hIST, pHYs, sPLT (see
    045     * 11.3.5: Miscellaneous information). 5. Time information: tIME (see 11.3.6: Time stamp information).
    046     */
    047
    048    private static final class ImageHeader {
    049        public final int width;
    050        public final int height;
    051        public final byte bitDepth;
    052        public final PngColorType pngColorType;
    053        public final byte compressionMethod;
    054        public final byte filterMethod;
    055        public final InterlaceMethod interlaceMethod;
    056
    057        ImageHeader(final int width, final int height, final byte bitDepth, final PngColorType pngColorType, final byte compressionMethod,
    058                final byte filterMethod, final InterlaceMethod interlaceMethod) {
    059            this.width = width;
    060            this.height = height;
    061            this.bitDepth = bitDepth;
    062            this.pngColorType = pngColorType;
    063            this.compressionMethod = compressionMethod;
    064            this.filterMethod = filterMethod;
    065            this.interlaceMethod = interlaceMethod;
    066        }
    067
    068    }
    069
    070    private byte[] deflate(final byte[] bytes) throws IOException {
    071        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
    072            try (DeflaterOutputStream dos = new DeflaterOutputStream(baos)) {
    073                dos.write(bytes);
    074                // dos.flush() doesn't work - we must close it before baos.toByteArray()
    075            }
    076            return baos.toByteArray();
    077        }
    078    }
    079
    080    private byte getBitDepth(final PngColorType pngColorType, final PngImagingParameters params) {
    081        final byte depth = params.getBitDepth();
    082
    083        return pngColorType.isBitDepthAllowed(depth) ? depth : PngImagingParameters.DEFAULT_BIT_DEPTH;
    084    }
    085
    086    private boolean isValidISO_8859_1(final String s) {
    087        final String roundtrip = new String(s.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.ISO_8859_1);
    088        return s.equals(roundtrip);
    089    }
    090
    091    private void writeChunk(final OutputStream os, final ChunkType chunkType, final byte[] data) throws IOException {
    092        final int dataLength = data == null ? 0 : data.length;
    093        writeInt(os, dataLength);
    094        os.write(chunkType.array);
    095        if (data != null) {
    096            os.write(data);
    097        }
    098
    099        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        final 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        final 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            final 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}