|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
|
|
package com.sun.imageio.plugins.png; |
|
|
|
import java.awt.Rectangle; |
|
import java.awt.image.ColorModel; |
|
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.DataOutput; |
|
import java.io.IOException; |
|
import java.io.OutputStream; |
|
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.metadata.IIOMetadata; |
|
import javax.imageio.spi.ImageWriterSpi; |
|
import javax.imageio.stream.ImageOutputStream; |
|
import javax.imageio.stream.ImageOutputStreamImpl; |
|
|
|
class CRC { |
|
|
|
private static int[] crcTable = new int[256]; |
|
private int crc = 0xffffffff; |
|
|
|
static { |
|
|
|
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; |
|
} |
|
} |
|
} |
|
|
|
public CRC() {} |
|
|
|
public void reset() { |
|
crc = 0xffffffff; |
|
} |
|
|
|
public void update(byte[] data, int off, int len) { |
|
for (int n = 0; n < len; n++) { |
|
crc = crcTable[(crc ^ data[off + n]) & 0xff] ^ (crc >>> 8); |
|
} |
|
} |
|
|
|
public void update(int data) { |
|
crc = crcTable[(crc ^ data) & 0xff] ^ (crc >>> 8); |
|
} |
|
|
|
public int getValue() { |
|
return crc ^ 0xffffffff; |
|
} |
|
} |
|
|
|
|
|
final class ChunkStream extends ImageOutputStreamImpl { |
|
|
|
private ImageOutputStream stream; |
|
private long startPos; |
|
private CRC crc = new CRC(); |
|
|
|
public ChunkStream(int type, ImageOutputStream stream) throws IOException { |
|
this.stream = stream; |
|
this.startPos = stream.getStreamPosition(); |
|
|
|
stream.writeInt(-1); |
|
writeInt(type); |
|
} |
|
|
|
public int read() throws IOException { |
|
throw new RuntimeException("Method not available"); |
|
} |
|
|
|
public int read(byte[] b, int off, int len) throws IOException { |
|
throw new RuntimeException("Method not available"); |
|
} |
|
|
|
public void write(byte[] b, int off, int len) throws IOException { |
|
crc.update(b, off, len); |
|
stream.write(b, off, len); |
|
} |
|
|
|
public void write(int b) throws IOException { |
|
crc.update(b); |
|
stream.write(b); |
|
} |
|
|
|
public void finish() throws IOException { |
|
|
|
stream.writeInt(crc.getValue()); |
|
|
|
|
|
long pos = stream.getStreamPosition(); |
|
stream.seek(startPos); |
|
stream.writeInt((int)(pos - startPos) - 12); |
|
|
|
|
|
stream.seek(pos); |
|
stream.flushBefore(pos); |
|
} |
|
|
|
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 |
|
|
|
final class IDATOutputStream extends ImageOutputStreamImpl { |
|
|
|
private static byte[] chunkType = { |
|
(byte)'I', (byte)'D', (byte)'A', (byte)'T' |
|
}; |
|
|
|
private ImageOutputStream stream; |
|
private int chunkLength; |
|
private long startPos; |
|
private CRC crc = new CRC(); |
|
|
|
Deflater def = new Deflater(Deflater.BEST_COMPRESSION); |
|
byte[] buf = new byte[512]; |
|
|
|
private int bytesRemaining; |
|
|
|
public IDATOutputStream(ImageOutputStream stream, int chunkLength) |
|
throws IOException { |
|
this.stream = stream; |
|
this.chunkLength = chunkLength; |
|
startChunk(); |
|
} |
|
|
|
private void startChunk() throws IOException { |
|
crc.reset(); |
|
this.startPos = stream.getStreamPosition(); |
|
stream.writeInt(-1); |
|
|
|
crc.update(chunkType, 0, 4); |
|
stream.write(chunkType, 0, 4); |
|
|
|
this.bytesRemaining = chunkLength; |
|
} |
|
|
|
private void finishChunk() throws IOException { |
|
|
|
stream.writeInt(crc.getValue()); |
|
|
|
|
|
long pos = stream.getStreamPosition(); |
|
stream.seek(startPos); |
|
stream.writeInt((int)(pos - startPos) - 12); |
|
|
|
|
|
stream.seek(pos); |
|
stream.flushBefore(pos); |
|
} |
|
|
|
public int read() throws IOException { |
|
throw new RuntimeException("Method not available"); |
|
} |
|
|
|
public int read(byte[] b, int off, int len) throws IOException { |
|
throw new RuntimeException("Method not available"); |
|
} |
|
|
|
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(); |
|
} |
|
} |
|
} |
|
|
|
public 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; |
|
} |
|
} |
|
|
|
public void write(int b) throws IOException { |
|
byte[] wbuf = new byte[1]; |
|
wbuf[0] = (byte)b; |
|
write(wbuf, 0, 1); |
|
} |
|
|
|
public void finish() throws IOException { |
|
try { |
|
if (!def.finished()) { |
|
def.finish(); |
|
while (!def.finished()) { |
|
deflate(); |
|
} |
|
} |
|
finishChunk(); |
|
} finally { |
|
def.end(); |
|
} |
|
} |
|
|
|
protected void finalize() throws Throwable { |
|
// Empty finalizer (for improved performance; no need to call |
|
// super.finalize() in this case) |
|
} |
|
} |
|
|
|
|
|
class PNGImageWriteParam extends ImageWriteParam { |
|
|
|
public PNGImageWriteParam(Locale locale) { |
|
super(); |
|
this.canWriteProgressive = true; |
|
this.locale = locale; |
|
} |
|
} |
|
|
|
|
|
*/ |
|
public class PNGImageWriter extends ImageWriter { |
|
|
|
ImageOutputStream stream = null; |
|
|
|
PNGMetadata metadata = null; |
|
|
|
|
|
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; |
|
int scalingBitDepth = -1; |
|
|
|
// Tables for 1, 2, 4, or 8 bit output |
|
byte[][] scale = null; |
|
byte[] scale0 = null; |
|
|
|
// Tables for 16 bit output |
|
byte[][] scaleh = null; |
|
byte[][] scalel = null; |
|
|
|
int totalPixels; |
|
int pixelsDone; |
|
|
|
public PNGImageWriter(ImageWriterSpi originatingProvider) { |
|
super(originatingProvider); |
|
} |
|
|
|
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; |
|
} |
|
} |
|
|
|
private static int[] allowedProgressivePasses = { 1, 7 }; |
|
|
|
public ImageWriteParam getDefaultWriteParam() { |
|
return new PNGImageWriteParam(getLocale()); |
|
} |
|
|
|
public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) { |
|
return null; |
|
} |
|
|
|
public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, |
|
ImageWriteParam param) { |
|
PNGMetadata m = new PNGMetadata(); |
|
m.initialize(imageType, imageType.getSampleModel().getNumBands()); |
|
return m; |
|
} |
|
|
|
public IIOMetadata convertStreamMetadata(IIOMetadata inData, |
|
ImageWriteParam param) { |
|
return null; |
|
} |
|
|
|
public IIOMetadata convertImageMetadata(IIOMetadata inData, |
|
ImageTypeSpecifier imageType, |
|
ImageWriteParam param) { |
|
|
|
if (inData instanceof PNGMetadata) { |
|
return (PNGMetadata)((PNGMetadata)inData).clone(); |
|
} else { |
|
return new PNGMetadata(inData); |
|
} |
|
} |
|
|
|
private void write_magic() throws IOException { |
|
|
|
byte[] magic = { (byte)137, 80, 78, 71, 13, 10, 26, 10 }; |
|
stream.write(magic); |
|
} |
|
|
|
private void write_IHDR() throws IOException { |
|
|
|
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); |
|
|
|
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 |
|
|
|
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 |
|
|
|
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) { |
|
|
|
chunkType = colorType; |
|
chunkRed = chunkGreen = chunkBlue = |
|
metadata.bKGD_gray; |
|
} |
|
|
|
|
|
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 || |
|
|
|
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); |
|
|
|
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 { |
|
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 keywordIter = metadata.tEXt_keyword.iterator(); |
|
Iterator textIter = metadata.tEXt_text.iterator(); |
|
|
|
while (keywordIter.hasNext()) { |
|
ChunkStream cs = new ChunkStream(PNGImageReader.tEXt_TYPE, stream); |
|
String keyword = (String)keywordIter.next(); |
|
cs.writeBytes(keyword); |
|
cs.writeByte(0); |
|
|
|
String text = (String)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 keywordIter = metadata.zTXt_keyword.iterator(); |
|
Iterator methodIter = metadata.zTXt_compressionMethod.iterator(); |
|
Iterator textIter = metadata.zTXt_text.iterator(); |
|
|
|
while (keywordIter.hasNext()) { |
|
ChunkStream cs = new ChunkStream(PNGImageReader.zTXt_TYPE, stream); |
|
String keyword = (String)keywordIter.next(); |
|
cs.writeBytes(keyword); |
|
cs.writeByte(0); |
|
|
|
int compressionMethod = ((Integer)methodIter.next()).intValue(); |
|
cs.writeByte(compressionMethod); |
|
|
|
String text = (String)textIter.next(); |
|
cs.write(deflate(text.getBytes("ISO-8859-1"))); |
|
cs.finish(); |
|
} |
|
} |
|
|
|
private void writeUnknownChunks() throws IOException { |
|
Iterator typeIter = metadata.unknownChunkType.iterator(); |
|
Iterator dataIter = metadata.unknownChunkData.iterator(); |
|
|
|
while (typeIter.hasNext() && dataIter.hasNext()) { |
|
String type = (String)typeIter.next(); |
|
ChunkStream cs = new ChunkStream(chunkType(type), stream); |
|
byte[] data = (byte[])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; |
|
|
|
|
|
xOffset *= periodX; |
|
xSkip *= periodX; |
|
yOffset *= periodY; |
|
ySkip *= periodY; |
|
|
|
|
|
int hpixels = (width - xOffset + xSkip - 1)/xSkip; |
|
int vpixels = (height - yOffset + ySkip - 1)/ySkip; |
|
if (hpixels == 0 || vpixels == 0) { |
|
return; |
|
} |
|
|
|
|
|
xOffset *= numBands; |
|
xSkip *= numBands; |
|
|
|
|
|
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) |
|
{ |
|
|
|
bytesPerRow *= 2; |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
int[] paletteOrder = metadata.PLTE_order; |
|
if (paletteOrder != null) { |
|
for (int i = 0; i < numSamples; i++) { |
|
samples[i] = paletteOrder[samples[i]]; |
|
} |
|
} |
|
|
|
int count = bpp; |
|
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; |
|
} |
|
} |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
int filterType = rowFilter.filterRow(metadata.IHDR_colorType, |
|
currRow, prevRow, |
|
filteredRows, |
|
bytesPerRow, bpp); |
|
|
|
os.write(filterType); |
|
os.write(filteredRows[filterType], bpp, bytesPerRow); |
|
|
|
|
|
byte[] swap = currRow; |
|
currRow = prevRow; |
|
prevRow = swap; |
|
|
|
pixelsDone += hpixels; |
|
processImageProgress(100.0F*pixelsDone/totalPixels); |
|
|
|
// If write has been aborted, just return; |
|
|
|
if (abortRequested()) { |
|
return; |
|
} |
|
} |
|
} |
|
|
|
|
|
private void write_IDAT(RenderedImage image) throws IOException { |
|
IDATOutputStream ios = new IDATOutputStream(stream, 32768); |
|
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 |
|
|
|
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 |
|
|
|
private void initializeScaleTables(int[] sampleSize) { |
|
int bitDepth = metadata.IHDR_bitDepth; |
|
|
|
|
|
if (bitDepth == scalingBitDepth && |
|
equals(sampleSize, this.sampleSize)) { |
|
return; |
|
} |
|
|
|
|
|
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 |
|
|
|
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; |
|
} |
|
} |
|
|
|
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(); |
|
|
|
|
|
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) { |
|
|
|
Rectangle sourceRegion = param.getSourceRegion(); |
|
if (sourceRegion != null) { |
|
Rectangle imageBounds = new Rectangle(im.getMinX(), |
|
im.getMinY(), |
|
im.getWidth(), |
|
im.getHeight()); |
|
|
|
sourceRegion = sourceRegion.intersection(imageBounds); |
|
sourceXOffset = sourceRegion.x; |
|
sourceYOffset = sourceRegion.y; |
|
sourceWidth = sourceRegion.width; |
|
sourceHeight = sourceRegion.height; |
|
} |
|
|
|
|
|
int gridX = param.getSubsamplingXOffset(); |
|
int gridY = param.getSubsamplingYOffset(); |
|
sourceXOffset += gridX; |
|
sourceYOffset += gridY; |
|
sourceWidth -= gridX; |
|
sourceHeight -= gridY; |
|
|
|
|
|
periodX = param.getSourceXSubsampling(); |
|
periodY = param.getSourceYSubsampling(); |
|
|
|
int[] sBands = param.getSourceBands(); |
|
if (sBands != null) { |
|
sourceBands = sBands; |
|
numBands = sourceBands.length; |
|
} |
|
} |
|
|
|
|
|
int destWidth = (sourceWidth + periodX - 1)/periodX; |
|
int destHeight = (sourceHeight + periodY - 1)/periodY; |
|
if (destWidth <= 0 || destHeight <= 0) { |
|
throw new IllegalArgumentException("Empty source region!"); |
|
} |
|
|
|
|
|
this.totalPixels = destWidth*destHeight; |
|
this.pixelsDone = 0; |
|
|
|
|
|
IIOMetadata imd = image.getMetadata(); |
|
if (imd != null) { |
|
metadata = (PNGMetadata)convertImageMetadata(imd, |
|
ImageTypeSpecifier.createFromRenderedImage(im), |
|
null); |
|
} else { |
|
metadata = new PNGMetadata(); |
|
} |
|
|
|
if (param != null) { |
|
|
|
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 alreay be taken care of |
|
// MODE_EXPLICIT is not allowed |
|
} |
|
} |
|
|
|
|
|
metadata.initialize(new ImageTypeSpecifier(im), numBands); |
|
|
|
|
|
metadata.IHDR_width = destWidth; |
|
metadata.IHDR_height = destHeight; |
|
|
|
this.bpp = numBands*((metadata.IHDR_bitDepth == 16) ? 2 : 1); |
|
|
|
|
|
initializeScaleTables(sampleModel.getSampleSize()); |
|
|
|
clearAbortRequest(); |
|
|
|
processImageStarted(0); |
|
|
|
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); |
|
|
|
if (abortRequested()) { |
|
processWriteAborted(); |
|
} else { |
|
|
|
writeIEND(); |
|
processImageComplete(); |
|
} |
|
} catch (IOException e) { |
|
throw new IIOException("I/O error writing PNG file!", e); |
|
} |
|
} |
|
} |