You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1172 lines
38 KiB
1172 lines
38 KiB
/*
|
|
* Copyright (c) 2000, 2005, Oracle and/or its affiliates. All rights reserved.
|
|
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*/
|
|
|
|
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 {
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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); // length, will backpatch
|
|
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 {
|
|
// 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);
|
|
}
|
|
|
|
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 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); // 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);
|
|
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;
|
|
|
|
// 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);
|
|
}
|
|
|
|
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) {
|
|
// 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 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;
|
|
|
|
// 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) 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
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
if (param != null) {
|
|
// 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 alreay be taken care of
|
|
// MODE_EXPLICIT is not allowed
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
|
|
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 {
|
|
// Finish up and inform the listeners we are done
|
|
writeIEND();
|
|
processImageComplete();
|
|
}
|
|
} catch (IOException e) {
|
|
throw new IIOException("I/O error writing PNG file!", e);
|
|
}
|
|
}
|
|
}
|