| /* |
| * Copyright (c) 2000, 2017, 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.Rectangle; |
| import java.awt.image.IndexColorModel; |
| import java.awt.image.Raster; |
| import java.awt.image.WritableRaster; |
| import java.awt.image.RenderedImage; |
| import java.awt.image.SampleModel; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.util.Iterator; |
| import java.util.Locale; |
| import java.util.zip.Deflater; |
| import java.util.zip.DeflaterOutputStream; |
| import javax.imageio.IIOException; |
| import javax.imageio.IIOImage; |
| import javax.imageio.ImageTypeSpecifier; |
| import javax.imageio.ImageWriteParam; |
| import javax.imageio.ImageWriter; |
| import javax.imageio.metadata.IIOMetadata; |
| import javax.imageio.spi.ImageWriterSpi; |
| import javax.imageio.stream.ImageOutputStream; |
| import javax.imageio.stream.ImageOutputStreamImpl; |
| |
| final class CRC { |
| |
| private static final int[] crcTable = new int[256]; |
| private int crc = 0xffffffff; |
| |
| static { |
| // Initialize CRC table |
| for (int n = 0; n < 256; n++) { |
| int c = n; |
| for (int k = 0; k < 8; k++) { |
| if ((c & 1) == 1) { |
| c = 0xedb88320 ^ (c >>> 1); |
| } else { |
| c >>>= 1; |
| } |
| |
| crcTable[n] = c; |
| } |
| } |
| } |
| |
| CRC() {} |
| |
| void reset() { |
| crc = 0xffffffff; |
| } |
| |
| void update(byte[] data, int off, int len) { |
| int c = crc; |
| for (int n = 0; n < len; n++) { |
| c = crcTable[(c ^ data[off + n]) & 0xff] ^ (c >>> 8); |
| } |
| crc = c; |
| } |
| |
| void update(int data) { |
| crc = crcTable[(crc ^ data) & 0xff] ^ (crc >>> 8); |
| } |
| |
| int getValue() { |
| return crc ^ 0xffffffff; |
| } |
| } |
| |
| |
| final class ChunkStream extends ImageOutputStreamImpl { |
| |
| private final ImageOutputStream stream; |
| private final long startPos; |
| private final CRC crc = new CRC(); |
| |
| ChunkStream(int type, ImageOutputStream stream) throws IOException { |
| this.stream = stream; |
| this.startPos = stream.getStreamPosition(); |
| |
| stream.writeInt(-1); // length, will backpatch |
| writeInt(type); |
| } |
| |
| @Override |
| public int read() throws IOException { |
| throw new RuntimeException("Method not available"); |
| } |
| |
| @Override |
| public int read(byte[] b, int off, int len) throws IOException { |
| throw new RuntimeException("Method not available"); |
| } |
| |
| @Override |
| public void write(byte[] b, int off, int len) throws IOException { |
| crc.update(b, off, len); |
| stream.write(b, off, len); |
| } |
| |
| @Override |
| public void write(int b) throws IOException { |
| crc.update(b); |
| stream.write(b); |
| } |
| |
| void finish() throws IOException { |
| // Write CRC |
| stream.writeInt(crc.getValue()); |
| |
| // Write length |
| long pos = stream.getStreamPosition(); |
| stream.seek(startPos); |
| stream.writeInt((int)(pos - startPos) - 12); |
| |
| // Return to end of chunk and flush to minimize buffering |
| stream.seek(pos); |
| stream.flushBefore(pos); |
| } |
| |
| @Override |
| @SuppressWarnings("deprecation") |
| protected void finalize() throws Throwable { |
| // Empty finalizer (for improved performance; no need to call |
| // super.finalize() in this case) |
| } |
| } |
| |
| // Compress output and write as a series of 'IDAT' chunks of |
| // fixed length. |
| final class IDATOutputStream extends ImageOutputStreamImpl { |
| |
| private static final byte[] chunkType = { |
| (byte)'I', (byte)'D', (byte)'A', (byte)'T' |
| }; |
| |
| private final ImageOutputStream stream; |
| private final int chunkLength; |
| private long startPos; |
| private final CRC crc = new CRC(); |
| |
| private final Deflater def; |
| private final byte[] buf = new byte[512]; |
| // reused 1 byte[] array: |
| private final byte[] wbuf1 = new byte[1]; |
| |
| private int bytesRemaining; |
| |
| IDATOutputStream(ImageOutputStream stream, int chunkLength, |
| int deflaterLevel) throws IOException |
| { |
| this.stream = stream; |
| this.chunkLength = chunkLength; |
| this.def = new Deflater(deflaterLevel); |
| |
| startChunk(); |
| } |
| |
| private void startChunk() throws IOException { |
| crc.reset(); |
| this.startPos = stream.getStreamPosition(); |
| stream.writeInt(-1); // length, will backpatch |
| |
| crc.update(chunkType, 0, 4); |
| stream.write(chunkType, 0, 4); |
| |
| this.bytesRemaining = chunkLength; |
| } |
| |
| private void finishChunk() throws IOException { |
| // Write CRC |
| stream.writeInt(crc.getValue()); |
| |
| // Write length |
| long pos = stream.getStreamPosition(); |
| stream.seek(startPos); |
| stream.writeInt((int)(pos - startPos) - 12); |
| |
| // Return to end of chunk and flush to minimize buffering |
| stream.seek(pos); |
| try { |
| stream.flushBefore(pos); |
| } catch (IOException e) { |
| /* |
| * If flushBefore() fails we try to access startPos in finally |
| * block of write_IDAT(). We should update startPos to avoid |
| * IndexOutOfBoundException while seek() is happening. |
| */ |
| this.startPos = stream.getStreamPosition(); |
| throw e; |
| } |
| } |
| |
| @Override |
| public int read() throws IOException { |
| throw new RuntimeException("Method not available"); |
| } |
| |
| @Override |
| public int read(byte[] b, int off, int len) throws IOException { |
| throw new RuntimeException("Method not available"); |
| } |
| |
| @Override |
| public void write(byte[] b, int off, int len) throws IOException { |
| if (len == 0) { |
| return; |
| } |
| |
| if (!def.finished()) { |
| def.setInput(b, off, len); |
| while (!def.needsInput()) { |
| deflate(); |
| } |
| } |
| } |
| |
| void deflate() throws IOException { |
| int len = def.deflate(buf, 0, buf.length); |
| int off = 0; |
| |
| while (len > 0) { |
| if (bytesRemaining == 0) { |
| finishChunk(); |
| startChunk(); |
| } |
| |
| int nbytes = Math.min(len, bytesRemaining); |
| crc.update(buf, off, nbytes); |
| stream.write(buf, off, nbytes); |
| |
| off += nbytes; |
| len -= nbytes; |
| bytesRemaining -= nbytes; |
| } |
| } |
| |
| @Override |
| public void write(int b) throws IOException { |
| wbuf1[0] = (byte)b; |
| write(wbuf1, 0, 1); |
| } |
| |
| void finish() throws IOException { |
| try { |
| if (!def.finished()) { |
| def.finish(); |
| while (!def.finished()) { |
| deflate(); |
| } |
| } |
| finishChunk(); |
| } finally { |
| def.end(); |
| } |
| } |
| |
| @Override |
| @SuppressWarnings("deprecation") |
| protected void finalize() throws Throwable { |
| // Empty finalizer (for improved performance; no need to call |
| // super.finalize() in this case) |
| } |
| } |
| |
| |
| final class PNGImageWriteParam extends ImageWriteParam { |
| |
| /** Default quality level = 0.5 ie medium compression */ |
| private static final float DEFAULT_QUALITY = 0.5f; |
| |
| private static final String[] compressionNames = {"Deflate"}; |
| private static final float[] qualityVals = { 0.00F, 0.30F, 0.75F, 1.00F }; |
| private static final String[] qualityDescs = { |
| "High compression", // 0.00 -> 0.30 |
| "Medium compression", // 0.30 -> 0.75 |
| "Low compression" // 0.75 -> 1.00 |
| }; |
| |
| PNGImageWriteParam(Locale locale) { |
| super(); |
| this.canWriteProgressive = true; |
| this.locale = locale; |
| this.canWriteCompressed = true; |
| this.compressionTypes = compressionNames; |
| this.compressionType = compressionTypes[0]; |
| this.compressionMode = MODE_DEFAULT; |
| this.compressionQuality = DEFAULT_QUALITY; |
| } |
| |
| /** |
| * Removes any previous compression quality setting. |
| * |
| * <p> The default implementation resets the compression quality |
| * to <code>0.5F</code>. |
| * |
| * @exception IllegalStateException if the compression mode is not |
| * <code>MODE_EXPLICIT</code>. |
| */ |
| @Override |
| public void unsetCompression() { |
| super.unsetCompression(); |
| this.compressionType = compressionTypes[0]; |
| this.compressionQuality = DEFAULT_QUALITY; |
| } |
| |
| /** |
| * Returns <code>true</code> since the PNG plug-in only supports |
| * lossless compression. |
| * |
| * @return <code>true</code>. |
| */ |
| @Override |
| public boolean isCompressionLossless() { |
| return true; |
| } |
| |
| @Override |
| public String[] getCompressionQualityDescriptions() { |
| super.getCompressionQualityDescriptions(); |
| return qualityDescs.clone(); |
| } |
| |
| @Override |
| public float[] getCompressionQualityValues() { |
| super.getCompressionQualityValues(); |
| return qualityVals.clone(); |
| } |
| } |
| |
| /** |
| */ |
| public final class PNGImageWriter extends ImageWriter { |
| |
| /** Default compression level = 4 ie medium compression */ |
| private static final int DEFAULT_COMPRESSION_LEVEL = 4; |
| |
| ImageOutputStream stream = null; |
| |
| PNGMetadata metadata = null; |
| |
| // Factors from the ImageWriteParam |
| int sourceXOffset = 0; |
| int sourceYOffset = 0; |
| int sourceWidth = 0; |
| int sourceHeight = 0; |
| int[] sourceBands = null; |
| int periodX = 1; |
| int periodY = 1; |
| |
| int numBands; |
| int bpp; |
| |
| RowFilter rowFilter = new RowFilter(); |
| byte[] prevRow = null; |
| byte[] currRow = null; |
| byte[][] filteredRows = null; |
| |
| // Per-band scaling tables |
| // |
| // After the first call to initializeScaleTables, either scale and scale0 |
| // will be valid, or scaleh and scalel will be valid, but not both. |
| // |
| // The tables will be designed for use with a set of input but depths |
| // given by sampleSize, and an output bit depth given by scalingBitDepth. |
| // |
| int[] sampleSize = null; // Sample size per band, in bits |
| int scalingBitDepth = -1; // Output bit depth of the scaling tables |
| |
| // Tables for 1, 2, 4, or 8 bit output |
| byte[][] scale = null; // 8 bit table |
| byte[] scale0 = null; // equivalent to scale[0] |
| |
| // Tables for 16 bit output |
| byte[][] scaleh = null; // High bytes of output |
| byte[][] scalel = null; // Low bytes of output |
| |
| int totalPixels; // Total number of pixels to be written by write_IDAT |
| int pixelsDone; // Running count of pixels written by write_IDAT |
| |
| public PNGImageWriter(ImageWriterSpi originatingProvider) { |
| super(originatingProvider); |
| } |
| |
| @Override |
| public void setOutput(Object output) { |
| super.setOutput(output); |
| if (output != null) { |
| if (!(output instanceof ImageOutputStream)) { |
| throw new IllegalArgumentException("output not an ImageOutputStream!"); |
| } |
| this.stream = (ImageOutputStream)output; |
| } else { |
| this.stream = null; |
| } |
| } |
| |
| @Override |
| public ImageWriteParam getDefaultWriteParam() { |
| return new PNGImageWriteParam(getLocale()); |
| } |
| |
| @Override |
| public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) { |
| return null; |
| } |
| |
| @Override |
| public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, |
| ImageWriteParam param) { |
| PNGMetadata m = new PNGMetadata(); |
| m.initialize(imageType, imageType.getSampleModel().getNumBands()); |
| return m; |
| } |
| |
| @Override |
| public IIOMetadata convertStreamMetadata(IIOMetadata inData, |
| ImageWriteParam param) { |
| return null; |
| } |
| |
| @Override |
| public IIOMetadata convertImageMetadata(IIOMetadata inData, |
| ImageTypeSpecifier imageType, |
| ImageWriteParam param) { |
| // TODO - deal with imageType |
| if (inData instanceof PNGMetadata) { |
| return (PNGMetadata)((PNGMetadata)inData).clone(); |
| } else { |
| return new PNGMetadata(inData); |
| } |
| } |
| |
| private void write_magic() throws IOException { |
| // Write signature |
| byte[] magic = { (byte)137, 80, 78, 71, 13, 10, 26, 10 }; |
| stream.write(magic); |
| } |
| |
| private void write_IHDR() throws IOException { |
| // Write IHDR chunk |
| ChunkStream cs = new ChunkStream(PNGImageReader.IHDR_TYPE, stream); |
| cs.writeInt(metadata.IHDR_width); |
| cs.writeInt(metadata.IHDR_height); |
| cs.writeByte(metadata.IHDR_bitDepth); |
| cs.writeByte(metadata.IHDR_colorType); |
| if (metadata.IHDR_compressionMethod != 0) { |
| throw new IIOException( |
| "Only compression method 0 is defined in PNG 1.1"); |
| } |
| cs.writeByte(metadata.IHDR_compressionMethod); |
| if (metadata.IHDR_filterMethod != 0) { |
| throw new IIOException( |
| "Only filter method 0 is defined in PNG 1.1"); |
| } |
| cs.writeByte(metadata.IHDR_filterMethod); |
| if (metadata.IHDR_interlaceMethod < 0 || |
| metadata.IHDR_interlaceMethod > 1) { |
| throw new IIOException( |
| "Only interlace methods 0 (node) and 1 (adam7) are defined in PNG 1.1"); |
| } |
| cs.writeByte(metadata.IHDR_interlaceMethod); |
| cs.finish(); |
| } |
| |
| private void write_cHRM() throws IOException { |
| if (metadata.cHRM_present) { |
| ChunkStream cs = new ChunkStream(PNGImageReader.cHRM_TYPE, stream); |
| cs.writeInt(metadata.cHRM_whitePointX); |
| cs.writeInt(metadata.cHRM_whitePointY); |
| cs.writeInt(metadata.cHRM_redX); |
| cs.writeInt(metadata.cHRM_redY); |
| cs.writeInt(metadata.cHRM_greenX); |
| cs.writeInt(metadata.cHRM_greenY); |
| cs.writeInt(metadata.cHRM_blueX); |
| cs.writeInt(metadata.cHRM_blueY); |
| cs.finish(); |
| } |
| } |
| |
| private void write_gAMA() throws IOException { |
| if (metadata.gAMA_present) { |
| ChunkStream cs = new ChunkStream(PNGImageReader.gAMA_TYPE, stream); |
| cs.writeInt(metadata.gAMA_gamma); |
| cs.finish(); |
| } |
| } |
| |
| private void write_iCCP() throws IOException { |
| if (metadata.iCCP_present) { |
| ChunkStream cs = new ChunkStream(PNGImageReader.iCCP_TYPE, stream); |
| cs.writeBytes(metadata.iCCP_profileName); |
| cs.writeByte(0); // null terminator |
| |
| cs.writeByte(metadata.iCCP_compressionMethod); |
| cs.write(metadata.iCCP_compressedProfile); |
| cs.finish(); |
| } |
| } |
| |
| private void write_sBIT() throws IOException { |
| if (metadata.sBIT_present) { |
| ChunkStream cs = new ChunkStream(PNGImageReader.sBIT_TYPE, stream); |
| int colorType = metadata.IHDR_colorType; |
| if (metadata.sBIT_colorType != colorType) { |
| processWarningOccurred(0, |
| "sBIT metadata has wrong color type.\n" + |
| "The chunk will not be written."); |
| return; |
| } |
| |
| if (colorType == PNGImageReader.PNG_COLOR_GRAY || |
| colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) { |
| cs.writeByte(metadata.sBIT_grayBits); |
| } else if (colorType == PNGImageReader.PNG_COLOR_RGB || |
| colorType == PNGImageReader.PNG_COLOR_PALETTE || |
| colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) { |
| cs.writeByte(metadata.sBIT_redBits); |
| cs.writeByte(metadata.sBIT_greenBits); |
| cs.writeByte(metadata.sBIT_blueBits); |
| } |
| |
| if (colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA || |
| colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) { |
| cs.writeByte(metadata.sBIT_alphaBits); |
| } |
| cs.finish(); |
| } |
| } |
| |
| private void write_sRGB() throws IOException { |
| if (metadata.sRGB_present) { |
| ChunkStream cs = new ChunkStream(PNGImageReader.sRGB_TYPE, stream); |
| cs.writeByte(metadata.sRGB_renderingIntent); |
| cs.finish(); |
| } |
| } |
| |
| private void write_PLTE() throws IOException { |
| if (metadata.PLTE_present) { |
| if (metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY || |
| metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) { |
| // PLTE cannot occur in a gray image |
| |
| processWarningOccurred(0, |
| "A PLTE chunk may not appear in a gray or gray alpha image.\n" + |
| "The chunk will not be written"); |
| return; |
| } |
| |
| ChunkStream cs = new ChunkStream(PNGImageReader.PLTE_TYPE, stream); |
| |
| int numEntries = metadata.PLTE_red.length; |
| byte[] palette = new byte[numEntries*3]; |
| int index = 0; |
| for (int i = 0; i < numEntries; i++) { |
| palette[index++] = metadata.PLTE_red[i]; |
| palette[index++] = metadata.PLTE_green[i]; |
| palette[index++] = metadata.PLTE_blue[i]; |
| } |
| |
| cs.write(palette); |
| cs.finish(); |
| } |
| } |
| |
| private void write_hIST() throws IOException, IIOException { |
| if (metadata.hIST_present) { |
| ChunkStream cs = new ChunkStream(PNGImageReader.hIST_TYPE, stream); |
| |
| if (!metadata.PLTE_present) { |
| throw new IIOException("hIST chunk without PLTE chunk!"); |
| } |
| |
| cs.writeChars(metadata.hIST_histogram, |
| 0, metadata.hIST_histogram.length); |
| cs.finish(); |
| } |
| } |
| |
| private void write_tRNS() throws IOException, IIOException { |
| if (metadata.tRNS_present) { |
| ChunkStream cs = new ChunkStream(PNGImageReader.tRNS_TYPE, stream); |
| int colorType = metadata.IHDR_colorType; |
| int chunkType = metadata.tRNS_colorType; |
| |
| // Special case: image is RGB and chunk is Gray |
| // Promote chunk contents to RGB |
| int chunkRed = metadata.tRNS_red; |
| int chunkGreen = metadata.tRNS_green; |
| int chunkBlue = metadata.tRNS_blue; |
| if (colorType == PNGImageReader.PNG_COLOR_RGB && |
| chunkType == PNGImageReader.PNG_COLOR_GRAY) { |
| chunkType = colorType; |
| chunkRed = chunkGreen = chunkBlue = |
| metadata.tRNS_gray; |
| } |
| |
| if (chunkType != colorType) { |
| processWarningOccurred(0, |
| "tRNS metadata has incompatible color type.\n" + |
| "The chunk will not be written."); |
| return; |
| } |
| |
| if (colorType == PNGImageReader.PNG_COLOR_PALETTE) { |
| if (!metadata.PLTE_present) { |
| throw new IIOException("tRNS chunk without PLTE chunk!"); |
| } |
| cs.write(metadata.tRNS_alpha); |
| } else if (colorType == PNGImageReader.PNG_COLOR_GRAY) { |
| cs.writeShort(metadata.tRNS_gray); |
| } else if (colorType == PNGImageReader.PNG_COLOR_RGB) { |
| cs.writeShort(chunkRed); |
| cs.writeShort(chunkGreen); |
| cs.writeShort(chunkBlue); |
| } else { |
| throw new IIOException("tRNS chunk for color type 4 or 6!"); |
| } |
| cs.finish(); |
| } |
| } |
| |
| private void write_bKGD() throws IOException { |
| if (metadata.bKGD_present) { |
| ChunkStream cs = new ChunkStream(PNGImageReader.bKGD_TYPE, stream); |
| int colorType = metadata.IHDR_colorType & 0x3; |
| int chunkType = metadata.bKGD_colorType; |
| |
| // Special case: image is RGB(A) and chunk is Gray |
| // Promote chunk contents to RGB |
| int chunkRed = metadata.bKGD_red; |
| int chunkGreen = metadata.bKGD_red; |
| int chunkBlue = metadata.bKGD_red; |
| if (colorType == PNGImageReader.PNG_COLOR_RGB && |
| chunkType == PNGImageReader.PNG_COLOR_GRAY) { |
| // Make a gray bKGD chunk look like RGB |
| chunkType = colorType; |
| chunkRed = chunkGreen = chunkBlue = |
| metadata.bKGD_gray; |
| } |
| |
| // Ignore status of alpha in colorType |
| if (chunkType != colorType) { |
| processWarningOccurred(0, |
| "bKGD metadata has incompatible color type.\n" + |
| "The chunk will not be written."); |
| return; |
| } |
| |
| if (colorType == PNGImageReader.PNG_COLOR_PALETTE) { |
| cs.writeByte(metadata.bKGD_index); |
| } else if (colorType == PNGImageReader.PNG_COLOR_GRAY || |
| colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) { |
| cs.writeShort(metadata.bKGD_gray); |
| } else { // colorType == PNGImageReader.PNG_COLOR_RGB || |
| // colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA |
| cs.writeShort(chunkRed); |
| cs.writeShort(chunkGreen); |
| cs.writeShort(chunkBlue); |
| } |
| cs.finish(); |
| } |
| } |
| |
| private void write_pHYs() throws IOException { |
| if (metadata.pHYs_present) { |
| ChunkStream cs = new ChunkStream(PNGImageReader.pHYs_TYPE, stream); |
| cs.writeInt(metadata.pHYs_pixelsPerUnitXAxis); |
| cs.writeInt(metadata.pHYs_pixelsPerUnitYAxis); |
| cs.writeByte(metadata.pHYs_unitSpecifier); |
| cs.finish(); |
| } |
| } |
| |
| private void write_sPLT() throws IOException { |
| if (metadata.sPLT_present) { |
| ChunkStream cs = new ChunkStream(PNGImageReader.sPLT_TYPE, stream); |
| |
| cs.writeBytes(metadata.sPLT_paletteName); |
| cs.writeByte(0); // null terminator |
| |
| cs.writeByte(metadata.sPLT_sampleDepth); |
| int numEntries = metadata.sPLT_red.length; |
| |
| if (metadata.sPLT_sampleDepth == 8) { |
| for (int i = 0; i < numEntries; i++) { |
| cs.writeByte(metadata.sPLT_red[i]); |
| cs.writeByte(metadata.sPLT_green[i]); |
| cs.writeByte(metadata.sPLT_blue[i]); |
| cs.writeByte(metadata.sPLT_alpha[i]); |
| cs.writeShort(metadata.sPLT_frequency[i]); |
| } |
| } else { // sampleDepth == 16 |
| for (int i = 0; i < numEntries; i++) { |
| cs.writeShort(metadata.sPLT_red[i]); |
| cs.writeShort(metadata.sPLT_green[i]); |
| cs.writeShort(metadata.sPLT_blue[i]); |
| cs.writeShort(metadata.sPLT_alpha[i]); |
| cs.writeShort(metadata.sPLT_frequency[i]); |
| } |
| } |
| cs.finish(); |
| } |
| } |
| |
| private void write_tIME() throws IOException { |
| if (metadata.tIME_present) { |
| ChunkStream cs = new ChunkStream(PNGImageReader.tIME_TYPE, stream); |
| cs.writeShort(metadata.tIME_year); |
| cs.writeByte(metadata.tIME_month); |
| cs.writeByte(metadata.tIME_day); |
| cs.writeByte(metadata.tIME_hour); |
| cs.writeByte(metadata.tIME_minute); |
| cs.writeByte(metadata.tIME_second); |
| cs.finish(); |
| } |
| } |
| |
| private void write_tEXt() throws IOException { |
| Iterator<String> keywordIter = metadata.tEXt_keyword.iterator(); |
| Iterator<String> textIter = metadata.tEXt_text.iterator(); |
| |
| while (keywordIter.hasNext()) { |
| ChunkStream cs = new ChunkStream(PNGImageReader.tEXt_TYPE, stream); |
| String keyword = keywordIter.next(); |
| cs.writeBytes(keyword); |
| cs.writeByte(0); |
| |
| String text = textIter.next(); |
| cs.writeBytes(text); |
| cs.finish(); |
| } |
| } |
| |
| private byte[] deflate(byte[] b) throws IOException { |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| DeflaterOutputStream dos = new DeflaterOutputStream(baos); |
| dos.write(b); |
| dos.close(); |
| return baos.toByteArray(); |
| } |
| |
| private void write_iTXt() throws IOException { |
| Iterator<String> keywordIter = metadata.iTXt_keyword.iterator(); |
| Iterator<Boolean> flagIter = metadata.iTXt_compressionFlag.iterator(); |
| Iterator<Integer> methodIter = metadata.iTXt_compressionMethod.iterator(); |
| Iterator<String> languageIter = metadata.iTXt_languageTag.iterator(); |
| Iterator<String> translatedKeywordIter = |
| metadata.iTXt_translatedKeyword.iterator(); |
| Iterator<String> textIter = metadata.iTXt_text.iterator(); |
| |
| while (keywordIter.hasNext()) { |
| ChunkStream cs = new ChunkStream(PNGImageReader.iTXt_TYPE, stream); |
| |
| cs.writeBytes(keywordIter.next()); |
| cs.writeByte(0); |
| |
| Boolean compressed = flagIter.next(); |
| cs.writeByte(compressed ? 1 : 0); |
| |
| cs.writeByte(methodIter.next().intValue()); |
| |
| cs.writeBytes(languageIter.next()); |
| cs.writeByte(0); |
| |
| |
| cs.write(translatedKeywordIter.next().getBytes("UTF8")); |
| cs.writeByte(0); |
| |
| String text = textIter.next(); |
| if (compressed) { |
| cs.write(deflate(text.getBytes("UTF8"))); |
| } else { |
| cs.write(text.getBytes("UTF8")); |
| } |
| cs.finish(); |
| } |
| } |
| |
| private void write_zTXt() throws IOException { |
| Iterator<String> keywordIter = metadata.zTXt_keyword.iterator(); |
| Iterator<Integer> methodIter = metadata.zTXt_compressionMethod.iterator(); |
| Iterator<String> textIter = metadata.zTXt_text.iterator(); |
| |
| while (keywordIter.hasNext()) { |
| ChunkStream cs = new ChunkStream(PNGImageReader.zTXt_TYPE, stream); |
| String keyword = keywordIter.next(); |
| cs.writeBytes(keyword); |
| cs.writeByte(0); |
| |
| int compressionMethod = (methodIter.next()).intValue(); |
| cs.writeByte(compressionMethod); |
| |
| String text = textIter.next(); |
| cs.write(deflate(text.getBytes("ISO-8859-1"))); |
| cs.finish(); |
| } |
| } |
| |
| private void writeUnknownChunks() throws IOException { |
| Iterator<String> typeIter = metadata.unknownChunkType.iterator(); |
| Iterator<byte[]> dataIter = metadata.unknownChunkData.iterator(); |
| |
| while (typeIter.hasNext() && dataIter.hasNext()) { |
| String type = typeIter.next(); |
| ChunkStream cs = new ChunkStream(chunkType(type), stream); |
| byte[] data = dataIter.next(); |
| cs.write(data); |
| cs.finish(); |
| } |
| } |
| |
| private 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 void encodePass(ImageOutputStream os, |
| RenderedImage image, |
| int xOffset, int yOffset, |
| int xSkip, int ySkip) throws IOException { |
| int minX = sourceXOffset; |
| int minY = sourceYOffset; |
| int width = sourceWidth; |
| int height = sourceHeight; |
| |
| // Adjust offsets and skips based on source subsampling factors |
| xOffset *= periodX; |
| xSkip *= periodX; |
| yOffset *= periodY; |
| ySkip *= periodY; |
| |
| // Early exit if no data for this pass |
| int hpixels = (width - xOffset + xSkip - 1)/xSkip; |
| int vpixels = (height - yOffset + ySkip - 1)/ySkip; |
| if (hpixels == 0 || vpixels == 0) { |
| return; |
| } |
| |
| // Convert X offset and skip from pixels to samples |
| xOffset *= numBands; |
| xSkip *= numBands; |
| |
| // Create row buffers |
| int samplesPerByte = 8/metadata.IHDR_bitDepth; |
| int numSamples = width*numBands; |
| int[] samples = new int[numSamples]; |
| |
| int bytesPerRow = hpixels*numBands; |
| if (metadata.IHDR_bitDepth < 8) { |
| bytesPerRow = (bytesPerRow + samplesPerByte - 1)/samplesPerByte; |
| } else if (metadata.IHDR_bitDepth == 16) { |
| bytesPerRow *= 2; |
| } |
| |
| IndexColorModel icm_gray_alpha = null; |
| if (metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA && |
| image.getColorModel() instanceof IndexColorModel) |
| { |
| // reserve space for alpha samples |
| bytesPerRow *= 2; |
| |
| // will be used to calculate alpha value for the pixel |
| icm_gray_alpha = (IndexColorModel)image.getColorModel(); |
| } |
| |
| currRow = new byte[bytesPerRow + bpp]; |
| prevRow = new byte[bytesPerRow + bpp]; |
| filteredRows = new byte[5][bytesPerRow + bpp]; |
| |
| int bitDepth = metadata.IHDR_bitDepth; |
| for (int row = minY + yOffset; row < minY + height; row += ySkip) { |
| Rectangle rect = new Rectangle(minX, row, width, 1); |
| Raster ras = image.getData(rect); |
| if (sourceBands != null) { |
| ras = ras.createChild(minX, row, width, 1, minX, row, |
| sourceBands); |
| } |
| |
| ras.getPixels(minX, row, width, 1, samples); |
| |
| if (image.getColorModel().isAlphaPremultiplied()) { |
| WritableRaster wr = ras.createCompatibleWritableRaster(); |
| wr.setPixels(wr.getMinX(), wr.getMinY(), |
| wr.getWidth(), wr.getHeight(), |
| samples); |
| |
| image.getColorModel().coerceData(wr, false); |
| wr.getPixels(wr.getMinX(), wr.getMinY(), |
| wr.getWidth(), wr.getHeight(), |
| samples); |
| } |
| |
| // Reorder palette data if necessary |
| int[] paletteOrder = metadata.PLTE_order; |
| if (paletteOrder != null) { |
| for (int i = 0; i < numSamples; i++) { |
| samples[i] = paletteOrder[samples[i]]; |
| } |
| } |
| |
| int count = bpp; // leave first 'bpp' bytes zero |
| int pos = 0; |
| int tmp = 0; |
| |
| switch (bitDepth) { |
| case 1: case 2: case 4: |
| // Image can only have a single band |
| |
| int mask = samplesPerByte - 1; |
| for (int s = xOffset; s < numSamples; s += xSkip) { |
| byte val = scale0[samples[s]]; |
| tmp = (tmp << bitDepth) | val; |
| |
| if ((pos++ & mask) == mask) { |
| currRow[count++] = (byte)tmp; |
| tmp = 0; |
| pos = 0; |
| } |
| } |
| |
| // Left shift the last byte |
| if ((pos & mask) != 0) { |
| tmp <<= ((8/bitDepth) - pos)*bitDepth; |
| currRow[count++] = (byte)tmp; |
| } |
| break; |
| |
| case 8: |
| if (numBands == 1) { |
| for (int s = xOffset; s < numSamples; s += xSkip) { |
| currRow[count++] = scale0[samples[s]]; |
| if (icm_gray_alpha != null) { |
| currRow[count++] = |
| scale0[icm_gray_alpha.getAlpha(0xff & samples[s])]; |
| } |
| } |
| } else { |
| for (int s = xOffset; s < numSamples; s += xSkip) { |
| for (int b = 0; b < numBands; b++) { |
| currRow[count++] = scale[b][samples[s + b]]; |
| } |
| } |
| } |
| break; |
| |
| case 16: |
| for (int s = xOffset; s < numSamples; s += xSkip) { |
| for (int b = 0; b < numBands; b++) { |
| currRow[count++] = scaleh[b][samples[s + b]]; |
| currRow[count++] = scalel[b][samples[s + b]]; |
| } |
| } |
| break; |
| } |
| |
| // Perform filtering |
| int filterType = rowFilter.filterRow(metadata.IHDR_colorType, |
| currRow, prevRow, |
| filteredRows, |
| bytesPerRow, bpp); |
| |
| os.write(filterType); |
| os.write(filteredRows[filterType], bpp, bytesPerRow); |
| |
| // Swap current and previous rows |
| byte[] swap = currRow; |
| currRow = prevRow; |
| prevRow = swap; |
| |
| pixelsDone += hpixels; |
| processImageProgress(100.0F*pixelsDone/totalPixels); |
| |
| // If write has been aborted, just return; |
| // processWriteAborted will be called later |
| if (abortRequested()) { |
| return; |
| } |
| } |
| } |
| |
| // Use sourceXOffset, etc. |
| private void write_IDAT(RenderedImage image, int deflaterLevel) |
| throws IOException |
| { |
| IDATOutputStream ios = new IDATOutputStream(stream, 32768, |
| deflaterLevel); |
| try { |
| if (metadata.IHDR_interlaceMethod == 1) { |
| for (int i = 0; i < 7; i++) { |
| encodePass(ios, image, |
| PNGImageReader.adam7XOffset[i], |
| PNGImageReader.adam7YOffset[i], |
| PNGImageReader.adam7XSubsampling[i], |
| PNGImageReader.adam7YSubsampling[i]); |
| if (abortRequested()) { |
| break; |
| } |
| } |
| } else { |
| encodePass(ios, image, 0, 0, 1, 1); |
| } |
| } finally { |
| ios.finish(); |
| } |
| } |
| |
| private void writeIEND() throws IOException { |
| ChunkStream cs = new ChunkStream(PNGImageReader.IEND_TYPE, stream); |
| cs.finish(); |
| } |
| |
| // Check two int arrays for value equality, always returns false |
| // if either array is null |
| private boolean equals(int[] s0, int[] s1) { |
| if (s0 == null || s1 == null) { |
| return false; |
| } |
| if (s0.length != s1.length) { |
| return false; |
| } |
| for (int i = 0; i < s0.length; i++) { |
| if (s0[i] != s1[i]) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // Initialize the scale/scale0 or scaleh/scalel arrays to |
| // hold the results of scaling an input value to the desired |
| // output bit depth |
| private void initializeScaleTables(int[] sampleSize) { |
| int bitDepth = metadata.IHDR_bitDepth; |
| |
| // If the existing tables are still valid, just return |
| if (bitDepth == scalingBitDepth && |
| equals(sampleSize, this.sampleSize)) { |
| return; |
| } |
| |
| // Compute new tables |
| this.sampleSize = sampleSize; |
| this.scalingBitDepth = bitDepth; |
| int maxOutSample = (1 << bitDepth) - 1; |
| if (bitDepth <= 8) { |
| scale = new byte[numBands][]; |
| for (int b = 0; b < numBands; b++) { |
| int maxInSample = (1 << sampleSize[b]) - 1; |
| int halfMaxInSample = maxInSample/2; |
| scale[b] = new byte[maxInSample + 1]; |
| for (int s = 0; s <= maxInSample; s++) { |
| scale[b][s] = |
| (byte)((s*maxOutSample + halfMaxInSample)/maxInSample); |
| } |
| } |
| scale0 = scale[0]; |
| scaleh = scalel = null; |
| } else { // bitDepth == 16 |
| // Divide scaling table into high and low bytes |
| scaleh = new byte[numBands][]; |
| scalel = new byte[numBands][]; |
| |
| for (int b = 0; b < numBands; b++) { |
| int maxInSample = (1 << sampleSize[b]) - 1; |
| int halfMaxInSample = maxInSample/2; |
| scaleh[b] = new byte[maxInSample + 1]; |
| scalel[b] = new byte[maxInSample + 1]; |
| for (int s = 0; s <= maxInSample; s++) { |
| int val = (s*maxOutSample + halfMaxInSample)/maxInSample; |
| scaleh[b][s] = (byte)(val >> 8); |
| scalel[b][s] = (byte)(val & 0xff); |
| } |
| } |
| scale = null; |
| scale0 = null; |
| } |
| } |
| |
| @Override |
| public void write(IIOMetadata streamMetadata, |
| IIOImage image, |
| ImageWriteParam param) throws IIOException { |
| if (stream == null) { |
| throw new IllegalStateException("output == null!"); |
| } |
| if (image == null) { |
| throw new IllegalArgumentException("image == null!"); |
| } |
| if (image.hasRaster()) { |
| throw new UnsupportedOperationException("image has a Raster!"); |
| } |
| |
| RenderedImage im = image.getRenderedImage(); |
| SampleModel sampleModel = im.getSampleModel(); |
| this.numBands = sampleModel.getNumBands(); |
| |
| // Set source region and subsampling to default values |
| this.sourceXOffset = im.getMinX(); |
| this.sourceYOffset = im.getMinY(); |
| this.sourceWidth = im.getWidth(); |
| this.sourceHeight = im.getHeight(); |
| this.sourceBands = null; |
| this.periodX = 1; |
| this.periodY = 1; |
| |
| if (param != null) { |
| // Get source region and subsampling factors |
| Rectangle sourceRegion = param.getSourceRegion(); |
| if (sourceRegion != null) { |
| Rectangle imageBounds = new Rectangle(im.getMinX(), |
| im.getMinY(), |
| im.getWidth(), |
| im.getHeight()); |
| // Clip to actual image bounds |
| sourceRegion = sourceRegion.intersection(imageBounds); |
| sourceXOffset = sourceRegion.x; |
| sourceYOffset = sourceRegion.y; |
| sourceWidth = sourceRegion.width; |
| sourceHeight = sourceRegion.height; |
| } |
| |
| // Adjust for subsampling offsets |
| int gridX = param.getSubsamplingXOffset(); |
| int gridY = param.getSubsamplingYOffset(); |
| sourceXOffset += gridX; |
| sourceYOffset += gridY; |
| sourceWidth -= gridX; |
| sourceHeight -= gridY; |
| |
| // Get subsampling factors |
| periodX = param.getSourceXSubsampling(); |
| periodY = param.getSourceYSubsampling(); |
| |
| int[] sBands = param.getSourceBands(); |
| if (sBands != null) { |
| sourceBands = sBands; |
| numBands = sourceBands.length; |
| } |
| } |
| |
| // Compute output dimensions |
| int destWidth = (sourceWidth + periodX - 1)/periodX; |
| int destHeight = (sourceHeight + periodY - 1)/periodY; |
| if (destWidth <= 0 || destHeight <= 0) { |
| throw new IllegalArgumentException("Empty source region!"); |
| } |
| |
| // Compute total number of pixels for progress notification |
| this.totalPixels = destWidth*destHeight; |
| this.pixelsDone = 0; |
| |
| // Create metadata |
| IIOMetadata imd = image.getMetadata(); |
| if (imd != null) { |
| metadata = (PNGMetadata)convertImageMetadata(imd, |
| ImageTypeSpecifier.createFromRenderedImage(im), |
| null); |
| } else { |
| metadata = new PNGMetadata(); |
| } |
| |
| // reset compression level to default: |
| int deflaterLevel = DEFAULT_COMPRESSION_LEVEL; |
| |
| if (param != null) { |
| switch(param.getCompressionMode()) { |
| case ImageWriteParam.MODE_DISABLED: |
| deflaterLevel = Deflater.NO_COMPRESSION; |
| break; |
| case ImageWriteParam.MODE_EXPLICIT: |
| float quality = param.getCompressionQuality(); |
| if (quality >= 0f && quality <= 1f) { |
| deflaterLevel = 9 - Math.round(9f * quality); |
| } |
| break; |
| default: |
| } |
| |
| // Use Adam7 interlacing if set in write param |
| switch (param.getProgressiveMode()) { |
| case ImageWriteParam.MODE_DEFAULT: |
| metadata.IHDR_interlaceMethod = 1; |
| break; |
| case ImageWriteParam.MODE_DISABLED: |
| metadata.IHDR_interlaceMethod = 0; |
| break; |
| // MODE_COPY_FROM_METADATA should already be taken care of |
| // MODE_EXPLICIT is not allowed |
| default: |
| } |
| } |
| |
| // Initialize bitDepth and colorType |
| metadata.initialize(new ImageTypeSpecifier(im), numBands); |
| |
| // Overwrite IHDR width and height values with values from image |
| metadata.IHDR_width = destWidth; |
| metadata.IHDR_height = destHeight; |
| |
| this.bpp = numBands*((metadata.IHDR_bitDepth == 16) ? 2 : 1); |
| |
| // Initialize scaling tables for this image |
| initializeScaleTables(sampleModel.getSampleSize()); |
| |
| clearAbortRequest(); |
| |
| processImageStarted(0); |
| if (abortRequested()) { |
| processWriteAborted(); |
| } else { |
| try { |
| write_magic(); |
| write_IHDR(); |
| |
| write_cHRM(); |
| write_gAMA(); |
| write_iCCP(); |
| write_sBIT(); |
| write_sRGB(); |
| |
| write_PLTE(); |
| |
| write_hIST(); |
| write_tRNS(); |
| write_bKGD(); |
| |
| write_pHYs(); |
| write_sPLT(); |
| write_tIME(); |
| write_tEXt(); |
| write_iTXt(); |
| write_zTXt(); |
| |
| writeUnknownChunks(); |
| |
| write_IDAT(im, deflaterLevel); |
| |
| if (abortRequested()) { |
| processWriteAborted(); |
| } else { |
| // Finish up and inform the listeners we are done |
| writeIEND(); |
| processImageComplete(); |
| } |
| } catch (IOException e) { |
| throw new IIOException("I/O error writing PNG file!", e); |
| } |
| } |
| } |
| } |