| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.commons.imaging.formats.png; |
| |
| import java.awt.image.BufferedImage; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.nio.charset.StandardCharsets; |
| import java.util.List; |
| import java.util.zip.Deflater; |
| import java.util.zip.DeflaterOutputStream; |
| |
| import org.apache.commons.imaging.ImagingException; |
| import org.apache.commons.imaging.PixelDensity; |
| import org.apache.commons.imaging.common.Allocator; |
| import org.apache.commons.imaging.internal.Debug; |
| import org.apache.commons.imaging.palette.Palette; |
| import org.apache.commons.imaging.palette.PaletteFactory; |
| |
| public class PngWriter { |
| |
| /* |
| 1. IHDR: image header, which is the first chunk in a PNG datastream. |
| 2. PLTE: palette table associated with indexed PNG images. |
| 3. IDAT: image data chunks. |
| 4. IEND: image trailer, which is the last chunk in a PNG datastream. |
| |
| The remaining 14 chunk types are termed ancillary chunk types, which encoders may generate and decoders may interpret. |
| |
| 1. Transparency information: tRNS (see 11.3.2: Transparency information). |
| 2. Color space information: cHRM, gAMA, iCCP, sBIT, sRGB (see 11.3.3: Color space information). |
| 3. Textual information: iTXt, tEXt, zTXt (see 11.3.4: Textual information). |
| 4. Miscellaneous information: bKGD, hIST, pHYs, sPLT (see 11.3.5: Miscellaneous information). |
| 5. Time information: tIME (see 11.3.6: Time stamp information). |
| */ |
| |
| private static class ImageHeader { |
| public final int width; |
| public final int height; |
| public final byte bitDepth; |
| public final PngColorType pngColorType; |
| public final byte compressionMethod; |
| public final byte filterMethod; |
| public final InterlaceMethod interlaceMethod; |
| |
| ImageHeader(final int width, final int height, final byte bitDepth, |
| final PngColorType pngColorType, final byte compressionMethod, final byte filterMethod, |
| final InterlaceMethod interlaceMethod) { |
| this.width = width; |
| this.height = height; |
| this.bitDepth = bitDepth; |
| this.pngColorType = pngColorType; |
| this.compressionMethod = compressionMethod; |
| this.filterMethod = filterMethod; |
| this.interlaceMethod = interlaceMethod; |
| } |
| |
| } |
| |
| private byte[] deflate(final byte[] bytes) throws IOException { |
| try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { |
| try (DeflaterOutputStream dos = new DeflaterOutputStream(baos)) { |
| dos.write(bytes); |
| // dos.flush() doesn't work - we must close it before baos.toByteArray() |
| } |
| return baos.toByteArray(); |
| } |
| } |
| |
| private byte getBitDepth(final PngColorType pngColorType, final PngImagingParameters params) { |
| final byte depth = params.getBitDepth(); |
| |
| return pngColorType.isBitDepthAllowed(depth) ? depth : PngImagingParameters.DEFAULT_BIT_DEPTH; |
| } |
| |
| private boolean isValidISO_8859_1(final String s) { |
| final String roundtrip = new String(s.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.ISO_8859_1); |
| return s.equals(roundtrip); |
| } |
| |
| private void writeChunk(final OutputStream os, final ChunkType chunkType, |
| final byte[] data) throws IOException { |
| final int dataLength = data == null ? 0 : data.length; |
| writeInt(os, dataLength); |
| os.write(chunkType.array); |
| if (data != null) { |
| os.write(data); |
| } |
| |
| final PngCrc png_crc = new PngCrc(); |
| |
| final long crc1 = png_crc.start_partial_crc(chunkType.array, chunkType.array.length); |
| final long crc2 = data == null ? crc1 : png_crc.continue_partial_crc( |
| crc1, data, data.length); |
| final int crc = (int) png_crc.finish_partial_crc(crc2); |
| |
| writeInt(os, crc); |
| } |
| |
| private void writeChunkIDAT(final OutputStream os, final byte[] bytes) |
| throws IOException { |
| writeChunk(os, ChunkType.IDAT, bytes); |
| } |
| |
| private void writeChunkIEND(final OutputStream os) throws IOException { |
| writeChunk(os, ChunkType.IEND, null); |
| } |
| |
| private void writeChunkIHDR(final OutputStream os, final ImageHeader value) throws IOException { |
| final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| writeInt(baos, value.width); |
| writeInt(baos, value.height); |
| baos.write(0xff & value.bitDepth); |
| baos.write(0xff & value.pngColorType.getValue()); |
| baos.write(0xff & value.compressionMethod); |
| baos.write(0xff & value.filterMethod); |
| baos.write(0xff & value.interlaceMethod.ordinal()); |
| |
| writeChunk(os, ChunkType.IHDR, baos.toByteArray()); |
| } |
| |
| private void writeChunkiTXt(final OutputStream os, final PngText.Itxt text) |
| throws IOException, ImagingException { |
| if (!isValidISO_8859_1(text.keyword)) { |
| throw new ImagingException("PNG tEXt chunk keyword is not ISO-8859-1: " + text.keyword); |
| } |
| if (!isValidISO_8859_1(text.languageTag)) { |
| throw new ImagingException("PNG tEXt chunk language tag is not ISO-8859-1: " + text.languageTag); |
| } |
| |
| final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| |
| // keyword |
| baos.write(text.keyword.getBytes(StandardCharsets.ISO_8859_1)); |
| baos.write(0); |
| |
| baos.write(1); // compressed flag, true |
| baos.write(PngConstants.COMPRESSION_DEFLATE_INFLATE); // compression method |
| |
| // language tag |
| baos.write(text.languageTag.getBytes(StandardCharsets.ISO_8859_1)); |
| baos.write(0); |
| |
| // translated keyword |
| baos.write(text.translatedKeyword.getBytes(StandardCharsets.UTF_8)); |
| baos.write(0); |
| |
| baos.write(deflate(text.text.getBytes(StandardCharsets.UTF_8))); |
| |
| writeChunk(os, ChunkType.iTXt, baos.toByteArray()); |
| } |
| |
| private void writeChunkPHYS(final OutputStream os, final int xPPU, final int yPPU, final byte units) |
| throws IOException { |
| final byte[] bytes = new byte[9]; |
| bytes[0] = (byte) (0xff & (xPPU >> 24)); |
| bytes[1] = (byte) (0xff & (xPPU >> 16)); |
| bytes[2] = (byte) (0xff & (xPPU >> 8)); |
| bytes[3] = (byte) (0xff & (xPPU >> 0)); |
| bytes[4] = (byte) (0xff & (yPPU >> 24)); |
| bytes[5] = (byte) (0xff & (yPPU >> 16)); |
| bytes[6] = (byte) (0xff & (yPPU >> 8)); |
| bytes[7] = (byte) (0xff & (yPPU >> 0)); |
| bytes[8] = units; |
| writeChunk(os, ChunkType.pHYs, bytes); |
| } |
| |
| private void writeChunkPLTE(final OutputStream os, final Palette palette) |
| throws IOException { |
| final int length = palette.length(); |
| final byte[] bytes = Allocator.byteArray(length * 3); |
| |
| // Debug.debug("length", length); |
| for (int i = 0; i < length; i++) { |
| final int rgb = palette.getEntry(i); |
| final int index = i * 3; |
| // Debug.debug("index", index); |
| bytes[index + 0] = (byte) (0xff & (rgb >> 16)); |
| bytes[index + 1] = (byte) (0xff & (rgb >> 8)); |
| bytes[index + 2] = (byte) (0xff & (rgb >> 0)); |
| } |
| |
| writeChunk(os, ChunkType.PLTE, bytes); |
| } |
| |
| private void writeChunkSCAL(final OutputStream os, final double xUPP, final double yUPP, final byte units) |
| throws IOException { |
| final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| |
| // unit specifier |
| baos.write(units); |
| |
| // units per pixel, x-axis |
| baos.write(String.valueOf(xUPP).getBytes(StandardCharsets.ISO_8859_1)); |
| baos.write(0); |
| |
| baos.write(String.valueOf(yUPP).getBytes(StandardCharsets.ISO_8859_1)); |
| |
| writeChunk(os, ChunkType.sCAL, baos.toByteArray()); |
| } |
| |
| private void writeChunktEXt(final OutputStream os, final PngText.Text text) |
| throws IOException, ImagingException { |
| if (!isValidISO_8859_1(text.keyword)) { |
| throw new ImagingException("PNG tEXt chunk keyword is not ISO-8859-1: " + text.keyword); |
| } |
| if (!isValidISO_8859_1(text.text)) { |
| throw new ImagingException("PNG tEXt chunk text is not ISO-8859-1: " + text.text); |
| } |
| |
| final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| |
| // keyword |
| baos.write(text.keyword.getBytes(StandardCharsets.ISO_8859_1)); |
| baos.write(0); |
| |
| // text |
| baos.write(text.text.getBytes(StandardCharsets.ISO_8859_1)); |
| |
| writeChunk(os, ChunkType.tEXt, baos.toByteArray()); |
| } |
| |
| private void writeChunkTRNS(final OutputStream os, final Palette palette) throws IOException { |
| final byte[] bytes = Allocator.byteArray(palette.length()); |
| |
| for (int i = 0; i < bytes.length; i++) { |
| bytes[i] = (byte) (0xff & (palette.getEntry(i) >> 24)); |
| } |
| |
| writeChunk(os, ChunkType.tRNS, bytes); |
| } |
| |
| private void writeChunkXmpiTXt(final OutputStream os, final String xmpXml) |
| throws IOException { |
| |
| final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| |
| // keyword |
| baos.write(PngConstants.XMP_KEYWORD.getBytes(StandardCharsets.ISO_8859_1)); |
| baos.write(0); |
| |
| baos.write(1); // compressed flag, true |
| baos.write(PngConstants.COMPRESSION_DEFLATE_INFLATE); // compression method |
| |
| baos.write(0); // language tag (ignore). TODO |
| |
| // translated keyword |
| baos.write(PngConstants.XMP_KEYWORD.getBytes(StandardCharsets.UTF_8)); |
| baos.write(0); |
| |
| baos.write(deflate(xmpXml.getBytes(StandardCharsets.UTF_8))); |
| |
| writeChunk(os, ChunkType.iTXt, baos.toByteArray()); |
| } |
| |
| private void writeChunkzTXt(final OutputStream os, final PngText.Ztxt text) |
| throws IOException, ImagingException { |
| if (!isValidISO_8859_1(text.keyword)) { |
| throw new ImagingException("PNG zTXt chunk keyword is not ISO-8859-1: " + text.keyword); |
| } |
| if (!isValidISO_8859_1(text.text)) { |
| throw new ImagingException("PNG zTXt chunk text is not ISO-8859-1: " + text.text); |
| } |
| |
| final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| |
| // keyword |
| baos.write(text.keyword.getBytes(StandardCharsets.ISO_8859_1)); |
| baos.write(0); |
| |
| // compression method |
| baos.write(PngConstants.COMPRESSION_DEFLATE_INFLATE); |
| |
| // text |
| baos.write(deflate(text.text.getBytes(StandardCharsets.ISO_8859_1))); |
| |
| writeChunk(os, ChunkType.zTXt, baos.toByteArray()); |
| } |
| |
| /* |
| between two chunk types indicates alternatives. |
| Table 5.3 - Chunk ordering rules |
| Critical chunks |
| (shall appear in this order, except PLTE is optional) |
| 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 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 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 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 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 |
| */ |
| |
| /** |
| * Writes an image to an output stream. |
| * |
| * @param src The image to write. |
| * @param os The output stream to write to. |
| * @param params The parameters to use (can be {@code NULL} to use the default parameters). |
| * @throws ImagingException When errors are detected. |
| * @throws IOException When IO problems occur. |
| */ |
| public void writeImage(final BufferedImage src, final OutputStream os, final PngImagingParameters params) throws ImagingException, IOException { |
| writeImage(src, os, params, new PaletteFactory()); |
| } |
| |
| /** |
| * Writes an image to an output stream. |
| * |
| * @param src The image to write. |
| * @param os The output stream to write to. |
| * @param params The parameters to use (can be {@code NULL} to use the default {@link PngImagingParameters}). |
| * @param paletteFactory The palette factory to use (can be {@code NULL} to use the default {@link PaletteFactory}). |
| * @throws ImagingException When errors are detected. |
| * @throws IOException When IO problems occur. |
| */ |
| public void writeImage(final BufferedImage src, final OutputStream os, PngImagingParameters params, PaletteFactory paletteFactory) throws ImagingException, IOException { |
| if (params == null) { |
| params = new PngImagingParameters(); |
| } |
| if (paletteFactory == null) { |
| paletteFactory = new PaletteFactory(); |
| } |
| final int compressionLevel = Deflater.DEFAULT_COMPRESSION; |
| |
| final int width = src.getWidth(); |
| final int height = src.getHeight(); |
| |
| final boolean hasAlpha = paletteFactory.hasTransparency(src); |
| Debug.debug("hasAlpha: " + hasAlpha); |
| // int transparency = paletteFactory.getTransparency(src); |
| |
| boolean isGrayscale = paletteFactory.isGrayscale(src); |
| Debug.debug("isGrayscale: " + isGrayscale); |
| |
| PngColorType pngColorType; |
| { |
| final boolean forceIndexedColor = params.isForceIndexedColor(); |
| final boolean forceTrueColor = params.isForceTrueColor(); |
| |
| if (forceIndexedColor && forceTrueColor) { |
| throw new ImagingException( |
| "Params: Cannot force both indexed and true color modes"); |
| } |
| if (forceIndexedColor) { |
| pngColorType = PngColorType.INDEXED_COLOR; |
| } else if (forceTrueColor) { |
| pngColorType = (hasAlpha ? PngColorType.TRUE_COLOR_WITH_ALPHA : PngColorType.TRUE_COLOR); |
| isGrayscale = false; |
| } else { |
| pngColorType = PngColorType.getColorType(hasAlpha, isGrayscale); |
| } |
| Debug.debug("colorType: " + pngColorType); |
| } |
| |
| final byte bitDepth = getBitDepth(pngColorType, params); |
| Debug.debug("bitDepth: " + bitDepth); |
| |
| int sampleDepth; |
| if (pngColorType == PngColorType.INDEXED_COLOR) { |
| sampleDepth = 8; |
| } else { |
| sampleDepth = bitDepth; |
| } |
| Debug.debug("sampleDepth: " + sampleDepth); |
| |
| { |
| PngConstants.PNG_SIGNATURE.writeTo(os); |
| } |
| { |
| // IHDR must be first |
| |
| final byte compressionMethod = PngConstants.COMPRESSION_TYPE_INFLATE_DEFLATE; |
| final byte filterMethod = PngConstants.FILTER_METHOD_ADAPTIVE; |
| final InterlaceMethod interlaceMethod = InterlaceMethod.NONE; |
| |
| final ImageHeader imageHeader = new ImageHeader(width, height, bitDepth, |
| pngColorType, compressionMethod, filterMethod, interlaceMethod); |
| |
| writeChunkIHDR(os, imageHeader); |
| } |
| |
| //{ |
| // sRGB No Before PLTE and IDAT. If the sRGB chunk is present, the |
| // iCCP chunk should not be present. |
| |
| // charles |
| //} |
| |
| Palette palette = null; |
| if (pngColorType == PngColorType.INDEXED_COLOR) { |
| // PLTE No Before first IDAT |
| |
| final int maxColors = 256; |
| |
| if (hasAlpha) { |
| palette = paletteFactory.makeQuantizedRgbaPalette(src, hasAlpha, maxColors); |
| writeChunkPLTE(os, palette); |
| writeChunkTRNS(os, palette); |
| } else { |
| palette = paletteFactory.makeQuantizedRgbPalette(src, maxColors); |
| writeChunkPLTE(os, palette); |
| } |
| } |
| |
| final Object pixelDensityObj = params.getPixelDensity(); |
| if (pixelDensityObj != null) { |
| final PixelDensity pixelDensity = (PixelDensity) pixelDensityObj; |
| if (pixelDensity.isUnitless()) { |
| writeChunkPHYS(os, (int) Math.round(pixelDensity.getRawHorizontalDensity()), |
| (int) Math.round(pixelDensity.getRawVerticalDensity()), |
| (byte) 0); |
| } else { |
| writeChunkPHYS(os, (int) Math.round(pixelDensity.horizontalDensityMetres()), |
| (int) Math.round(pixelDensity.verticalDensityMetres()), |
| (byte) 1); |
| } |
| } |
| |
| final PhysicalScale physicalScale = params.getPhysicalScale(); |
| if (physicalScale != null) { |
| writeChunkSCAL( |
| os, |
| physicalScale.getHorizontalUnitsPerPixel(), |
| physicalScale.getVerticalUnitsPerPixel(), |
| physicalScale.isInMeters() ? (byte) 1 : (byte) 2); |
| } |
| |
| final String xmpXml = params.getXmpXml(); |
| if (xmpXml != null) { |
| writeChunkXmpiTXt(os, xmpXml); |
| } |
| |
| final List<? extends PngText> outputTexts = params.getTextChunks(); |
| if (outputTexts != null) { |
| for (final PngText text : outputTexts) { |
| if (text instanceof PngText.Text) { |
| writeChunktEXt(os, (PngText.Text) text); |
| } else if (text instanceof PngText.Ztxt) { |
| writeChunkzTXt(os, (PngText.Ztxt) text); |
| } else if (text instanceof PngText.Itxt) { |
| writeChunkiTXt(os, (PngText.Itxt) text); |
| } else { |
| throw new ImagingException("Unknown text to embed in PNG: " + text); |
| } |
| } |
| } |
| |
| { |
| // Debug.debug("writing IDAT"); |
| |
| // IDAT Yes Multiple IDAT chunks shall be consecutive |
| |
| // 28 March 2022. At this time, we only apply the predictor |
| // for non-grayscale, true-color images. This choice is made |
| // out of caution and is not necessarily required by the PNG |
| // spec. We may broaden the use of predictors in future versions. |
| final boolean usePredictor = params.isPredictorEnabled() && |
| !isGrayscale && palette==null; |
| |
| byte[] uncompressed; |
| if(!usePredictor) { |
| final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| |
| final boolean useAlpha = pngColorType == PngColorType.GREYSCALE_WITH_ALPHA |
| || pngColorType == PngColorType.TRUE_COLOR_WITH_ALPHA; |
| |
| final int[] row = Allocator.intArray(width); |
| for (int y = 0; y < height; y++) { |
| // Debug.debug("y", y + "/" + height); |
| src.getRGB(0, y, width, 1, row, 0, width); |
| |
| baos.write(FilterType.NONE.ordinal()); |
| for (int x = 0; x < width; x++) { |
| final int argb = row[x]; |
| |
| if (palette != null) { |
| final int index = palette.getPaletteIndex(argb); |
| baos.write(0xff & index); |
| } else { |
| final int alpha = 0xff & (argb >> 24); |
| final int red = 0xff & (argb >> 16); |
| final int green = 0xff & (argb >> 8); |
| final int blue = 0xff & (argb >> 0); |
| |
| if (isGrayscale) { |
| final int gray = (red + green + blue) / 3; |
| // if (y == 0) |
| // { |
| // Debug.debug("gray: " + x + ", " + y + |
| // " argb: 0x" |
| // + Integer.toHexString(argb) + " gray: 0x" |
| // + Integer.toHexString(gray)); |
| // // Debug.debug(x + ", " + y + " gray", gray); |
| // // Debug.debug(x + ", " + y + " gray", gray); |
| // Debug.debug(x + ", " + y + " gray", gray + |
| // " " + Integer.toHexString(gray)); |
| // Debug.debug(); |
| // } |
| baos.write(gray); |
| } else { |
| baos.write(red); |
| baos.write(green); |
| baos.write(blue); |
| } |
| if (useAlpha) { |
| baos.write(alpha); |
| } |
| } |
| } |
| } |
| uncompressed = baos.toByteArray(); |
| } else { |
| final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| |
| final boolean useAlpha = pngColorType == PngColorType.GREYSCALE_WITH_ALPHA |
| || pngColorType == PngColorType.TRUE_COLOR_WITH_ALPHA; |
| |
| final int[] row = Allocator.intArray(width); |
| for (int y = 0; y < height; y++) { |
| // Debug.debug("y", y + "/" + height); |
| src.getRGB(0, y, width, 1, row, 0, width); |
| |
| int priorA = 0; |
| int priorR = 0; |
| int priorG = 0; |
| int priorB = 0; |
| baos.write(FilterType.SUB.ordinal()); |
| for (int x = 0; x < width; x++) { |
| final int argb = row[x]; |
| final int alpha = 0xff & (argb >> 24); |
| final int red = 0xff & (argb >> 16); |
| final int green = 0xff & (argb >> 8); |
| final int blue = 0xff & argb; |
| |
| baos.write(red - priorR); |
| baos.write(green - priorG); |
| baos.write(blue - priorB); |
| priorR = red; |
| priorG = green; |
| priorB = blue; |
| |
| if (useAlpha) { |
| baos.write(alpha - priorA); |
| priorA = alpha; |
| } |
| } |
| } |
| uncompressed = baos.toByteArray(); |
| } |
| |
| |
| // Debug.debug("uncompressed", uncompressed.length); |
| |
| final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| final int chunkSize = 256 * 1024; |
| final Deflater deflater = new Deflater(compressionLevel); |
| final DeflaterOutputStream dos = new DeflaterOutputStream(baos,deflater,chunkSize); |
| |
| for (int index = 0; index < uncompressed.length; index += chunkSize) { |
| final int end = Math.min(uncompressed.length, index + chunkSize); |
| final int length = end - index; |
| |
| dos.write(uncompressed, index, length); |
| dos.flush(); |
| baos.flush(); |
| |
| final byte[] compressed = baos.toByteArray(); |
| baos.reset(); |
| if (compressed.length > 0) { |
| // Debug.debug("compressed", compressed.length); |
| writeChunkIDAT(os, compressed); |
| } |
| |
| } |
| { |
| dos.finish(); |
| final byte[] compressed = baos.toByteArray(); |
| if (compressed.length > 0) { |
| // Debug.debug("compressed final", compressed.length); |
| writeChunkIDAT(os, compressed); |
| } |
| } |
| } |
| |
| { |
| // IEND No Shall be last |
| |
| writeChunkIEND(os); |
| } |
| |
| /* |
| Ancillary chunks |
| (need not appear in this order) |
| Chunk name Multiple allowed Ordering constraints |
| cHRM No Before PLTE and IDAT |
| gAMA No 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 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 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 |
| */ |
| |
| os.close(); |
| } // todo: filter types |
| // proper colour types |
| // srgb, etc. |
| |
| private void writeInt(final OutputStream os, final int value) throws IOException { |
| os.write(0xff & (value >> 24)); |
| os.write(0xff & (value >> 16)); |
| os.write(0xff & (value >> 8)); |
| os.write(0xff & (value >> 0)); |
| } |
| } |