| /* |
| * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| 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.EOFException; |
| 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.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.ReaderUtil; |
| import com.sun.imageio.plugins.common.SubImageInputStream; |
| import java.io.ByteArrayOutputStream; |
| import sun.awt.image.ByteInterleavedRaster; |
| |
| class PNGImageDataEnumeration implements Enumeration<InputStream> { |
| |
| 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 InputStream 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; |
| } |
| } |
| } |
| |
| public class PNGImageReader extends ImageReader { |
| |
| /* |
| * Note: The following chunk type constants are autogenerated. Each |
| * one is derived from the ASCII values of its 4-character name. For |
| * example, IHDR_TYPE is calculated as follows: |
| * ('I' << 24) | ('H' << 16) | ('D' << 8) | 'R' |
| */ |
| |
| // Critical chunks |
| static final int IHDR_TYPE = 0x49484452; |
| static final int PLTE_TYPE = 0x504c5445; |
| static final int IDAT_TYPE = 0x49444154; |
| static final int IEND_TYPE = 0x49454e44; |
| |
| // Ancillary chunks |
| static final int bKGD_TYPE = 0x624b4744; |
| static final int cHRM_TYPE = 0x6348524d; |
| static final int gAMA_TYPE = 0x67414d41; |
| static final int hIST_TYPE = 0x68495354; |
| static final int iCCP_TYPE = 0x69434350; |
| static final int iTXt_TYPE = 0x69545874; |
| static final int pHYs_TYPE = 0x70485973; |
| static final int sBIT_TYPE = 0x73424954; |
| static final int sPLT_TYPE = 0x73504c54; |
| static final int sRGB_TYPE = 0x73524742; |
| static final int tEXt_TYPE = 0x74455874; |
| static final int tIME_TYPE = 0x74494d45; |
| static final int tRNS_TYPE = 0x74524e53; |
| static final int zTXt_TYPE = 0x7a545874; |
| |
| 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(); |
| } |
| |
| private String readNullTerminatedString(String charset, int maxLen) throws IOException { |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| int b; |
| int count = 0; |
| while ((maxLen > count++) && ((b = stream.read()) != 0)) { |
| if (b == -1) throw new EOFException(); |
| baos.write(b); |
| } |
| return new String(baos.toByteArray(), charset); |
| } |
| |
| 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(); |
| |
| // Re-use signature array to bulk-read these unsigned byte values |
| stream.readFully(signature, 0, 5); |
| int bitDepth = signature[0] & 0xff; |
| int colorType = signature[1] & 0xff; |
| int compressionMethod = signature[2] & 0xff; |
| int filterMethod = signature[3] & 0xff; |
| int interlaceMethod = signature[4] & 0xff; |
| |
| // Skip IHDR CRC |
| stream.skipBytes(4); |
| |
| 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(int chunkLength) throws IOException, |
| IIOException |
| { |
| if (!metadata.PLTE_present) { |
| throw new IIOException("hIST chunk without prior PLTE chunk!"); |
| } |
| |
| /* According to PNG specification length of |
| * hIST chunk is specified in bytes and |
| * hIST chunk consists of 2 byte elements |
| * (so we expect length is even). |
| */ |
| metadata.hIST_histogram = new char[chunkLength/2]; |
| 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("ISO-8859-1", 80); |
| 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("ISO-8859-1", 80); |
| metadata.iTXt_keyword.add(keyword); |
| |
| int compressionFlag = stream.readUnsignedByte(); |
| metadata.iTXt_compressionFlag.add(Boolean.valueOf(compressionFlag == 1)); |
| |
| int compressionMethod = stream.readUnsignedByte(); |
| metadata.iTXt_compressionMethod.add(Integer.valueOf(compressionMethod)); |
| |
| String languageTag = readNullTerminatedString("UTF8", 80); |
| metadata.iTXt_languageTag.add(languageTag); |
| |
| long pos = stream.getStreamPosition(); |
| int maxLen = (int)(chunkStart + chunkLength - pos); |
| String translatedKeyword = |
| readNullTerminatedString("UTF8", maxLen); |
| metadata.iTXt_translatedKeyword.add(translatedKeyword); |
| |
| String text; |
| pos = stream.getStreamPosition(); |
| byte[] b = new byte[(int)(chunkStart + chunkLength - pos)]; |
| stream.readFully(b); |
| |
| if (compressionFlag == 1) { // Decompress the text |
| text = new String(inflate(b), "UTF8"); |
| } else { |
| text = new String(b, "UTF8"); |
| } |
| 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("ISO-8859-1", 80); |
| 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("ISO-8859-1", 80); |
| metadata.tEXt_keyword.add(keyword); |
| |
| byte[] b = new byte[chunkLength - keyword.length() - 1]; |
| stream.readFully(b); |
| metadata.tEXt_text.add(new String(b, "ISO-8859-1")); |
| } |
| |
| 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 byte[] inflate(byte[] b) throws IOException { |
| InputStream bais = new ByteArrayInputStream(b); |
| InputStream iis = new InflaterInputStream(bais); |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| |
| int c; |
| try { |
| while ((c = iis.read()) != -1) { |
| baos.write(c); |
| } |
| } finally { |
| iis.close(); |
| } |
| return baos.toByteArray(); |
| } |
| |
| private void parse_zTXt_chunk(int chunkLength) throws IOException { |
| String keyword = readNullTerminatedString("ISO-8859-1", 80); |
| 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(new String(inflate(b), "ISO-8859-1")); |
| } |
| |
| private void readMetadata() throws IIOException { |
| if (gotMetadata) { |
| return; |
| } |
| |
| readHeader(); |
| |
| /* |
| * Optimization: We can skip the remaining metadata if the |
| * ignoreMetadata flag is set, and only if this is not a palette |
| * image (in that case, we need to read the metadata to get the |
| * tRNS chunk, which is needed for the getImageTypes() method). |
| */ |
| int colorType = metadata.IHDR_colorType; |
| if (ignoreMetadata && colorType != PNG_COLOR_PALETTE) { |
| try { |
| while (true) { |
| int chunkLength = stream.readInt(); |
| int chunkType = stream.readInt(); |
| |
| if (chunkType == IDAT_TYPE) { |
| // We've reached the image data |
| stream.skipBytes(-8); |
| imageStartPosition = stream.getStreamPosition(); |
| break; |
| } else { |
| // Skip the chunk plus the 4 CRC bytes that follow |
| stream.skipBytes(chunkLength + 4); |
| } |
| } |
| } catch (IOException e) { |
| throw new IIOException("Error skipping PNG metadata", e); |
| } |
| |
| gotMetadata = true; |
| return; |
| } |
| |
| try { |
| loop: while (true) { |
| int chunkLength = stream.readInt(); |
| int chunkType = stream.readInt(); |
| int chunkCRC; |
| |
| // verify the chunk length |
| if (chunkLength < 0) { |
| throw new IIOException("Invalid chunk lenght " + chunkLength); |
| }; |
| |
| try { |
| stream.mark(); |
| stream.seek(stream.getStreamPosition() + chunkLength); |
| chunkCRC = stream.readInt(); |
| stream.reset(); |
| } catch (IOException e) { |
| throw new IIOException("Invalid chunk length " + chunkLength); |
| } |
| |
| switch (chunkType) { |
| case IDAT_TYPE: |
| // If chunk type is 'IDAT', we've reached the image data. |
| stream.skipBytes(-8); |
| imageStartPosition = stream.getStreamPosition(); |
| break loop; |
| case PLTE_TYPE: |
| parse_PLTE_chunk(chunkLength); |
| break; |
| case bKGD_TYPE: |
| parse_bKGD_chunk(); |
| break; |
| case cHRM_TYPE: |
| parse_cHRM_chunk(); |
| break; |
| case gAMA_TYPE: |
| parse_gAMA_chunk(); |
| break; |
| case hIST_TYPE: |
| parse_hIST_chunk(chunkLength); |
| break; |
| case iCCP_TYPE: |
| parse_iCCP_chunk(chunkLength); |
| break; |
| case iTXt_TYPE: |
| if (ignoreMetadata) { |
| stream.skipBytes(chunkLength); |
| } else { |
| parse_iTXt_chunk(chunkLength); |
| } |
| break; |
| case pHYs_TYPE: |
| parse_pHYs_chunk(); |
| break; |
| case sBIT_TYPE: |
| parse_sBIT_chunk(); |
| break; |
| case sPLT_TYPE: |
| parse_sPLT_chunk(chunkLength); |
| break; |
| case sRGB_TYPE: |
| parse_sRGB_chunk(); |
| break; |
| case tEXt_TYPE: |
| parse_tEXt_chunk(chunkLength); |
| break; |
| case tIME_TYPE: |
| parse_tIME_chunk(); |
| break; |
| case tRNS_TYPE: |
| parse_tRNS_chunk(chunkLength); |
| break; |
| case zTXt_TYPE: |
| if (ignoreMetadata) { |
| stream.skipBytes(chunkLength); |
| } else { |
| parse_zTXt_chunk(chunkLength); |
| } |
| break; |
| default: |
| // Read an unknown chunk |
| byte[] b = new byte[chunkLength]; |
| stream.readFully(b); |
| |
| StringBuilder chunkName = new StringBuilder(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); |
| break; |
| } |
| |
| // double check whether all chunk data were consumed |
| if (chunkCRC != stream.readInt()) { |
| throw new IIOException("Failed to read a chunk of type " + |
| chunkType); |
| } |
| stream.flushBefore(stream.getStreamPosition()); |
| } |
| } catch (IOException e) { |
| 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; |
| |
| // Read the image row-by-row |
| for (int srcY = 0; srcY < passHeight; srcY++) { |
| // Skip filter byte and the remaining row bytes |
| pixelStream.skipBytes(1 + bytesPerRow); |
| |
| // If read has been aborted, just return |
| // processReadAborted will be called later |
| if (abortRequested()) { |
| return; |
| } |
| } |
| } |
| |
| 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 = |
| ReaderUtil.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); |
| // Skip filter byte and the remaining row bytes |
| 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 && |
| (imRas instanceof ByteInterleavedRaster); |
| |
| 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 |
| 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(); |
| } |
| Inflater inf = null; |
| try { |
| stream.seek(imageStartPosition); |
| |
| Enumeration<InputStream> e = new PNGImageDataEnumeration(stream); |
| InputStream is = new SequenceInputStream(e); |
| |
| /* InflaterInputStream uses an Inflater instance which consumes |
| * native (non-GC visible) resources. This is normally implicitly |
| * freed when the stream is closed. However since the |
| * InflaterInputStream wraps a client-supplied input stream, |
| * we cannot close it. |
| * But the app may depend on GC finalization to close the stream. |
| * Therefore to ensure timely freeing of native resources we |
| * explicitly create the Inflater instance and free its resources |
| * when we are done with the InflaterInputStream by calling |
| * inf.end(); |
| */ |
| inf = new Inflater(); |
| is = new InflaterInputStream(is, inf); |
| is = new BufferedInputStream(is); |
| this.pixelStream = new DataInputStream(is); |
| |
| /* |
| * NB: the PNG spec declares that valid range for width |
| * and height is [1, 2^31-1], so here we may fail to allocate |
| * a buffer for destination image due to memory limitation. |
| * |
| * However, the recovery strategy for this case should be |
| * defined on the level of application, so we will not |
| * try to estimate the required amount of the memory and/or |
| * handle OOM in any way. |
| */ |
| theImage = getDestination(param, |
| getImageTypes(0), |
| width, |
| height); |
| |
| Rectangle destRegion = new Rectangle(0, 0, 0, 0); |
| sourceRegion = new Rectangle(0, 0, 0, 0); |
| computeRegions(param, width, height, |
| theImage, |
| sourceRegion, destRegion); |
| destinationOffset.setLocation(destRegion.getLocation()); |
| |
| // 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) { |
| throw new IIOException("Error reading PNG image data", e); |
| } finally { |
| if (inf != null) { |
| inf.end(); |
| } |
| } |
| } |
| |
| 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 { |
| if (imageIndex != 0) { |
| throw new IndexOutOfBoundsException("imageIndex != 0!"); |
| } |
| |
| readHeader(); |
| |
| return metadata.IHDR_width; |
| } |
| |
| public int getHeight(int imageIndex) throws IIOException { |
| if (imageIndex != 0) { |
| throw new IndexOutOfBoundsException("imageIndex != 0!"); |
| } |
| |
| readHeader(); |
| |
| return metadata.IHDR_height; |
| } |
| |
| public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) |
| throws IIOException |
| { |
| if (imageIndex != 0) { |
| throw new IndexOutOfBoundsException("imageIndex != 0!"); |
| } |
| |
| readHeader(); |
| |
| ArrayList<ImageTypeSpecifier> l = |
| new ArrayList<ImageTypeSpecifier>(1); |
| |
| 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: |
| if (bitDepth == 8) { |
| // some standard types of buffered images |
| // which can be used as destination |
| l.add(ImageTypeSpecifier.createFromBufferedImageType( |
| BufferedImage.TYPE_3BYTE_BGR)); |
| |
| l.add(ImageTypeSpecifier.createFromBufferedImageType( |
| BufferedImage.TYPE_INT_RGB)); |
| |
| l.add(ImageTypeSpecifier.createFromBufferedImageType( |
| BufferedImage.TYPE_INT_BGR)); |
| |
| } |
| // 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 |
| |
| /* |
| * The PLTE chunk spec says: |
| * |
| * The number of palette entries must not exceed the range that |
| * can be represented in the image bit depth (for example, 2^4 = 16 |
| * for a bit depth of 4). It is permissible to have fewer entries |
| * than the bit depth would allow. In that case, any out-of-range |
| * pixel value found in the image data is an error. |
| * |
| * http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.PLTE |
| * |
| * Consequently, the case when the palette length is smaller than |
| * 2^bitDepth is legal in the view of PNG spec. |
| * |
| * However the spec of createIndexed() method demands the exact |
| * equality of the palette lengh and number of possible palette |
| * entries (2^bitDepth). |
| * |
| * {@link javax.imageio.ImageTypeSpecifier.html#createIndexed} |
| * |
| * In order to avoid this contradiction we need to extend the |
| * palette arrays to the limit defined by the bitDepth. |
| */ |
| |
| int plength = 1 << bitDepth; |
| |
| byte[] red = metadata.PLTE_red; |
| byte[] green = metadata.PLTE_green; |
| byte[] blue = metadata.PLTE_blue; |
| |
| if (metadata.PLTE_red.length < plength) { |
| red = Arrays.copyOf(metadata.PLTE_red, plength); |
| Arrays.fill(red, metadata.PLTE_red.length, plength, |
| metadata.PLTE_red[metadata.PLTE_red.length - 1]); |
| |
| green = Arrays.copyOf(metadata.PLTE_green, plength); |
| Arrays.fill(green, metadata.PLTE_green.length, plength, |
| metadata.PLTE_green[metadata.PLTE_green.length - 1]); |
| |
| blue = Arrays.copyOf(metadata.PLTE_blue, plength); |
| Arrays.fill(blue, metadata.PLTE_blue.length, plength, |
| metadata.PLTE_blue[metadata.PLTE_blue.length - 1]); |
| |
| } |
| |
| // 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 == red.length) { |
| alpha = metadata.tRNS_alpha; |
| } else { |
| alpha = Arrays.copyOf(metadata.tRNS_alpha, red.length); |
| Arrays.fill(alpha, |
| metadata.tRNS_alpha.length, |
| red.length, (byte)255); |
| } |
| } |
| |
| l.add(ImageTypeSpecifier.createIndexed(red, green, |
| 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: |
| if (bitDepth == 8) { |
| // some standard types of buffered images |
| // wich can be used as destination |
| l.add(ImageTypeSpecifier.createFromBufferedImageType( |
| BufferedImage.TYPE_4BYTE_ABGR)); |
| |
| l.add(ImageTypeSpecifier.createFromBufferedImageType( |
| BufferedImage.TYPE_INT_ARGB)); |
| } |
| |
| // 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(); |
| } |
| |
| /* |
| * Super class implementation uses first element |
| * of image types list as raw image type. |
| * |
| * Also, super implementation uses first element of this list |
| * as default destination type image read param does not specify |
| * anything other. |
| * |
| * However, in case of RGB and RGBA color types, raw image type |
| * produces buffered image of custom type. It causes some |
| * performance degradation of subsequent rendering operations. |
| * |
| * To resolve this contradiction we put standard image types |
| * at the first positions of image types list (to produce standard |
| * images by default) and put raw image type (which is custom) |
| * at the last position of this list. |
| * |
| * After this changes we should override getRawImageType() |
| * to return last element of image types list. |
| */ |
| public ImageTypeSpecifier getRawImageType(int imageIndex) |
| throws IOException { |
| |
| Iterator<ImageTypeSpecifier> types = getImageTypes(imageIndex); |
| ImageTypeSpecifier raw = null; |
| do { |
| raw = types.next(); |
| } while (types.hasNext()); |
| return raw; |
| } |
| |
| 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; |
| } |
| } |