/* * @(#)PNGImageReader.java 1.55 03/12/19 * * Copyright 2004 Sun Microsystems, Inc. All rights reserved. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. */ package com.sun.imageio.plugins.png; import java.awt.Point; import java.awt.Rectangle; import java.awt.color.ColorSpace; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferUShort; import java.awt.image.Raster; import java.awt.image.WritableRaster; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.InputStream; import java.io.IOException; import java.io.SequenceInputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; import javax.imageio.IIOException; import javax.imageio.ImageReader; import javax.imageio.ImageReadParam; import javax.imageio.ImageTypeSpecifier; import javax.imageio.metadata.IIOMetadata; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import com.sun.imageio.plugins.common.InputStreamAdapter; import com.sun.imageio.plugins.common.SubImageInputStream; class PNGImageDataEnumeration implements Enumeration { boolean firstTime = true; ImageInputStream stream; int length; public PNGImageDataEnumeration(ImageInputStream stream) throws IOException { this.stream = stream; this.length = stream.readInt(); int type = stream.readInt(); // skip chunk type } public Object nextElement() { try { firstTime = false; ImageInputStream iis = new SubImageInputStream(stream, length); return new InputStreamAdapter(iis); } catch (IOException e) { return null; } } public boolean hasMoreElements() { if (firstTime) { return true; } try { int crc = stream.readInt(); this.length = stream.readInt(); int type = stream.readInt(); if (type == PNGImageReader.IDAT_TYPE) { return true; } else { return false; } } catch (IOException e) { return false; } } } /** * @version 0.5 */ public class PNGImageReader extends ImageReader { // Critical chunks static final int IHDR_TYPE = chunkType("IHDR"); static final int PLTE_TYPE = chunkType("PLTE"); static final int IDAT_TYPE = chunkType("IDAT"); static final int IEND_TYPE = chunkType("IEND"); // Ancillary chunks static final int bKGD_TYPE = chunkType("bKGD"); static final int cHRM_TYPE = chunkType("cHRM"); static final int gAMA_TYPE = chunkType("gAMA"); static final int hIST_TYPE = chunkType("hIST"); static final int iCCP_TYPE = chunkType("iCCP"); static final int iTXt_TYPE = chunkType("iTXt"); static final int pHYs_TYPE = chunkType("pHYs"); static final int sBIT_TYPE = chunkType("sBIT"); static final int sPLT_TYPE = chunkType("sPLT"); static final int sRGB_TYPE = chunkType("sRGB"); static final int tEXt_TYPE = chunkType("tEXt"); static final int tIME_TYPE = chunkType("tIME"); static final int tRNS_TYPE = chunkType("tRNS"); static final int zTXt_TYPE = chunkType("zTXt"); static final int PNG_COLOR_GRAY = 0; static final int PNG_COLOR_RGB = 2; static final int PNG_COLOR_PALETTE = 3; static final int PNG_COLOR_GRAY_ALPHA = 4; static final int PNG_COLOR_RGB_ALPHA = 6; // The number of bands by PNG color type static final int[] inputBandsForColorType = { 1, // gray -1, // unused 3, // rgb 1, // palette 2, // gray + alpha -1, // unused 4 // rgb + alpha }; static final int PNG_FILTER_NONE = 0; static final int PNG_FILTER_SUB = 1; static final int PNG_FILTER_UP = 2; static final int PNG_FILTER_AVERAGE = 3; static final int PNG_FILTER_PAETH = 4; static final int[] adam7XOffset = { 0, 4, 0, 2, 0, 1, 0 }; static final int[] adam7YOffset = { 0, 0, 4, 0, 2, 0, 1 }; static final int[] adam7XSubsampling = { 8, 8, 4, 4, 2, 2, 1, 1 }; static final int[] adam7YSubsampling = { 8, 8, 8, 4, 4, 2, 2, 1 }; private static final boolean debug = true; ImageInputStream stream = null; boolean gotHeader = false; boolean gotMetadata = false; ImageReadParam lastParam = null; long imageStartPosition = -1L; Rectangle sourceRegion = null; int sourceXSubsampling = -1; int sourceYSubsampling = -1; int sourceMinProgressivePass = 0; int sourceMaxProgressivePass = 6; int[] sourceBands = null; int[] destinationBands = null; Point destinationOffset = new Point(0, 0); PNGMetadata metadata = new PNGMetadata(); DataInputStream pixelStream = null; BufferedImage theImage = null; // The number of source pixels processed int pixelsDone = 0; // The total number of pixels in the source image int totalPixels; public PNGImageReader(ImageReaderSpi originatingProvider) { super(originatingProvider); } public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { super.setInput(input, seekForwardOnly, ignoreMetadata); this.stream = (ImageInputStream)input; // Always works // Clear all values based on the previous stream contents resetStreamSettings(); } // Callable from ImageWriter static int chunkType(String typeString) { char c0 = typeString.charAt(0); char c1 = typeString.charAt(1); char c2 = typeString.charAt(2); char c3 = typeString.charAt(3); int type = (c0 << 24) | (c1 << 16) | (c2 << 8) | c3; return type; } private String readNullTerminatedString() throws IOException { StringBuffer b = new StringBuffer(); int c; while ((c = stream.read()) != 0) { b.append((char)c); } return b.toString(); } private void readHeader() throws IIOException { if (gotHeader) { return; } if (stream == null) { throw new IllegalStateException("Input source not set!"); } try { byte[] signature = new byte[8]; stream.readFully(signature); if (signature[0] != (byte)137 || signature[1] != (byte)80 || signature[2] != (byte)78 || signature[3] != (byte)71 || signature[4] != (byte)13 || signature[5] != (byte)10 || signature[6] != (byte)26 || signature[7] != (byte)10) { throw new IIOException("Bad PNG signature!"); } int IHDR_length = stream.readInt(); if (IHDR_length != 13) { throw new IIOException("Bad length for IHDR chunk!"); } int IHDR_type = stream.readInt(); if (IHDR_type != IHDR_TYPE) { throw new IIOException("Bad type for IHDR chunk!"); } this.metadata = new PNGMetadata(); int width = stream.readInt(); int height = stream.readInt(); int bitDepth = stream.readUnsignedByte(); int colorType = stream.readUnsignedByte(); int compressionMethod = stream.readUnsignedByte(); int filterMethod = stream.readUnsignedByte(); int interlaceMethod = stream.readUnsignedByte(); int IHDR_CRC = stream.readInt(); stream.flushBefore(stream.getStreamPosition()); if (width == 0) { throw new IIOException("Image width == 0!"); } if (height == 0) { throw new IIOException("Image height == 0!"); } if (bitDepth != 1 && bitDepth != 2 && bitDepth != 4 && bitDepth != 8 && bitDepth != 16) { throw new IIOException("Bit depth must be 1, 2, 4, 8, or 16!"); } if (colorType != 0 && colorType != 2 && colorType != 3 && colorType != 4 && colorType != 6) { throw new IIOException("Color type must be 0, 2, 3, 4, or 6!"); } if (colorType == PNG_COLOR_PALETTE && bitDepth == 16) { throw new IIOException("Bad color type/bit depth combination!"); } if ((colorType == PNG_COLOR_RGB || colorType == PNG_COLOR_RGB_ALPHA || colorType == PNG_COLOR_GRAY_ALPHA) && (bitDepth != 8 && bitDepth != 16)) { throw new IIOException("Bad color type/bit depth combination!"); } if (compressionMethod != 0) { throw new IIOException("Unknown compression method (not 0)!"); } if (filterMethod != 0) { throw new IIOException("Unknown filter method (not 0)!"); } if (interlaceMethod != 0 && interlaceMethod != 1) { throw new IIOException("Unknown interlace method (not 0 or 1)!"); } metadata.IHDR_present = true; metadata.IHDR_width = width; metadata.IHDR_height = height; metadata.IHDR_bitDepth = bitDepth; metadata.IHDR_colorType = colorType; metadata.IHDR_compressionMethod = compressionMethod; metadata.IHDR_filterMethod = filterMethod; metadata.IHDR_interlaceMethod = interlaceMethod; gotHeader = true; } catch (IOException e) { throw new IIOException("I/O error reading PNG header!", e); } } private void parse_PLTE_chunk(int chunkLength) throws IOException { if (metadata.PLTE_present) { processWarningOccurred( "A PNG image may not contain more than one PLTE chunk.\n" + "The chunk wil be ignored."); return; } else if (metadata.IHDR_colorType == PNG_COLOR_GRAY || metadata.IHDR_colorType == PNG_COLOR_GRAY_ALPHA) { processWarningOccurred( "A PNG gray or gray alpha image cannot have a PLTE chunk.\n" + "The chunk wil be ignored."); return; } byte[] palette = new byte[chunkLength]; stream.readFully(palette); int numEntries = chunkLength/3; if (metadata.IHDR_colorType == PNG_COLOR_PALETTE) { int maxEntries = 1 << metadata.IHDR_bitDepth; if (numEntries > maxEntries) { processWarningOccurred( "PLTE chunk contains too many entries for bit depth, ignoring extras."); numEntries = maxEntries; } numEntries = Math.min(numEntries, maxEntries); } // Round array sizes up to 2^2^n int paletteEntries; if (numEntries > 16) { paletteEntries = 256; } else if (numEntries > 4) { paletteEntries = 16; } else if (numEntries > 2) { paletteEntries = 4; } else { paletteEntries = 2; } metadata.PLTE_present = true; metadata.PLTE_red = new byte[paletteEntries]; metadata.PLTE_green = new byte[paletteEntries]; metadata.PLTE_blue = new byte[paletteEntries]; int index = 0; for (int i = 0; i < numEntries; i++) { metadata.PLTE_red[i] = palette[index++]; metadata.PLTE_green[i] = palette[index++]; metadata.PLTE_blue[i] = palette[index++]; } } private void parse_bKGD_chunk() throws IOException { if (metadata.IHDR_colorType == PNG_COLOR_PALETTE) { metadata.bKGD_colorType = PNG_COLOR_PALETTE; metadata.bKGD_index = stream.readUnsignedByte(); } else if (metadata.IHDR_colorType == PNG_COLOR_GRAY || metadata.IHDR_colorType == PNG_COLOR_GRAY_ALPHA) { metadata.bKGD_colorType = PNG_COLOR_GRAY; metadata.bKGD_gray = stream.readUnsignedShort(); } else { // RGB or RGB_ALPHA metadata.bKGD_colorType = PNG_COLOR_RGB; metadata.bKGD_red = stream.readUnsignedShort(); metadata.bKGD_green = stream.readUnsignedShort(); metadata.bKGD_blue = stream.readUnsignedShort(); } metadata.bKGD_present = true; } private void parse_cHRM_chunk() throws IOException { metadata.cHRM_whitePointX = stream.readInt(); metadata.cHRM_whitePointY = stream.readInt(); metadata.cHRM_redX = stream.readInt(); metadata.cHRM_redY = stream.readInt(); metadata.cHRM_greenX = stream.readInt(); metadata.cHRM_greenY = stream.readInt(); metadata.cHRM_blueX = stream.readInt(); metadata.cHRM_blueY = stream.readInt(); metadata.cHRM_present = true; } private void parse_gAMA_chunk() throws IOException { int gamma = stream.readInt(); metadata.gAMA_gamma = gamma; metadata.gAMA_present = true; } private void parse_hIST_chunk() throws IOException, IIOException { if (!metadata.PLTE_present) { throw new IIOException("hIST chunk without prior PLTE chunk!"); } metadata.hIST_histogram = new char[metadata.PLTE_red.length]; stream.readFully(metadata.hIST_histogram, 0, metadata.hIST_histogram.length); metadata.hIST_present = true; } private void parse_iCCP_chunk(int chunkLength) throws IOException { String keyword = readNullTerminatedString(); metadata.iCCP_profileName = keyword; metadata.iCCP_compressionMethod = stream.readUnsignedByte(); byte[] compressedProfile = new byte[chunkLength - keyword.length() - 2]; stream.readFully(compressedProfile); metadata.iCCP_compressedProfile = compressedProfile; metadata.iCCP_present = true; } private void parse_iTXt_chunk(int chunkLength) throws IOException { long chunkStart = stream.getStreamPosition(); String keyword = readNullTerminatedString(); metadata.iTXt_keyword.add(keyword); int compressionFlag = stream.readUnsignedByte(); metadata.iTXt_compressionFlag.add(new Integer(compressionFlag)); int compressionMethod = stream.readUnsignedByte(); metadata.iTXt_compressionMethod.add(new Integer(compressionMethod)); String languageTag = readNullTerminatedString(); metadata.iTXt_languageTag.add(languageTag); String translatedKeyword = stream.readUTF(); metadata.iTXt_translatedKeyword.add(translatedKeyword); stream.skipBytes(1); // Null separator String text; if (compressionFlag == 1) { // Decompress the text long pos = stream.getStreamPosition(); byte[] b = new byte[(int)(chunkStart + chunkLength - pos)]; stream.readFully(b); text = inflate(b); } else { text = stream.readUTF(); } metadata.iTXt_text.add(text); } private void parse_pHYs_chunk() throws IOException { metadata.pHYs_pixelsPerUnitXAxis = stream.readInt(); metadata.pHYs_pixelsPerUnitYAxis = stream.readInt(); metadata.pHYs_unitSpecifier = stream.readUnsignedByte(); metadata.pHYs_present = true; } private void parse_sBIT_chunk() throws IOException { int colorType = metadata.IHDR_colorType; if (colorType == PNG_COLOR_GRAY || colorType == PNG_COLOR_GRAY_ALPHA) { metadata.sBIT_grayBits = stream.readUnsignedByte(); } else if (colorType == PNG_COLOR_RGB || colorType == PNG_COLOR_PALETTE || colorType == PNG_COLOR_RGB_ALPHA) { metadata.sBIT_redBits = stream.readUnsignedByte(); metadata.sBIT_greenBits = stream.readUnsignedByte(); metadata.sBIT_blueBits = stream.readUnsignedByte(); } if (colorType == PNG_COLOR_GRAY_ALPHA || colorType == PNG_COLOR_RGB_ALPHA) { metadata.sBIT_alphaBits = stream.readUnsignedByte(); } metadata.sBIT_colorType = colorType; metadata.sBIT_present = true; } private void parse_sPLT_chunk(int chunkLength) throws IOException, IIOException { metadata.sPLT_paletteName = readNullTerminatedString(); chunkLength -= metadata.sPLT_paletteName.length() + 1; int sampleDepth = stream.readUnsignedByte(); metadata.sPLT_sampleDepth = sampleDepth; int numEntries = chunkLength/(4*(sampleDepth/8) + 2); metadata.sPLT_red = new int[numEntries]; metadata.sPLT_green = new int[numEntries]; metadata.sPLT_blue = new int[numEntries]; metadata.sPLT_alpha = new int[numEntries]; metadata.sPLT_frequency = new int[numEntries]; if (sampleDepth == 8) { for (int i = 0; i < numEntries; i++) { metadata.sPLT_red[i] = stream.readUnsignedByte(); metadata.sPLT_green[i] = stream.readUnsignedByte(); metadata.sPLT_blue[i] = stream.readUnsignedByte(); metadata.sPLT_alpha[i] = stream.readUnsignedByte(); metadata.sPLT_frequency[i] = stream.readUnsignedShort(); } } else if (sampleDepth == 16) { for (int i = 0; i < numEntries; i++) { metadata.sPLT_red[i] = stream.readUnsignedShort(); metadata.sPLT_green[i] = stream.readUnsignedShort(); metadata.sPLT_blue[i] = stream.readUnsignedShort(); metadata.sPLT_alpha[i] = stream.readUnsignedShort(); metadata.sPLT_frequency[i] = stream.readUnsignedShort(); } } else { throw new IIOException("sPLT sample depth not 8 or 16!"); } metadata.sPLT_present = true; } private void parse_sRGB_chunk() throws IOException { metadata.sRGB_renderingIntent = stream.readUnsignedByte(); metadata.sRGB_present = true; } private void parse_tEXt_chunk(int chunkLength) throws IOException { String keyword = readNullTerminatedString(); metadata.tEXt_keyword.add(keyword); byte[] b = new byte[chunkLength - keyword.length() - 1]; stream.readFully(b); metadata.tEXt_text.add(new String(b)); } private void parse_tIME_chunk() throws IOException { metadata.tIME_year = stream.readUnsignedShort(); metadata.tIME_month = stream.readUnsignedByte(); metadata.tIME_day = stream.readUnsignedByte(); metadata.tIME_hour = stream.readUnsignedByte(); metadata.tIME_minute = stream.readUnsignedByte(); metadata.tIME_second = stream.readUnsignedByte(); metadata.tIME_present = true; } private void parse_tRNS_chunk(int chunkLength) throws IOException { int colorType = metadata.IHDR_colorType; if (colorType == PNG_COLOR_PALETTE) { if (!metadata.PLTE_present) { processWarningOccurred( "tRNS chunk without prior PLTE chunk, ignoring it."); return; } // Alpha table may have fewer entries than RGB palette int maxEntries = metadata.PLTE_red.length; int numEntries = chunkLength; if (numEntries > maxEntries) { processWarningOccurred( "tRNS chunk has more entries than prior PLTE chunk, ignoring extras."); numEntries = maxEntries; } metadata.tRNS_alpha = new byte[numEntries]; metadata.tRNS_colorType = PNG_COLOR_PALETTE; stream.read(metadata.tRNS_alpha, 0, numEntries); stream.skipBytes(chunkLength - numEntries); } else if (colorType == PNG_COLOR_GRAY) { if (chunkLength != 2) { processWarningOccurred( "tRNS chunk for gray image must have length 2, ignoring chunk."); stream.skipBytes(chunkLength); return; } metadata.tRNS_gray = stream.readUnsignedShort(); metadata.tRNS_colorType = PNG_COLOR_GRAY; } else if (colorType == PNG_COLOR_RGB) { if (chunkLength != 6) { processWarningOccurred( "tRNS chunk for RGB image must have length 6, ignoring chunk."); stream.skipBytes(chunkLength); return; } metadata.tRNS_red = stream.readUnsignedShort(); metadata.tRNS_green = stream.readUnsignedShort(); metadata.tRNS_blue = stream.readUnsignedShort(); metadata.tRNS_colorType = PNG_COLOR_RGB; } else { processWarningOccurred( "Gray+Alpha and RGBS images may not have a tRNS chunk, ignoring it."); return; } metadata.tRNS_present = true; } private static String inflate(byte[] b) throws IOException { InputStream bais = new ByteArrayInputStream(b); InputStream iis = new InflaterInputStream(bais); StringBuffer sb = new StringBuffer(80); int c; while ((c = iis.read()) != -1) { sb.append((char)c); } return sb.toString(); } private void parse_zTXt_chunk(int chunkLength) throws IOException { String keyword = readNullTerminatedString(); metadata.zTXt_keyword.add(keyword); int method = stream.readUnsignedByte(); metadata.zTXt_compressionMethod.add(new Integer(method)); byte[] b = new byte[chunkLength - keyword.length() - 2]; stream.readFully(b); metadata.zTXt_text.add(inflate(b)); } private void readMetadata() throws IIOException { if (gotMetadata) { return; } readHeader(); try { while (true) { int chunkLength = stream.readInt(); int chunkType = stream.readInt(); // If chunk type is 'IDAT', we've reached the image data. if (chunkType == IDAT_TYPE) { stream.skipBytes(-8); imageStartPosition = stream.getStreamPosition(); break; } if (chunkType == PLTE_TYPE) { parse_PLTE_chunk(chunkLength); } else if (chunkType == bKGD_TYPE) { parse_bKGD_chunk(); } else if (chunkType == cHRM_TYPE) { parse_cHRM_chunk(); } else if (chunkType == gAMA_TYPE) { parse_gAMA_chunk(); } else if (chunkType == hIST_TYPE) { parse_hIST_chunk(); } else if (chunkType == iCCP_TYPE) { parse_iCCP_chunk(chunkLength); } else if (chunkType == iTXt_TYPE) { parse_iTXt_chunk(chunkLength); } else if (chunkType == pHYs_TYPE) { parse_pHYs_chunk(); } else if (chunkType == sBIT_TYPE) { parse_sBIT_chunk(); } else if (chunkType == sPLT_TYPE) { parse_sPLT_chunk(chunkLength); } else if (chunkType == sRGB_TYPE) { parse_sRGB_chunk(); } else if (chunkType == tEXt_TYPE) { parse_tEXt_chunk(chunkLength); } else if (chunkType == tIME_TYPE) { parse_tIME_chunk(); } else if (chunkType == tRNS_TYPE) { parse_tRNS_chunk(chunkLength); } else if (chunkType == zTXt_TYPE) { parse_zTXt_chunk(chunkLength); } else { // Read an unknown chunk byte[] b = new byte[chunkLength]; stream.readFully(b); StringBuffer chunkName = new StringBuffer(4); chunkName.append((char)(chunkType >>> 24)); chunkName.append((char)((chunkType >> 16) & 0xff)); chunkName.append((char)((chunkType >> 8) & 0xff)); chunkName.append((char)(chunkType & 0xff)); int ancillaryBit = chunkType >>> 28; if (ancillaryBit == 0) { processWarningOccurred( "Encountered unknown chunk with critical bit set!"); } metadata.unknownChunkType.add(chunkName.toString()); metadata.unknownChunkData.add(b); } int chunkCRC = stream.readInt(); stream.flushBefore(stream.getStreamPosition()); } } catch (IOException e) { e.printStackTrace(); throw new IIOException("Error reading PNG metadata", e); } gotMetadata = true; } // Data filtering methods private static void decodeSubFilter(byte[] curr, int coff, int count, int bpp) { for (int i = bpp; i < count; i++) { int val; val = curr[i + coff] & 0xff; val += curr[i + coff - bpp] & 0xff; curr[i + coff] = (byte)val; } } private static void decodeUpFilter(byte[] curr, int coff, byte[] prev, int poff, int count) { for (int i = 0; i < count; i++) { int raw = curr[i + coff] & 0xff; int prior = prev[i + poff] & 0xff; curr[i + coff] = (byte)(raw + prior); } } private static void decodeAverageFilter(byte[] curr, int coff, byte[] prev, int poff, int count, int bpp) { int raw, priorPixel, priorRow; for (int i = 0; i < bpp; i++) { raw = curr[i + coff] & 0xff; priorRow = prev[i + poff] & 0xff; curr[i + coff] = (byte)(raw + priorRow/2); } for (int i = bpp; i < count; i++) { raw = curr[i + coff] & 0xff; priorPixel = curr[i + coff - bpp] & 0xff; priorRow = prev[i + poff] & 0xff; curr[i + coff] = (byte)(raw + (priorPixel + priorRow)/2); } } private static int paethPredictor(int a, int b, int c) { int p = a + b - c; int pa = Math.abs(p - a); int pb = Math.abs(p - b); int pc = Math.abs(p - c); if ((pa <= pb) && (pa <= pc)) { return a; } else if (pb <= pc) { return b; } else { return c; } } private static void decodePaethFilter(byte[] curr, int coff, byte[] prev, int poff, int count, int bpp) { int raw, priorPixel, priorRow, priorRowPixel; for (int i = 0; i < bpp; i++) { raw = curr[i + coff] & 0xff; priorRow = prev[i + poff] & 0xff; curr[i + coff] = (byte)(raw + priorRow); } for (int i = bpp; i < count; i++) { raw = curr[i + coff] & 0xff; priorPixel = curr[i + coff - bpp] & 0xff; priorRow = prev[i + poff] & 0xff; priorRowPixel = prev[i + poff - bpp] & 0xff; curr[i + coff] = (byte)(raw + paethPredictor(priorPixel, priorRow, priorRowPixel)); } } private static final int[][] bandOffsets = { null, { 0 }, // G { 0, 1 }, // GA in GA order { 0, 1, 2 }, // RGB in RGB order { 0, 1, 2, 3 } // RGBA in RGBA order }; private WritableRaster createRaster(int width, int height, int bands, int scanlineStride, int bitDepth) { DataBuffer dataBuffer; WritableRaster ras = null; Point origin = new Point(0, 0); if ((bitDepth < 8) && (bands == 1)) { dataBuffer = new DataBufferByte(height*scanlineStride); ras = Raster.createPackedRaster(dataBuffer, width, height, bitDepth, origin); } else if (bitDepth <= 8) { dataBuffer = new DataBufferByte(height*scanlineStride); ras = Raster.createInterleavedRaster(dataBuffer, width, height, scanlineStride, bands, bandOffsets[bands], origin); } else { dataBuffer = new DataBufferUShort(height*scanlineStride); ras = Raster.createInterleavedRaster(dataBuffer, width, height, scanlineStride, bands, bandOffsets[bands], origin); } return ras; } private void skipPass(int passWidth, int passHeight) throws IOException, IIOException { if ((passWidth == 0) || (passHeight == 0)) { return; } int inputBands = inputBandsForColorType[metadata.IHDR_colorType]; int bytesPerRow = (inputBands*passWidth*metadata.IHDR_bitDepth + 7)/8; byte[] curr = new byte[bytesPerRow]; // Read the image row-by-row for (int srcY = 0; srcY < passHeight; srcY++) { // Read the filter type byte and a row of data int filter = pixelStream.read(); pixelStream.readFully(curr, 0, bytesPerRow); // If read has been aborted, just return // processReadAborted will be called later if (abortRequested()) { return; } } } // Helper for protected computeUpdatedPixels method private static void computeUpdatedPixels(int sourceOffset, int sourceExtent, int destinationOffset, int dstMin, int dstMax, int sourceSubsampling, int passStart, int passExtent, int passPeriod, int[] vals, int offset) { // We need to satisfy the congruences: // dst = destinationOffset + (src - sourceOffset)/sourceSubsampling // // src - passStart == 0 (mod passPeriod) // src - sourceOffset == 0 (mod sourceSubsampling) // // subject to the inequalities: // // src >= passStart // src < passStart + passExtent // src >= sourceOffset // src < sourceOffset + sourceExtent // dst >= dstMin // dst <= dstmax // // where // // dst = destinationOffset + (src - sourceOffset)/sourceSubsampling // // For now we use a brute-force approach although we could // attempt to analyze the congruences. If passPeriod and // sourceSubsamling are relatively prime, the period will be // their product. If they share a common factor, either the // period will be equal to the larger value, or the sequences // will be completely disjoint, depending on the relationship // between passStart and sourceOffset. Since we only have to do this // twice per image (once each for X and Y), it seems cheap enough // to do it the straightforward way. boolean gotPixel = false; int firstDst = -1; int secondDst = -1; int lastDst = -1; for (int i = 0; i < passExtent; i++) { int src = passStart + i*passPeriod; if (src < sourceOffset) { continue; } if ((src - sourceOffset) % sourceSubsampling != 0) { continue; } if (src >= sourceOffset + sourceExtent) { break; } int dst = destinationOffset + (src - sourceOffset)/sourceSubsampling; if (dst < dstMin) { continue; } if (dst > dstMax) { break; } if (!gotPixel) { firstDst = dst; // Record smallest valid pixel gotPixel = true; } else if (secondDst == -1) { secondDst = dst; // Record second smallest valid pixel } lastDst = dst; // Record largest valid pixel } vals[offset] = firstDst; // If we never saw a valid pixel, set width to 0 if (!gotPixel) { vals[offset + 2] = 0; } else { vals[offset + 2] = lastDst - firstDst + 1; } // The period is given by the difference of any two adjacent pixels vals[offset + 4] = Math.max(secondDst - firstDst, 1); } /** * A utility method that computes the exact set of destination * pixels that will be written during a particular decoding pass. * The intent is to simplify the work done by readers in combining * the source region, source subsampling, and destination offset * information obtained from the ImageReadParam with * the offsets and periods of a progressive or interlaced decoding * pass. * * @param sourceRegion a Rectangle containing the * source region being read, offset by the source subsampling * offsets, and clipped against the source bounds, as returned by * the getSourceRegion method. * @param destinationOffset a Point containing the * coordinates of the upper-left pixel to be written in the * destination. * @param dstMinX the smallest X coordinate (inclusive) of the * destination Raster. * @param dstMinY the smallest Y coordinate (inclusive) of the * destination Raster. * @param dstMaxX the largest X coordinate (inclusive) of the destination * Raster. * @param dstMaxY the largest Y coordinate (inclusive) of the destination * Raster. * @param sourceXSubsampling the X subsampling factor. * @param sourceYSubsampling the Y subsampling factor. * @param passXStart the smallest source X coordinate (inclusive) * of the current progressive pass. * @param passYStart the smallest source Y coordinate (inclusive) * of the current progressive pass. * @param passWidth the width in pixels of the current progressive * pass. * @param passHeight the height in pixels of the current progressive * pass. * @param passPeriodX the X period (horizontal spacing between * pixels) of the current progressive pass. * @param passPeriodY the Y period (vertical spacing between * pixels) of the current progressive pass. * * @return an array of 6 ints containing the * destination min X, min Y, width, height, X period and Y period * of the region that will be updated. */ private static int[] computeUpdatedPixels(Rectangle sourceRegion, Point destinationOffset, int dstMinX, int dstMinY, int dstMaxX, int dstMaxY, int sourceXSubsampling, int sourceYSubsampling, int passXStart, int passYStart, int passWidth, int passHeight, int passPeriodX, int passPeriodY) { int[] vals = new int[6]; computeUpdatedPixels(sourceRegion.x, sourceRegion.width, destinationOffset.x, dstMinX, dstMaxX, sourceXSubsampling, passXStart, passWidth, passPeriodX, vals, 0); computeUpdatedPixels(sourceRegion.y, sourceRegion.height, destinationOffset.y, dstMinY, dstMaxY, sourceYSubsampling, passYStart, passHeight, passPeriodY, vals, 1); return vals; } private void updateImageProgress(int newPixels) { pixelsDone += newPixels; processImageProgress(100.0F*pixelsDone/totalPixels); } private void decodePass(int passNum, int xStart, int yStart, int xStep, int yStep, int passWidth, int passHeight) throws IOException { if ((passWidth == 0) || (passHeight == 0)) { return; } WritableRaster imRas = theImage.getWritableTile(0, 0); int dstMinX = imRas.getMinX(); int dstMaxX = dstMinX + imRas.getWidth() - 1; int dstMinY = imRas.getMinY(); int dstMaxY = dstMinY + imRas.getHeight() - 1; // Determine which pixels will be updated in this pass int[] vals = computeUpdatedPixels(sourceRegion, destinationOffset, dstMinX, dstMinY, dstMaxX, dstMaxY, sourceXSubsampling, sourceYSubsampling, xStart, yStart, passWidth, passHeight, xStep, yStep); int updateMinX = vals[0]; int updateMinY = vals[1]; int updateWidth = vals[2]; int updateXStep = vals[4]; int updateYStep = vals[5]; int bitDepth = metadata.IHDR_bitDepth; int inputBands = inputBandsForColorType[metadata.IHDR_colorType]; int bytesPerPixel = (bitDepth == 16) ? 2 : 1; bytesPerPixel *= inputBands; int bytesPerRow = (inputBands*passWidth*bitDepth + 7)/8; int eltsPerRow = (bitDepth == 16) ? bytesPerRow/2 : bytesPerRow; // If no pixels need updating, just skip the input data if (updateWidth == 0) { for (int srcY = 0; srcY < passHeight; srcY++) { // Update count of pixels read updateImageProgress(passWidth); pixelStream.skipBytes(1 + bytesPerRow); } return; } // Backwards map from destination pixels // (dstX = updateMinX + k*updateXStep) // to source pixels (sourceX), and then // to offset and skip in passRow (srcX and srcXStep) int sourceX = (updateMinX - destinationOffset.x)*sourceXSubsampling + sourceRegion.x; int srcX = (sourceX - xStart)/xStep; // Compute the step factor in the source int srcXStep = updateXStep*sourceXSubsampling/xStep; byte[] byteData = null; short[] shortData = null; byte[] curr = new byte[bytesPerRow]; byte[] prior = new byte[bytesPerRow]; // Create a 1-row tall Raster to hold the data WritableRaster passRow = createRaster(passWidth, 1, inputBands, eltsPerRow, bitDepth); // Create an array suitable for holding one pixel int[] ps = passRow.getPixel(0, 0, (int[])null); DataBuffer dataBuffer = passRow.getDataBuffer(); int type = dataBuffer.getDataType(); if (type == DataBuffer.TYPE_BYTE) { byteData = ((DataBufferByte)dataBuffer).getData(); } else { shortData = ((DataBufferUShort)dataBuffer).getData(); } processPassStarted(theImage, passNum, sourceMinProgressivePass, sourceMaxProgressivePass, updateMinX, updateMinY, updateXStep, updateYStep, destinationBands); // Handle source and destination bands if (sourceBands != null) { passRow = passRow.createWritableChild(0, 0, passRow.getWidth(), 1, 0, 0, sourceBands); } if (destinationBands != null) { imRas = imRas.createWritableChild(0, 0, imRas.getWidth(), imRas.getHeight(), 0, 0, destinationBands); } // Determine if all of the relevant output bands have the // same bit depth as the source data boolean adjustBitDepths = false; int[] outputSampleSize = imRas.getSampleModel().getSampleSize(); int numBands = outputSampleSize.length; for (int b = 0; b < numBands; b++) { if (outputSampleSize[b] != bitDepth) { adjustBitDepths = true; break; } } // If the bit depths differ, create a lookup table per band to perform // the conversion int[][] scale = null; if (adjustBitDepths) { int maxInSample = (1 << bitDepth) - 1; int halfMaxInSample = maxInSample/2; scale = new int[numBands][]; for (int b = 0; b < numBands; b++) { int maxOutSample = (1 << outputSampleSize[b]) - 1; scale[b] = new int[maxInSample + 1]; for (int s = 0; s <= maxInSample; s++) { scale[b][s] = (s*maxOutSample + halfMaxInSample)/maxInSample; } } } // Limit passRow to relevant area for the case where we // will can setRect to copy a contiguous span boolean useSetRect = srcXStep == 1 && updateXStep == 1 && !adjustBitDepths; if (useSetRect) { passRow = passRow.createWritableChild(srcX, 0, updateWidth, 1, 0, 0, null); } // Decode the (sub)image row-by-row for (int srcY = 0; srcY < passHeight; srcY++) { // Update count of pixels read updateImageProgress(passWidth); // Read the filter type byte and a row of data int filter = pixelStream.read(); try { // Swap curr and prior byte[] tmp = prior; prior = curr; curr = tmp; pixelStream.readFully(curr, 0, bytesPerRow); } catch (java.util.zip.ZipException ze) { // TODO - throw a more meaningful exception throw ze; } switch (filter) { case PNG_FILTER_NONE: break; case PNG_FILTER_SUB: decodeSubFilter(curr, 0, bytesPerRow, bytesPerPixel); break; case PNG_FILTER_UP: decodeUpFilter(curr, 0, prior, 0, bytesPerRow); break; case PNG_FILTER_AVERAGE: decodeAverageFilter(curr, 0, prior, 0, bytesPerRow, bytesPerPixel); break; case PNG_FILTER_PAETH: decodePaethFilter(curr, 0, prior, 0, bytesPerRow, bytesPerPixel); break; default: throw new IIOException("Unknown row filter type (= " + filter + ")!"); } // Copy data into passRow byte by byte if (bitDepth < 16) { System.arraycopy(curr, 0, byteData, 0, bytesPerRow); } else { int idx = 0; for (int j = 0; j < eltsPerRow; j++) { shortData[j] = (short)((curr[idx] << 8) | (curr[idx + 1] & 0xff)); idx += 2; } } // True Y position in source int sourceY = srcY*yStep + yStart; if ((sourceY >= sourceRegion.y) && (sourceY < sourceRegion.y + sourceRegion.height) && (((sourceY - sourceRegion.y) % sourceYSubsampling) == 0)) { int dstY = destinationOffset.y + (sourceY - sourceRegion.y)/sourceYSubsampling; if (dstY < dstMinY) { continue; } if (dstY > dstMaxY) { break; } if (useSetRect) { imRas.setRect(updateMinX, dstY, passRow); } else { int newSrcX = srcX; for (int dstX = updateMinX; dstX < updateMinX + updateWidth; dstX += updateXStep) { passRow.getPixel(newSrcX, 0, ps); if (adjustBitDepths) { for (int b = 0; b < numBands; b++) { ps[b] = scale[b][ps[b]]; } } imRas.setPixel(dstX, dstY, ps); newSrcX += srcXStep; } } processImageUpdate(theImage, updateMinX, dstY, updateWidth, 1, updateXStep, updateYStep, destinationBands); // If read has been aborted, just return // processReadAborted will be called later if (abortRequested()) { return; } } } processPassComplete(theImage); } private void decodeImage() throws IOException, IIOException { int width = metadata.IHDR_width; int height = metadata.IHDR_height; this.pixelsDone = 0; this.totalPixels = width*height; clearAbortRequest(); if (metadata.IHDR_interlaceMethod == 0) { decodePass(0, 0, 0, 1, 1, width, height); } else { for (int i = 0; i <= sourceMaxProgressivePass; i++) { int XOffset = adam7XOffset[i]; int YOffset = adam7YOffset[i]; int XSubsampling = adam7XSubsampling[i]; int YSubsampling = adam7YSubsampling[i]; int xbump = adam7XSubsampling[i + 1] - 1; int ybump = adam7YSubsampling[i + 1] - 1; if (i >= sourceMinProgressivePass) { decodePass(i, XOffset, YOffset, XSubsampling, YSubsampling, (width + xbump)/XSubsampling, (height + ybump)/YSubsampling); } else { skipPass((width + xbump)/XSubsampling, (height + ybump)/YSubsampling); } // If read has been aborted, just return // processReadAborted will be called later if (abortRequested()) { return; } } } } private void readImage(ImageReadParam param) throws IIOException { readMetadata(); int width = metadata.IHDR_width; int height = metadata.IHDR_height; // Init default values sourceRegion = getSourceRegion(param, width, height); sourceXSubsampling = 1; sourceYSubsampling = 1; sourceMinProgressivePass = 0; sourceMaxProgressivePass = 6; sourceBands = null; destinationBands = null; destinationOffset = new Point(0, 0); // If an ImageReadParam is available, get values from it if (param != null) { sourceXSubsampling = param.getSourceXSubsampling(); sourceYSubsampling = param.getSourceYSubsampling(); sourceMinProgressivePass = Math.max(param.getSourceMinProgressivePass(), 0); sourceMaxProgressivePass = Math.min(param.getSourceMaxProgressivePass(), 6); sourceBands = param.getSourceBands(); destinationBands = param.getDestinationBands(); destinationOffset = param.getDestinationOffset(); } try { stream.seek(imageStartPosition); Enumeration e = new PNGImageDataEnumeration(stream); InputStream is = new SequenceInputStream(e); is = new InflaterInputStream(is, new Inflater()); is = new BufferedInputStream(is); this.pixelStream = new DataInputStream(is); theImage = getDestination(param, getImageTypes(0), width, height); // At this point the header has been read and we know // how many bands are in the image, so perform checking // of the read param. int colorType = metadata.IHDR_colorType; checkReadParamBandSettings(param, inputBandsForColorType[colorType], theImage.getSampleModel().getNumBands()); processImageStarted(0); decodeImage(); if (abortRequested()) { processReadAborted(); } else { processImageComplete(); } } catch (IOException e) { e.printStackTrace(); throw new IIOException("Error reading PNG image data", e); } } public int getNumImages(boolean allowSearch) throws IIOException { if (stream == null) { throw new IllegalStateException("No input source set!"); } if (seekForwardOnly && allowSearch) { throw new IllegalStateException ("seekForwardOnly and allowSearch can't both be true!"); } return 1; } public int getWidth(int imageIndex) throws IIOException { readHeader(); return metadata.IHDR_width; } public int getHeight(int imageIndex) throws IIOException { readHeader(); return metadata.IHDR_height; } public Iterator getImageTypes(int imageIndex) throws IIOException { if (imageIndex != 0) { throw new IndexOutOfBoundsException("imageIndex != 0!"); } readHeader(); ArrayList l = new ArrayList(1); // List of ImageTypeSpecifiers ColorSpace rgb; ColorSpace gray; int[] bandOffsets; int bitDepth = metadata.IHDR_bitDepth; int colorType = metadata.IHDR_colorType; int dataType; if (bitDepth <= 8) { dataType = DataBuffer.TYPE_BYTE; } else { dataType = DataBuffer.TYPE_USHORT; } switch (colorType) { case PNG_COLOR_GRAY: // Packed grayscale l.add(ImageTypeSpecifier.createGrayscale(bitDepth, dataType, false)); break; case PNG_COLOR_RGB: // Component R, G, B rgb = ColorSpace.getInstance(ColorSpace.CS_sRGB); bandOffsets = new int[3]; bandOffsets[0] = 0; bandOffsets[1] = 1; bandOffsets[2] = 2; l.add(ImageTypeSpecifier.createInterleaved(rgb, bandOffsets, dataType, false, false)); break; case PNG_COLOR_PALETTE: readMetadata(); // Need tRNS chunk // Alpha from tRNS chunk may have fewer entries than // the RGB LUTs from the PLTE chunk; if so, pad with // 255. byte[] alpha = null; if (metadata.tRNS_present && (metadata.tRNS_alpha != null)) { if (metadata.tRNS_alpha.length == metadata.PLTE_red.length) { alpha = metadata.tRNS_alpha; } else { alpha = new byte[metadata.PLTE_red.length]; System.arraycopy(metadata.tRNS_alpha, 0, alpha, 0, metadata.tRNS_alpha.length); Arrays.fill(alpha, metadata.tRNS_alpha.length, metadata.PLTE_red.length, (byte)255); } } l.add(ImageTypeSpecifier.createIndexed(metadata.PLTE_red, metadata.PLTE_green, metadata.PLTE_blue, alpha, bitDepth, DataBuffer.TYPE_BYTE)); break; case PNG_COLOR_GRAY_ALPHA: // Component G, A gray = ColorSpace.getInstance(ColorSpace.CS_GRAY); bandOffsets = new int[2]; bandOffsets[0] = 0; bandOffsets[1] = 1; l.add(ImageTypeSpecifier.createInterleaved(gray, bandOffsets, dataType, true, false)); break; case PNG_COLOR_RGB_ALPHA: // Component R, G, B, A (non-premultiplied) rgb = ColorSpace.getInstance(ColorSpace.CS_sRGB); bandOffsets = new int[4]; bandOffsets[0] = 0; bandOffsets[1] = 1; bandOffsets[2] = 2; bandOffsets[3] = 3; l.add(ImageTypeSpecifier.createInterleaved(rgb, bandOffsets, dataType, true, false)); break; default: break; } return l.iterator(); } public ImageReadParam getDefaultReadParam() { return new ImageReadParam(); } public IIOMetadata getStreamMetadata() throws IIOException { return null; } public IIOMetadata getImageMetadata(int imageIndex) throws IIOException { if (imageIndex != 0) { throw new IndexOutOfBoundsException("imageIndex != 0!"); } readMetadata(); return metadata; } public BufferedImage read(int imageIndex, ImageReadParam param) throws IIOException { if (imageIndex != 0) { throw new IndexOutOfBoundsException("imageIndex != 0!"); } readImage(param); return theImage; } public void reset() { super.reset(); resetStreamSettings(); } private void resetStreamSettings() { gotHeader = false; gotMetadata = false; metadata = null; pixelStream = null; } }