TMI-26: TIFF write support sans LZW.

This commit is contained in:
Harald Kuhr
2015-03-18 21:46:04 +01:00
parent 824613b4f1
commit 1505aa651b
13 changed files with 2663 additions and 50 deletions
@@ -0,0 +1,302 @@
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.lang.Validate;
import java.io.EOFException;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
/**
* A decoder for data converted using "horizontal differencing predictor".
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: HorizontalDeDifferencingStream.java,v 1.0 11.03.13 14:20 haraldk Exp$
*/
final class HorizontalDifferencingStream extends OutputStream {
// See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
private final int columns;
// NOTE: PlanarConfiguration == 2 may be treated as samplesPerPixel == 1
private final int samplesPerPixel;
private final int bitsPerSample;
private final WritableByteChannel channel;
private final ByteBuffer buffer;
public HorizontalDifferencingStream(final OutputStream stream, final int columns, final int samplesPerPixel, final int bitsPerSample, final ByteOrder byteOrder) {
this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0");
this.samplesPerPixel = Validate.isTrue(bitsPerSample >= 8 || samplesPerPixel == 1, samplesPerPixel, "Unsupported samples per pixel for < 8 bit samples: %s");
this.bitsPerSample = Validate.isTrue(isValidBPS(bitsPerSample), bitsPerSample, "Unsupported bits per sample value: %s");
channel = Channels.newChannel(Validate.notNull(stream, "stream"));
buffer = ByteBuffer.allocate((columns * samplesPerPixel * bitsPerSample + 7) / 8).order(byteOrder);
}
private boolean isValidBPS(final int bitsPerSample) {
switch (bitsPerSample) {
case 1:
case 2:
case 4:
case 8:
case 16:
case 32:
case 64:
return true;
default:
return false;
}
}
private boolean flushBuffer() throws IOException {
if (buffer.position() == 0) {
return false;
}
encodeRow();
buffer.flip();
channel.write(buffer);
buffer.clear();
return true;
}
private void encodeRow() throws EOFException {
// Apply horizontal predictor
byte original;
int sample = 0;
int prev;
byte temp;
// Optimization:
// Access array directly for <= 8 bits per sample, as buffer does extra index bounds check for every
// put/get operation... (Measures to about 100 ms difference for 4000 x 3000 image)
final byte[] array = buffer.array();
switch (bitsPerSample) {
case 1:
for (int b = ((columns + 7) / 8) - 1; b > 0; b--) {
// Subtract previous sample from current sample
original = array[b];
prev = array[b - 1] & 0x1;
temp = (byte) ((((original & 0x80) >> 7) - prev) << 7);
sample = ((original & 0x40) >> 6) - ((original & 0x80) >> 7);
temp |= (sample << 6) & 0x40;
sample = ((original & 0x20) >> 5) - ((original & 0x40) >> 6);
temp |= (sample << 5) & 0x20;
sample = ((original & 0x10) >> 4) - ((original & 0x20) >> 5);
temp |= (sample << 4) & 0x10;
sample = ((original & 0x08) >> 3) - ((original & 0x10) >> 4);
temp |= (sample << 3) & 0x08;
sample = ((original & 0x04) >> 2) - ((original & 0x08) >> 3);
temp |= (sample << 2) & 0x04;
sample = ((original & 0x02) >> 1) - ((original & 0x04) >> 2);
temp |= (sample << 1) & 0x02;
sample = (original & 0x01) - ((original & 0x02) >> 1);
array[b] = (byte) (temp & 0xfe | sample & 0x01);
}
// First sample in row as is
original = array[0];
temp = (byte) (original & 0x80);
sample = ((original & 0x40) >> 6) - ((original & 0x80) >> 7);
temp |= (sample << 6) & 0x40;
sample = ((original & 0x20) >> 5) - ((original & 0x40) >> 6);
temp |= (sample << 5) & 0x20;
sample = ((original & 0x10) >> 4) - ((original & 0x20) >> 5);
temp |= (sample << 4) & 0x10;
sample = ((original & 0x08) >> 3) - ((original & 0x10) >> 4);
temp |= (sample << 3) & 0x08;
sample = ((original & 0x04) >> 2) - ((original & 0x08) >> 3);
temp |= (sample << 2) & 0x04;
sample = ((original & 0x02) >> 1) - ((original & 0x04) >> 2);
temp |= (sample << 1) & 0x02;
sample = (original & 0x01) - ((original & 0x02) >> 1);
array[0] = (byte) (temp & 0xfe | sample & 0x01);
break;
case 2:
for (int b = ((columns + 3) / 4) - 1; b > 0; b--) {
// Subtract previous sample from current sample
original = array[b];
prev = array[b - 1] & 0x3;
temp = (byte) ((((original & 0xc0) >> 6) - prev) << 6);
sample = ((original & 0x30) >> 4) - ((original & 0xc0) >> 6);
temp |= (sample << 4) & 0x30;
sample = ((original & 0x0c) >> 2) - ((original & 0x30) >> 4);
temp |= (sample << 2) & 0x0c;
sample = (original & 0x03) - ((original & 0x0c) >> 2);
array[b] = (byte) (temp & 0xfc | sample & 0x03);
}
// First sample in row as is
original = array[0];
temp = (byte) (original & 0xc0);
sample = ((original & 0x30) >> 4) - ((original & 0xc0) >> 6);
temp |= (sample << 4) & 0x30;
sample = ((original & 0x0c) >> 2) - ((original & 0x30) >> 4);
temp |= (sample << 2) & 0x0c;
sample = (original & 0x03) - ((original & 0x0c) >> 2);
array[0] = (byte) (temp & 0xfc | sample & 0x03);
break;
case 4:
for (int b = ((columns + 1) / 2) - 1; b > 0; b--) {
// Subtract previous sample from current sample
original = array[b];
prev = array[b - 1] & 0xf;
temp = (byte) ((((original & 0xf0) >> 4) - prev) << 4);
sample = (original & 0x0f) - ((original & 0xf0) >> 4);
array[b] = (byte) (temp & 0xf0 | sample & 0xf);
}
// First sample in row as is
original = array[0];
sample = (original & 0x0f) - ((original & 0xf0) >> 4);
array[0] = (byte) (original & 0xf0 | sample & 0xf);
break;
case 8:
for (int x = columns - 1; x > 0; x--) {
final int xOff = x * samplesPerPixel;
for (int b = 0; b < samplesPerPixel; b++) {
int off = xOff + b;
array[off] = (byte) (array[off] - array[off - samplesPerPixel]);
}
}
break;
case 16:
for (int x = columns - 1; x > 0; x--) {
for (int b = 0; b < samplesPerPixel; b++) {
int off = x * samplesPerPixel + b;
buffer.putShort(2 * off, (short) (buffer.getShort(2 * off) - buffer.getShort(2 * (off - samplesPerPixel))));
}
}
break;
case 32:
for (int x = columns - 1; x > 0; x--) {
for (int b = 0; b < samplesPerPixel; b++) {
int off = x * samplesPerPixel + b;
buffer.putInt(4 * off, buffer.getInt(4 * off) - buffer.getInt(4 * (off - samplesPerPixel)));
}
}
break;
case 64:
for (int x = columns - 1; x > 0; x--) {
for (int b = 0; b < samplesPerPixel; b++) {
int off = x * samplesPerPixel + b;
buffer.putLong(8 * off, buffer.getLong(8 * off) - buffer.getLong(8 * (off - samplesPerPixel)));
}
}
break;
default:
throw new AssertionError(String.format("Unsupported bits per sample value: %d", bitsPerSample));
}
}
@Override
public void write(int b) throws IOException {
buffer.put((byte) b);
if (!buffer.hasRemaining()) {
flushBuffer();
}
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
while (len > 0) {
int maxLenForRow = Math.min(len, buffer.remaining());
buffer.put(b, off, maxLenForRow);
off += maxLenForRow;
len -= maxLenForRow;
if (!buffer.hasRemaining()) {
flushBuffer();
}
}
}
@Override
public void flush() throws IOException {
flushBuffer();
}
@Override
public void close() throws IOException {
try {
flushBuffer();
super.close();
}
finally {
if (channel.isOpen()) {
channel.close();
}
}
}
}
@@ -300,8 +300,8 @@ abstract class LZWDecoder implements Decoder {
this.previous = previous;
}
public final LZWString concatenate(final byte firstChar) {
return new LZWString(firstChar, this.firstChar, length + 1, this);
public final LZWString concatenate(final byte value) {
return new LZWString(value, this.firstChar, length + 1, this);
}
public final void writeTo(final ByteBuffer buffer) {
@@ -58,15 +58,14 @@ public class TIFFImageReaderSpi extends ImageReaderSpi {
super(
providerInfo.getVendorName(),
providerInfo.getVersion(),
new String[]{"tiff", "TIFF"},
new String[]{"tif", "tiff"},
new String[]{
new String[] {"tiff", "TIFF"},
new String[] {"tif", "tiff"},
new String[] {
"image/tiff", "image/x-tiff"
},
"com.twelvemkonkeys.imageio.plugins.tiff.TIFFImageReader",
new Class[] {ImageInputStream.class},
// new String[]{"com.twelvemkonkeys.imageio.plugins.tif.TIFFImageWriterSpi"},
null,
new String[] {"com.twelvemkonkeys.imageio.plugins.tif.TIFFImageWriterSpi"},
true, // supports standard stream metadata
null, null, // native stream format name and class
null, null, // extra stream formats
@@ -0,0 +1,108 @@
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import javax.imageio.ImageWriteParam;
import java.util.Locale;
/**
* TIFFImageWriteParam
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: TIFFImageWriteParam.java,v 1.0 18.09.13 12:47 haraldk Exp$
*/
public final class TIFFImageWriteParam extends ImageWriteParam {
// TODO: Support no compression (None/1)
// TODO: Support ZLIB (/Deflate) compression (8)
// TODO: Support PackBits compression (32773)
// TODO: Support JPEG compression (7)
// TODO: Support CCITT Modified Huffman compression (2)
// TODO: Support LZW compression (5)?
// TODO: Support JBIG compression via ImageIO plugin/delegate?
// TODO: Support JPEG2000 compression via ImageIO plugin/delegate?
// TODO: Support tiling
// TODO: Support predictor. See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
TIFFImageWriteParam() {
this(Locale.getDefault());
}
TIFFImageWriteParam(final Locale locale) {
super(locale);
// NOTE: We use the same spelling/casing as the JAI equivalent to be as compatible as possible
// See: http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/TIFFImageWriteParam.html
compressionTypes = new String[] {"None", /* "CCITT RLE", "CCITT T.4", "CCITT T.6", */ "LZW", "JPEG", "ZLib", "PackBits", "Deflate", /* "EXIF JPEG" */ };
compressionType = compressionTypes[0];
canWriteCompressed = true;
}
@Override
public float[] getCompressionQualityValues() {
super.getCompressionQualityValues();
// TODO: Special case for JPEG and ZLib/Deflate
return null;
}
@Override
public String[] getCompressionQualityDescriptions() {
super.getCompressionQualityDescriptions();
// TODO: Special case for JPEG and ZLib/Deflate
return null;
}
static int getCompressionType(final ImageWriteParam param) {
// TODO: Support mode COPY_FROM_METADATA (when we have metadata...)
if (param == null || param.getCompressionMode() != MODE_EXPLICIT || param.getCompressionType().equals("None")) {
return TIFFBaseline.COMPRESSION_NONE;
}
else if (param.getCompressionType().equals("PackBits")) {
return TIFFBaseline.COMPRESSION_PACKBITS;
}
else if (param.getCompressionType().equals("ZLib")) {
return TIFFExtension.COMPRESSION_ZLIB;
}
else if (param.getCompressionType().equals("Deflate")) {
return TIFFExtension.COMPRESSION_DEFLATE;
}
else if (param.getCompressionType().equals("LZW")) {
return TIFFExtension.COMPRESSION_LZW;
}
else if (param.getCompressionType().equals("JPEG")) {
return TIFFExtension.COMPRESSION_JPEG;
}
throw new IllegalArgumentException(String.format("Unsupported compression type: %s", param.getCompressionType()));
}
}
@@ -0,0 +1,734 @@
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.imageio.ImageWriterBase;
import com.twelvemonkeys.imageio.metadata.AbstractEntry;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.exif.EXIFWriter;
import com.twelvemonkeys.imageio.metadata.exif.Rational;
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.io.enc.EncoderStream;
import com.twelvemonkeys.io.enc.PackBitsEncoder;
import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
/**
* TIFFImageWriter
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: TIFFImageWriter.java,v 1.0 18.09.13 12:46 haraldk Exp$
*/
public final class TIFFImageWriter extends ImageWriterBase {
// Short term
// TODO: Support JPEG compression (7) - might need extra input to allow multiple images with single DQT
// TODO: Use sensible defaults for compression based on input? None is sensible... :-)
// Long term
// TODO: Support tiling
// TODO: Support thumbnails
// TODO: Support ImageIO metadata
// TODO: Support CCITT Modified Huffman compression (2)
// TODO: Full "Baseline TIFF" support
// TODO: Support LZW compression (5)?
// ----
// TODO: Support storing multiple images in one stream (multi-page TIFF)
// TODO: Support use-case: Transcode multi-layer PSD to multi-page TIFF with metadata
// TODO: Support use-case: Transcode multi-page TIFF to multiple single-page TIFFs with metadata
// TODO: Support use-case: Losslessly transcode JPEG to JPEG in TIFF with (EXIF) metadata (and back)
// Very long term...
// TODO: Support JBIG compression via ImageIO plugin/delegate? Pending support in Reader
// TODO: Support JPEG2000 compression via ImageIO plugin/delegate? Pending support in Reader
// Done
// Create a basic writer that supports most inputs. Store them using the simplest possible format.
// Support no compression (None/1) - BASELINE
// Support predictor. See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
// Support PackBits compression (32773) - easy - BASELINE
// Support ZLIB (/Deflate) compression (8) - easy
public static final Rational STANDARD_DPI = new Rational(72);
TIFFImageWriter(final ImageWriterSpi provider) {
super(provider);
}
static final class TIFFEntry extends AbstractEntry {
TIFFEntry(Object identifier, Object value) {
super(identifier, value);
}
}
@Override
public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException {
// TODO: Validate input
assertOutput();
// TODO: Consider writing TIFF header, offset to IFD0 (leave blank), write image data with correct
// tiling/compression/etc, then write IFD0, go back and update IFD0 offset?
// Write minimal TIFF header (required "Baseline" fields)
// Use EXIFWriter to write leading metadata (TODO: consider rename to TTIFFWriter, again...)
// TODO: Make TIFFEntry and possibly TIFFDirectory? public
RenderedImage renderedImage = image.getRenderedImage();
ColorModel colorModel = renderedImage.getColorModel();
int numComponents = colorModel.getNumComponents();
SampleModel sampleModel = renderedImage.getSampleModel();
int[] bandOffsets;
int[] bitOffsets;
if (sampleModel instanceof ComponentSampleModel) {
bandOffsets = ((ComponentSampleModel) sampleModel).getBandOffsets();
// System.err.println("bandOffsets: " + Arrays.toString(bandOffsets));
bitOffsets = null;
}
else if (sampleModel instanceof SinglePixelPackedSampleModel) {
bitOffsets = ((SinglePixelPackedSampleModel) sampleModel).getBitOffsets();
// System.err.println("bitOffsets: " + Arrays.toString(bitOffsets));
bandOffsets = null;
}
else if (sampleModel instanceof MultiPixelPackedSampleModel) {
bitOffsets = null;
bandOffsets = new int[] {0};
}
else {
throw new IllegalArgumentException("Unknown bit/bandOffsets for sample model: " + sampleModel);
}
List<Entry> entries = new ArrayList<Entry>();
entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, renderedImage.getWidth()));
entries.add(new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, renderedImage.getHeight()));
// entries.add(new TIFFEntry(TIFF.TAG_ORIENTATION, 1)); // (optional)
entries.add(new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, asShortArray(sampleModel.getSampleSize())));
// If numComponents > 3, write ExtraSamples
if (numComponents > 3) {
// TODO: Write per component > 3
if (colorModel.hasAlpha()) {
entries.add(new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, colorModel.isAlphaPremultiplied() ? TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA : TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA));
}
else {
entries.add(new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, TIFFBaseline.EXTRASAMPLE_UNSPECIFIED));
}
}
// Write compression field from param or metadata
int compression = TIFFImageWriteParam.getCompressionType(param);
entries.add(new TIFFEntry(TIFF.TAG_COMPRESSION, compression));
// TODO: Let param control
switch (compression) {
case TIFFExtension.COMPRESSION_ZLIB:
case TIFFExtension.COMPRESSION_DEFLATE:
case TIFFExtension.COMPRESSION_LZW:
entries.add(new TIFFEntry(TIFF.TAG_PREDICTOR, TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING));
default:
}
int photometric = getPhotometricInterpretation(colorModel);
entries.add(new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, photometric));
if (photometric == TIFFBaseline.PHOTOMETRIC_PALETTE && colorModel instanceof IndexColorModel) {
entries.add(new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel)));
entries.add(new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1));
}
else {
entries.add(new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numComponents));
}
if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT /* TODO: if (isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) {
entries.add(new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT));
}
entries.add(new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer")); // TODO: Get from metadata (optional) + fill in version number
entries.add(new TIFFEntry(TIFF.TAG_X_RESOLUTION, STANDARD_DPI));
entries.add(new TIFFEntry(TIFF.TAG_Y_RESOLUTION, STANDARD_DPI));
entries.add(new TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFFBaseline.RESOLUTION_UNIT_DPI));
// TODO: RowsPerStrip - can be entire image (or even 2^32 -1), but it's recommended to write "about 8K bytes" per strip
entries.add(new TIFFEntry(TIFF.TAG_ROWS_PER_STRIP, Integer.MAX_VALUE)); // TODO: Allowed but not recommended
// - StripByteCounts - for no compression, entire image data... (TODO: How to know the byte counts prior to writing data?)
TIFFEntry dummyStripByteCounts = new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, -1);
entries.add(dummyStripByteCounts); // Updated later
// - StripOffsets - can be offset to single strip only (TODO: but how large is the IFD data...???)
TIFFEntry dummyStripOffsets = new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, -1);
entries.add(dummyStripOffsets); // Updated later
// TODO: If tiled, write tile indexes etc, or always do that?
EXIFWriter exifWriter = new EXIFWriter();
if (compression == TIFFBaseline.COMPRESSION_NONE) {
// This implementation, allows semi-streaming-compatible uncompressed TIFFs
long streamOffset = exifWriter.computeIFDSize(entries) + 12; // 12 == 4 byte magic, 4 byte IDD 0 pointer, 4 byte EOF
entries.remove(dummyStripByteCounts);
entries.add(new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, renderedImage.getWidth() * renderedImage.getHeight() * numComponents));
entries.remove(dummyStripOffsets);
entries.add(new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, streamOffset));
exifWriter.write(entries, imageOutput); // NOTE: Writer takes case of ordering tags
imageOutput.flush();
}
else {
// Unless compression == 1 / COMPRESSION_NONE (and all offsets known), write only TIFF header/magic + leave room for IFD0 offset
exifWriter.writeTIFFHeader(imageOutput);
imageOutput.writeInt(-1); // IFD0 pointer, will be updated later
}
// TODO: Create compressor stream per Tile/Strip
// Write image data
writeImageData(createCompressorStream(renderedImage, param), renderedImage, numComponents, bandOffsets, bitOffsets);
// TODO: Update IFD0-pointer, and write IFD
if (compression != TIFFBaseline.COMPRESSION_NONE) {
long streamPosition = imageOutput.getStreamPosition();
entries.remove(dummyStripOffsets);
entries.add(new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, 8));
entries.remove(dummyStripByteCounts);
entries.add(new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, streamPosition - 8));
long ifdOffset = exifWriter.writeIFD(entries, imageOutput);
imageOutput.writeInt(0); // Next IFD (none)
streamPosition = imageOutput.getStreamPosition();
// Update IFD0 pointer
imageOutput.seek(4);
imageOutput.writeInt((int) ifdOffset);
imageOutput.seek(streamPosition);
imageOutput.flush();
}
}
private DataOutput createCompressorStream(RenderedImage image, ImageWriteParam param) {
/*
36 MB test data:
No compression:
Write time: 450 ms
output.length: 36000226
PackBits:
Write time: 688 ms
output.length: 30322187
Deflate, BEST_SPEED (1):
Write time: 1276 ms
output.length: 14128866
Deflate, 2:
Write time: 1297 ms
output.length: 13848735
Deflate, 3:
Write time: 1594 ms
output.length: 13103224
Deflate, 4:
Write time: 1663 ms
output.length: 13380899 (!!)
5
Write time: 1941 ms
output.length: 13171244
6
Write time: 2311 ms
output.length: 12845101
7: Write time: 2853 ms
output.length: 12759426
8:
Write time: 4429 ms
output.length: 12624517
Deflate: DEFAULT_COMPRESSION (6?):
Write time: 2357 ms
output.length: 12845101
Deflate, BEST_COMPRESSION (9):
Write time: 4998 ms
output.length: 12600399
*/
// TODO: Use predictor only by default for -PackBits,- LZW and ZLib/Deflate, unless explicitly disabled (ImageWriteParam)
int compression = TIFFImageWriteParam.getCompressionType(param);
OutputStream stream;
switch (compression) {
case TIFFBaseline.COMPRESSION_NONE:
return imageOutput;
case TIFFBaseline.COMPRESSION_PACKBITS:
stream = IIOUtil.createStreamAdapter(imageOutput);
stream = new EncoderStream(stream, new PackBitsEncoder(), true);
// NOTE: PackBits + Predictor is possible, but not generally supported, disable it by default
// (and probably not even allow it, see http://stackoverflow.com/questions/20337400/tiff-packbits-compression-with-predictor-step)
// stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder());
return new DataOutputStream(stream);
case TIFFExtension.COMPRESSION_ZLIB:
case TIFFExtension.COMPRESSION_DEFLATE:
int deflateSetting = Deflater.BEST_SPEED; // This is consistent with default compression quality being 1.0 and 0 meaning max compression....
if (param.getCompressionMode() == ImageWriteParam.MODE_EXPLICIT) {
// TODO: Determine how to interpret compression quality...
// Docs says:
// A compression quality setting of 0.0 is most generically interpreted as "high compression is important,"
// while a setting of 1.0 is most generically interpreted as "high image quality is important."
// Is this what JAI TIFFImageWriter (TIFFDeflater) does? No, it does:
/*
if (param & compression etc...) {
float quality = param.getCompressionQuality();
deflateLevel = (int)(1 + 8*quality);
} else {
deflateLevel = Deflater.DEFAULT_COMPRESSION;
}
*/
// PS: PNGImageWriter just uses hardcoded BEST_COMPRESSION... :-P
deflateSetting = 9 - Math.round(8 * (param.getCompressionQuality())); // This seems more correct
}
stream = IIOUtil.createStreamAdapter(imageOutput);
stream = new DeflaterOutputStream(stream, new Deflater(deflateSetting), 1024);
stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder());
return new DataOutputStream(stream);
case TIFFExtension.COMPRESSION_LZW:
// stream = IIOUtil.createStreamAdapter(imageOutput);
// stream = new EncoderStream(stream, new LZWEncoder((image.getTileWidth() * image.getTileHeight() * image.getTile(0, 0).getNumBands() * image.getColorModel().getComponentSize(0) + 7) / 8));
// stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder());
//
// return new DataOutputStream(stream);
}
throw new IllegalArgumentException(String.format("Unsupported TIFF compression: %d", compression));
}
private int getPhotometricInterpretation(final ColorModel colorModel) {
if (colorModel.getNumComponents() == 1 && colorModel.getComponentSize(0) == 1) {
if (colorModel instanceof IndexColorModel) {
if (colorModel.getRGB(0) == 0xFFFFFF && colorModel.getRGB(1) == 0x000000) {
return TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO;
}
else if (colorModel.getRGB(0) != 0x000000 || colorModel.getRGB(1) != 0xFFFFFF) {
return TIFFBaseline.PHOTOMETRIC_PALETTE;
}
// Else, fall through to default, BLACK_IS_ZERO
}
return TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO;
}
else if (colorModel instanceof IndexColorModel) {
return TIFFBaseline.PHOTOMETRIC_PALETTE;
}
switch (colorModel.getColorSpace().getType()) {
case ColorSpace.TYPE_GRAY:
return TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO;
case ColorSpace.TYPE_RGB:
return TIFFBaseline.PHOTOMETRIC_RGB;
case ColorSpace.TYPE_CMYK:
return TIFFExtension.PHOTOMETRIC_SEPARATED;
}
throw new IllegalArgumentException("Can't determine PhotometricInterpretation for color model: " + colorModel);
}
private short[] createColorMap(final IndexColorModel colorModel) {
// TIFF6.pdf p. 23:
// A TIFF color map is stored as type SHORT, count = 3 * (2^BitsPerSample)
// "In a TIFF ColorMap, all the Red values come first, followed by the Green values, then the Blue values.
// In the ColorMap, black is represented by 0,0,0 and white is represented by 65535, 65535, 65535."
short[] colorMap = new short[(int) (3 * Math.pow(2, colorModel.getPixelSize()))];
for (int i = 0; i < colorModel.getMapSize(); i++) {
int color = colorModel.getRGB(i);
colorMap[i ] = (short) upScale((color >> 16) & 0xff);
colorMap[i + colorMap.length / 3] = (short) upScale((color >> 8) & 0xff);
colorMap[i + 2 * colorMap.length / 3] = (short) upScale((color ) & 0xff);
}
return colorMap;
}
private int upScale(final int color) {
return 257 * color;
}
private short[] asShortArray(final int[] integers) {
short[] shorts = new short[integers.length];
for (int i = 0; i < shorts.length; i++) {
shorts[i] = (short) integers[i];
}
return shorts;
}
private void writeImageData(DataOutput stream, RenderedImage renderedImage, int numComponents, int[] bandOffsets, int[] bitOffsets) throws IOException {
// Store 3BYTE, 4BYTE as is (possibly need to re-arrange to RGB order)
// Store INT_RGB as 3BYTE, INT_ARGB as 4BYTE?, INT_ABGR must be re-arranged
// Store IndexColorModel as is
// Store BYTE_GRAY as is
// Store USHORT_GRAY as is
processImageStarted(0);
final int minTileY = renderedImage.getMinTileY();
final int maxYTiles = minTileY + renderedImage.getNumYTiles();
final int minTileX = renderedImage.getMinTileX();
final int maxXTiles = minTileX + renderedImage.getNumXTiles();
// Use buffer to have longer, better performing writes
final int tileHeight = renderedImage.getTileHeight();
final int tileWidth = renderedImage.getTileWidth();
// TODO: SampleSize may differ between bands/banks
int sampleSize = renderedImage.getSampleModel().getSampleSize(0);
final ByteBuffer buffer = ByteBuffer.allocate(tileWidth * renderedImage.getSampleModel().getNumBands() * sampleSize / 8);
// System.err.println("tileWidth: " + tileWidth);
for (int yTile = minTileY; yTile < maxYTiles; yTile++) {
for (int xTile = minTileX; xTile < maxXTiles; xTile++) {
final Raster tile = renderedImage.getTile(xTile, yTile);
final DataBuffer dataBuffer = tile.getDataBuffer();
final int numBands = tile.getNumBands();
// final SampleModel sampleModel = tile.getSampleModel();
switch (dataBuffer.getDataType()) {
case DataBuffer.TYPE_BYTE:
// System.err.println("Writing " + numBands + "BYTE -> " + numBands + "BYTE");
for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
for (int y = 0; y < tileHeight; y++) {
final int yOff = y * tileWidth * numBands;
for (int x = 0; x < tileWidth; x++) {
final int xOff = yOff + x * numBands;
for (int s = 0; s < numBands; s++) {
buffer.put((byte) (dataBuffer.getElem(b, xOff + bandOffsets[s]) & 0xff));
}
}
flushBuffer(buffer, stream);
if (stream instanceof DataOutputStream) {
DataOutputStream dataOutputStream = (DataOutputStream) stream;
dataOutputStream.flush();
}
}
}
break;
case DataBuffer.TYPE_USHORT:
case DataBuffer.TYPE_SHORT:
if (numComponents == 1) {
// TODO: This is foobar...
// System.err.println("Writing USHORT -> " + numBands * 2 + "_BYTES");
for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
for (int y = 0; y < tileHeight; y++) {
final int yOff = y * tileWidth;
for (int x = 0; x < tileWidth; x++) {
final int xOff = yOff + x;
buffer.putShort((short) (dataBuffer.getElem(b, xOff) & 0xffff));
}
flushBuffer(buffer, stream);
if (stream instanceof DataOutputStream) {
DataOutputStream dataOutputStream = (DataOutputStream) stream;
dataOutputStream.flush();
}
}
}
}
else {
// for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
// for (int y = 0; y < tileHeight; y++) {
// final int yOff = y * tileWidth;
//
// for (int x = 0; x < tileWidth; x++) {
// final int xOff = yOff + x;
// int element = dataBuffer.getElem(b, xOff);
//
// for (int s = 0; s < numBands; s++) {
// buffer.put((byte) ((element >> bitOffsets[s]) & 0xff));
// }
// }
//
// flushBuffer(buffer, stream);
// if (stream instanceof DataOutputStream) {
// DataOutputStream dataOutputStream = (DataOutputStream) stream;
// dataOutputStream.flush();
// }
// }
// }
throw new IllegalArgumentException("Not implemented for data type: " + dataBuffer.getDataType());
}
break;
case DataBuffer.TYPE_INT:
// TODO: This is incorrect for 32 bits/sample, only works for packed (INT_(A)RGB)
// System.err.println("Writing INT -> " + numBands + "_BYTES");
for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
for (int y = 0; y < tileHeight; y++) {
final int yOff = y * tileWidth;
for (int x = 0; x < tileWidth; x++) {
final int xOff = yOff + x;
int element = dataBuffer.getElem(b, xOff);
for (int s = 0; s < numBands; s++) {
buffer.put((byte) ((element >> bitOffsets[s]) & 0xff));
}
}
flushBuffer(buffer, stream);
if (stream instanceof DataOutputStream) {
DataOutputStream dataOutputStream = (DataOutputStream) stream;
dataOutputStream.flush();
}
}
}
break;
default:
throw new IllegalArgumentException("Not implemented for data type: " + dataBuffer.getDataType());
}
}
// TODO: Need to flush/start new compression for each row, for proper LZW/PackBits/Deflate/ZLib
if (stream instanceof DataOutputStream) {
DataOutputStream dataOutputStream = (DataOutputStream) stream;
dataOutputStream.flush();
}
// TODO: Report better progress
processImageProgress((100f * yTile) / maxYTiles);
}
if (stream instanceof DataOutputStream) {
DataOutputStream dataOutputStream = (DataOutputStream) stream;
dataOutputStream.close();
}
processImageComplete();
}
// TODO: Would be better to solve this on stream level... But writers would then have to explicitly flush the buffer before done.
private void flushBuffer(final ByteBuffer buffer, final DataOutput stream) throws IOException {
buffer.flip();
stream.write(buffer.array(), buffer.arrayOffset(), buffer.remaining());
buffer.clear();
}
// Metadata
@Override
public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) {
return null;
}
@Override
public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) {
return null;
}
// Param
@Override
public ImageWriteParam getDefaultWriteParam() {
return new TIFFImageWriteParam();
}
// Test
public static void main(String[] args) throws IOException {
int argIdx = 0;
// TODO: Proper argument parsing: -t <type> -c <compression>
int type = args.length > argIdx + 1 ? Integer.parseInt(args[argIdx++]) : -1;
int compression = args.length > argIdx + 1 ? Integer.parseInt(args[argIdx++]) : 0;
if (args.length <= argIdx) {
System.err.println("No file specified");
System.exit(1);
}
File file = new File(args[argIdx++]);
BufferedImage original;
// BufferedImage original = ImageIO.read(file);
ImageInputStream inputStream = ImageIO.createImageInputStream(file);
try {
Iterator<ImageReader> readers = ImageIO.getImageReaders(inputStream);
if (!readers.hasNext()) {
System.err.println("No reader for: " + file);
System.exit(1);
}
ImageReader reader = readers.next();
reader.setInput(inputStream);
ImageReadParam param = reader.getDefaultReadParam();
param.setDestinationType(reader.getRawImageType(0));
if (param.getDestinationType() == null) {
Iterator<ImageTypeSpecifier> types = reader.getImageTypes(0);
while (types.hasNext()) {
ImageTypeSpecifier typeSpecifier = types.next();
if (typeSpecifier.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_CMYK) {
param.setDestinationType(typeSpecifier);
}
}
}
System.err.println("param.getDestinationType(): " + param.getDestinationType());
original = reader.read(0, param);
}
finally {
inputStream.close();
}
System.err.println("original: " + original);
// BufferedImage image = original;
// BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
// BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_RGB);
// BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
// BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_BGR);
// BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
BufferedImage image;
if (type < 0 || type == original.getType()) {
image = original;
}
else if (type == BufferedImage.TYPE_BYTE_INDEXED) {
// image = ImageUtil.createIndexed(original, 256, null, ImageUtil.COLOR_SELECTION_QUALITY | ImageUtil.DITHER_DIFFUSION_ALTSCANS);
image = ImageUtil.createIndexed(original, 256, null, ImageUtil.COLOR_SELECTION_FAST | ImageUtil.DITHER_DIFFUSION_ALTSCANS);
}
else {
image = new BufferedImage(original.getWidth(), original.getHeight(), type);
Graphics2D graphics = image.createGraphics();
try {
graphics.drawImage(original, 0, 0, null);
}
finally {
graphics.dispose();
}
}
original = null;
File output = File.createTempFile(file.getName().replace('.', '-'), ".tif");
// output.deleteOnExit();
System.err.println("output: " + output);
TIFFImageWriter writer = new TIFFImageWriter(null);
// ImageWriter writer = ImageIO.getImageWritersByFormatName("PNG").next();
// ImageWriter writer = ImageIO.getImageWritersByFormatName("BMP").next();
ImageOutputStream stream = ImageIO.createImageOutputStream(output);
try {
writer.setOutput(stream);
ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
// param.setCompressionType("None");
// param.setCompressionType("PackBits");
// param.setCompressionType("ZLib");
param.setCompressionType(param.getCompressionTypes()[compression]);
// if (compression == 2) {
// param.setCompressionQuality(0);
// }
System.err.println("compression: " + param.getLocalizedCompressionTypeName());
long start = System.currentTimeMillis();
writer.write(null, new IIOImage(image, null, null), param);
System.err.println("Write time: " + (System.currentTimeMillis() - start) + " ms");
}
finally {
stream.close();
}
System.err.println("output.length: " + output.length());
// TODO: Support writing multipage TIFF
// ImageOutputStream stream = ImageIO.createImageOutputStream(output);
// try {
// writer.setOutput(stream);
// writer.prepareWriteSequence(null);
// for(int i = 0; i < images.size(); i ++){
// writer.writeToSequence(new IIOImage(images.get(i), null, null), null);
// }
// writer.endWriteSequence();
// }
// finally {
// stream.close();
// }
// writer.dispose();
image = null;
BufferedImage read = ImageIO.read(output);
System.err.println("read: " + read);
TIFFImageReader.showIt(read, output.getName());
}
}
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
import java.io.IOException;
import java.util.Locale;
/**
* TIFFImageWriterSpi
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: TIFFImageWriterSpi.java,v 1.0 18.09.13 12:46 haraldk Exp$
*/
public final class TIFFImageWriterSpi extends ImageWriterSpi {
// TODO: Implement canEncodeImage better
public TIFFImageWriterSpi() {
this(IIOUtil.getProviderInfo(TIFFImageWriterSpi.class));
}
private TIFFImageWriterSpi(final ProviderInfo providerInfo) {
super(
providerInfo.getVendorName(), providerInfo.getVersion(),
new String[] {"tiff", "TIFF", "tif", "TIFF"},
new String[] {"tif", "tiff"},
new String[] {"image/tiff", "image/x-tiff"},
"com.twelvemonkeys.imageio.plugins.tiff.TIFFImageWriter",
new Class<?>[] {ImageOutputStream.class},
new String[] {"com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReaderSpi"},
true, // supports standard stream metadata
null, null, // native stream format name and class
null, null, // extra stream formats
true, // supports standard image metadata
null, null,
null, null // extra image metadata formats
);
}
@Override
public boolean canEncodeImage(ImageTypeSpecifier type) {
// TODO: Test bit depths compatibility
return true;
}
@Override
public ImageWriter createWriterInstance(Object extension) throws IOException {
return new TIFFImageWriter(this);
}
@Override
public String getDescription(Locale locale) {
return "Aldus/Adobe Tagged Image File Format (TIFF) image writer";
}
}
@@ -0,0 +1,574 @@
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.io.LittleEndianDataInputStream;
import com.twelvemonkeys.io.LittleEndianDataOutputStream;
import org.junit.Test;
import java.io.*;
import java.nio.ByteOrder;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
/**
* HorizontalDifferencingStreamTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: HorizontalDifferencingStreamTest.java,v 1.0 02.12.13 09:50 haraldk Exp$
*/
public class HorizontalDifferencingStreamTest {
@Test
public void testWrite1SPP1BPS() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
OutputStream stream = new HorizontalDifferencingStream(bytes, 24, 1, 1, ByteOrder.BIG_ENDIAN);
// Row 1
stream.write(0xff);
stream.write(0xff);
stream.write(0xff);
// Row 2
stream.write(0x5e);
stream.write(0x1e);
stream.write(0x78);
// 1 sample per pixel, 1 bits per sample (mono/indexed)
byte[] data = {
(byte) 0x80, 0x00, 0x00,
0x71, 0x11, 0x44,
};
assertArrayEquals(data, bytes.toByteArray());
}
@Test
public void testWrite1SPP2BPS() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
OutputStream stream = new HorizontalDifferencingStream(bytes, 16, 1, 2, ByteOrder.BIG_ENDIAN);
// Row 1
stream.write(0xff);
stream.write(0xff);
stream.write(0xff);
stream.write(0xff);
// Row 2
stream.write(0x41);
stream.write(0x6b);
stream.write(0x05);
stream.write(0x0f);
// 1 sample per pixel, 2 bits per sample (gray/indexed)
byte[] data = {
(byte) 0xc0, 0x00, 0x00, 0x00,
0x71, 0x11, 0x44, (byte) 0xcc,
};
assertArrayEquals(data, bytes.toByteArray());
}
@Test
public void testWrite1SPP4BPS() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
OutputStream stream = new HorizontalDifferencingStream(bytes, 8, 1, 4, ByteOrder.BIG_ENDIAN);
// Row 1
stream.write(0xff);
stream.write(0xff);
stream.write(0xff);
stream.write(0xff);
// Row 2
stream.write(0x77);
stream.write(0x89);
stream.write(0xd1);
stream.write(0xd9);
// Row 3
stream.write(0x00);
stream.write(0x01);
stream.write(0x22);
stream.write(0x00);
// 1 sample per pixel, 4 bits per sample (gray/indexed)
byte[] data = {
(byte) 0xf0, 0x00, 0x00, 0x00,
0x70, 0x11, 0x44, (byte) 0xcc,
0x00, 0x01, 0x10, (byte) 0xe0
};
assertArrayEquals(data, bytes.toByteArray());
}
@Test
public void testWrite1SPP8BPS() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
OutputStream stream = new HorizontalDifferencingStream(bytes, 4, 1, 8, ByteOrder.BIG_ENDIAN);
// Row 1
stream.write(0xff);
stream.write(0xff);
stream.write(0xff);
stream.write(0xff);
// Row 2
stream.write(0x7f);
stream.write(0x80);
stream.write(0x84);
stream.write(0x80);
// Row 3
stream.write(0x00);
stream.write(0x7f);
stream.write(0xfe);
stream.write(0x7f);
// 1 sample per pixel, 8 bits per sample (gray/indexed)
byte[] data = {
(byte) 0xff, 0, 0, 0,
0x7f, 1, 4, -4,
0x00, 127, 127, -127
};
assertArrayEquals(data, bytes.toByteArray());
}
@Test
public void testWriteArray1SPP8BPS() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
OutputStream stream = new HorizontalDifferencingStream(bytes, 4, 1, 8, ByteOrder.BIG_ENDIAN);
stream.write(new byte[] {
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
0x7f, (byte) 0x80, (byte) 0x84, (byte) 0x80,
0x00, 0x7f, (byte) 0xfe, 0x7f,
});
// 1 sample per pixel, 8 bits per sample (gray/indexed)
byte[] data = {
(byte) 0xff, 0, 0, 0,
0x7f, 1, 4, -4,
0x00, 127, 127, -127
};
assertArrayEquals(data, bytes.toByteArray());
}
@Test
public void testWrite1SPP32BPS() throws IOException {
// 1 sample per pixel, 32 bits per sample (gray)
FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(16);
OutputStream out = new HorizontalDifferencingStream(bytes, 4, 1, 32, ByteOrder.BIG_ENDIAN);
DataOutput dataOut = new DataOutputStream(out);
dataOut.writeInt(0x00000000);
dataOut.writeInt(305419896);
dataOut.writeInt(305419896);
dataOut.writeInt(-610839792);
InputStream in = bytes.createInputStream();
DataInput dataIn = new DataInputStream(in);
// Row 1
assertEquals(0, dataIn.readInt());
assertEquals(305419896, dataIn.readInt());
assertEquals(0, dataIn.readInt());
assertEquals(-916259688, dataIn.readInt());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testWrite1SPP32BPSLittleEndian() throws IOException {
// 1 sample per pixel, 32 bits per sample (gray)
FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(16);
OutputStream out = new HorizontalDifferencingStream(bytes, 4, 1, 32, ByteOrder.LITTLE_ENDIAN);
DataOutput dataOut = new LittleEndianDataOutputStream(out);
dataOut.writeInt(0x00000000);
dataOut.writeInt(305419896);
dataOut.writeInt(305419896);
dataOut.writeInt(-610839792);
InputStream in = bytes.createInputStream();
DataInput dataIn = new LittleEndianDataInputStream(in);
// Row 1
assertEquals(0, dataIn.readInt());
assertEquals(305419896, dataIn.readInt());
assertEquals(0, dataIn.readInt());
assertEquals(-916259688, dataIn.readInt());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testWrite1SPP64BPS() throws IOException {
// 1 sample per pixel, 64 bits per sample (gray)
FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(32);
OutputStream out = new HorizontalDifferencingStream(bytes, 4, 1, 64, ByteOrder.BIG_ENDIAN);
DataOutput dataOut = new DataOutputStream(out);
dataOut.writeLong(0x00000000);
dataOut.writeLong(81985529216486895L);
dataOut.writeLong(81985529216486895L);
dataOut.writeLong(-163971058432973790L);
InputStream in = bytes.createInputStream();
DataInput dataIn = new DataInputStream(in);
// Row 1
assertEquals(0, dataIn.readLong());
assertEquals(81985529216486895L, dataIn.readLong());
assertEquals(0, dataIn.readLong());
assertEquals(-245956587649460685L, dataIn.readLong());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testWrite1SPP64BPSLittleEndian() throws IOException {
// 1 sample per pixel, 64 bits per sample (gray)
FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(32);
OutputStream out = new HorizontalDifferencingStream(bytes, 4, 1, 64, ByteOrder.LITTLE_ENDIAN);
DataOutput dataOut = new LittleEndianDataOutputStream(out);
dataOut.writeLong(0x00000000);
dataOut.writeLong(81985529216486895L);
dataOut.writeLong(81985529216486895L);
dataOut.writeLong(-163971058432973790L);
InputStream in = bytes.createInputStream();
DataInput dataIn = new LittleEndianDataInputStream(in);
// Row 1
assertEquals(0, dataIn.readLong());
assertEquals(81985529216486895L, dataIn.readLong());
assertEquals(0, dataIn.readLong());
assertEquals(-245956587649460685L, dataIn.readLong());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testWrite3SPP8BPS() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
OutputStream stream = new HorizontalDifferencingStream(bytes, 4, 3, 8, ByteOrder.BIG_ENDIAN);
// Row 1
stream.write(0xff);
stream.write(0x00);
stream.write(0x7f);
stream.write(0xfe);
stream.write(0xff);
stream.write(0x7e);
stream.write(0xfa);
stream.write(0xfb);
stream.write(0x7a);
stream.write(0xfe);
stream.write(0xff);
stream.write(0x7e);
// Row 2
stream.write(0x7f);
stream.write(0x7f);
stream.write(0x7f);
stream.write(0x80);
stream.write(0x80);
stream.write(0x80);
stream.write(0x84);
stream.write(0x84);
stream.write(0x84);
stream.write(0x80);
stream.write(0x80);
stream.write(0x80);
// Row 3
stream.write(0x00);
stream.write(0x00);
stream.write(0x00);
stream.write(0x7f);
stream.write(0x81);
stream.write(0x00);
stream.write(0x00);
stream.write(0x00);
stream.write(0x00);
stream.write(0x00);
stream.write(0x00);
stream.write(0x7f);
// 3 samples per pixel, 8 bits per sample (RGB)
byte[] data = {
(byte) 0xff, (byte) 0x00, (byte) 0x7f, -1, -1, -1, -4, -4, -4, 4, 4, 4,
0x7f, 0x7f, 0x7f, 1, 1, 1, 4, 4, 4, -4, -4, -4,
0x00, 0x00, 0x00, 127, -127, 0, -127, 127, 0, 0, 0, 127,
};
assertArrayEquals(data, bytes.toByteArray());
}
@Test
public void testWrite3SPP16BPS() throws IOException {
FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(24);
OutputStream out = new HorizontalDifferencingStream(bytes, 4, 3, 16, ByteOrder.BIG_ENDIAN);
DataOutput dataOut = new DataOutputStream(out);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(4660);
dataOut.writeShort(30292);
dataOut.writeShort(4660);
dataOut.writeShort(4660);
dataOut.writeShort(30292);
dataOut.writeShort(4660);
dataOut.writeShort(-9320);
dataOut.writeShort(-60584);
dataOut.writeShort(-9320);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(-60584);
dataOut.writeShort(-60584);
dataOut.writeShort(-60584);
InputStream in = bytes.createInputStream();
DataInput dataIn = new DataInputStream(in);
// Row 1
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(4660, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(4660, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(51556, dataIn.readUnsignedShort());
assertEquals(40196, dataIn.readUnsignedShort());
assertEquals(51556, dataIn.readUnsignedShort());
// Row 2
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(40196, dataIn.readUnsignedShort());
assertEquals(40196, dataIn.readUnsignedShort());
assertEquals(40196, dataIn.readUnsignedShort());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testWrite3SPP16BPSLittleEndian() throws IOException {
FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(24);
OutputStream out = new HorizontalDifferencingStream(bytes, 4, 3, 16, ByteOrder.LITTLE_ENDIAN);
DataOutput dataOut = new LittleEndianDataOutputStream(out);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(4660);
dataOut.writeShort(30292);
dataOut.writeShort(4660);
dataOut.writeShort(4660);
dataOut.writeShort(30292);
dataOut.writeShort(4660);
dataOut.writeShort(-9320);
dataOut.writeShort(-60584);
dataOut.writeShort(-9320);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(-60584);
dataOut.writeShort(-60584);
dataOut.writeShort(-60584);
InputStream in = bytes.createInputStream();
DataInput dataIn = new LittleEndianDataInputStream(in);
// Row 1
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(4660, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(4660, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(51556, dataIn.readUnsignedShort());
assertEquals(40196, dataIn.readUnsignedShort());
assertEquals(51556, dataIn.readUnsignedShort());
// Row 2
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(40196, dataIn.readUnsignedShort());
assertEquals(40196, dataIn.readUnsignedShort());
assertEquals(40196, dataIn.readUnsignedShort());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testWrite4SPP8BPS() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
OutputStream stream = new HorizontalDifferencingStream(bytes, 4, 4, 8, ByteOrder.BIG_ENDIAN);
// Row 1
stream.write(0xff);
stream.write(0x00);
stream.write(0x7f);
stream.write(0x00);
stream.write(0xfe);
stream.write(0xff);
stream.write(0x7e);
stream.write(0xff);
stream.write(0xfa);
stream.write(0xfb);
stream.write(0x7a);
stream.write(0xfb);
stream.write(0xfe);
stream.write(0xff);
stream.write(0x7e);
stream.write(0xff);
// Row 2
stream.write(0x7f);
stream.write(0x7f);
stream.write(0x7f);
stream.write(0x7f);
stream.write(0x80);
stream.write(0x80);
stream.write(0x80);
stream.write(0x80);
stream.write(0x84);
stream.write(0x84);
stream.write(0x84);
stream.write(0x84);
stream.write(0x80);
stream.write(0x80);
stream.write(0x80);
stream.write(0x80);
// 4 samples per pixel, 8 bits per sample (RGBA)
byte[] data = {
(byte) 0xff, (byte) 0x00, (byte) 0x7f, 0x00, -1, -1, -1, -1, -4, -4, -4, -4, 4, 4, 4, 4,
0x7f, 0x7f, 0x7f, 0x7f, 1, 1, 1, 1, 4, 4, 4, 4, -4, -4, -4, -4,
};
assertArrayEquals(data, bytes.toByteArray());
}
@Test
public void testWriteArray4SPP8BPS() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
OutputStream stream = new HorizontalDifferencingStream(bytes, 4, 4, 8, ByteOrder.BIG_ENDIAN);
stream.write(
new byte[] {
(byte) 0xff, 0x00, 0x7f, 0x00,
(byte) 0xfe, (byte) 0xff, 0x7e, (byte) 0xff,
(byte) 0xfa, (byte) 0xfb, 0x7a, (byte) 0xfb,
(byte) 0xfe, (byte) 0xff, 0x7e, (byte) 0xff,
0x7f, 0x7f, 0x7f, 0x7f,
(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
(byte) 0x84, (byte) 0x84, (byte) 0x84, (byte) 0x84,
(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
}
);
// 4 samples per pixel, 8 bits per sample (RGBA)
byte[] data = {
(byte) 0xff, (byte) 0x00, (byte) 0x7f, 0x00, -1, -1, -1, -1, -4, -4, -4, -4, 4, 4, 4, 4,
0x7f, 0x7f, 0x7f, 0x7f, 1, 1, 1, 1, 4, 4, 4, 4, -4, -4, -4, -4,
};
assertArrayEquals(data, bytes.toByteArray());
}
}
@@ -0,0 +1,73 @@
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.imageio.util.ImageWriterAbstractTestCase;
import javax.imageio.ImageWriter;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.util.Arrays;
import java.util.List;
/**
* TIFFImageWriterTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: TIFFImageWriterTest.java,v 1.0 19.09.13 13:22 haraldk Exp$
*/
public class TIFFImageWriterTest extends ImageWriterAbstractTestCase {
public static final TIFFImageWriterSpi PROVIDER = new TIFFImageWriterSpi();
@Override
protected ImageWriter createImageWriter() {
return new TIFFImageWriter(PROVIDER);
}
@Override
protected List<? extends RenderedImage> getTestData() {
BufferedImage image = new BufferedImage(300, 200, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = image.createGraphics();
try {
graphics.setColor(Color.RED);
graphics.fillRect(0, 0, 100, 200);
graphics.setColor(Color.BLUE);
graphics.fillRect(100, 0, 100, 200);
graphics.clearRect(200, 0, 100, 200);
}
finally {
graphics.dispose();
}
return Arrays.asList(image);
}
}