mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-04 03:55:28 -04:00
#306 TIFF LZW IndexColorModel issue + sequence index
This commit is contained in:
parent
10b8c11a8e
commit
762b59674b
@ -38,11 +38,13 @@ import com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry;
|
||||
import com.twelvemonkeys.imageio.metadata.tiff.TIFFWriter;
|
||||
import com.twelvemonkeys.imageio.stream.SubImageOutputStream;
|
||||
import com.twelvemonkeys.imageio.util.IIOUtil;
|
||||
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
|
||||
import com.twelvemonkeys.io.enc.EncoderStream;
|
||||
import com.twelvemonkeys.io.enc.PackBitsEncoder;
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
import javax.imageio.*;
|
||||
import javax.imageio.event.IIOWriteWarningListener;
|
||||
import javax.imageio.metadata.IIOInvalidTreeException;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.metadata.IIOMetadataFormatImpl;
|
||||
@ -69,19 +71,12 @@ import static com.twelvemonkeys.imageio.plugins.tiff.TIFFStreamMetadata.configur
|
||||
* @version $Id: TIFFImageWriter.java,v 1.0 18.09.13 12:46 haraldk Exp$
|
||||
*/
|
||||
public final class TIFFImageWriter extends ImageWriterBase {
|
||||
// Short term
|
||||
// TODO: Support more of the ImageIO metadata (ie. compression from metadata, etc)
|
||||
|
||||
// Long term
|
||||
// TODO: Support tiling
|
||||
// TODO: Support thumbnails
|
||||
// TODO: Support CCITT Modified Huffman compression (2)
|
||||
// TODO: Full "Baseline TIFF" support (pending CCITT compression 2)
|
||||
// TODO: CCITT compressions T.4 and T.6
|
||||
// TODO: Support JPEG compression of CMYK data (pending JPEGImageWriter CMYK write support)
|
||||
// ----
|
||||
// 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-layer PSD to multi-page TIFF with metadata (hard, as Photoshop don't store layers as multi-page TIFF...)
|
||||
// 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)
|
||||
|
||||
@ -99,18 +94,25 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
// Support JPEG compression (7) - might need extra input to allow multiple images with single DQT
|
||||
// Use sensible defaults for compression based on input? None is sensible... :-)
|
||||
// Support resolution, resolution unit and software tags from ImageIO metadata
|
||||
// Support CCITT Modified Huffman compression (2)
|
||||
// Full "Baseline TIFF" support (pending CCITT compression 2)
|
||||
// CCITT compressions T.4 and T.6
|
||||
// Support storing multiple images in one stream (multi-page TIFF)
|
||||
// Support more of the ImageIO metadata (ie. compression from metadata, etc)
|
||||
|
||||
private static final Rational STANDARD_DPI = new Rational(72);
|
||||
|
||||
/**
|
||||
* Flag for active sequence writing
|
||||
*/
|
||||
private boolean isWritingSequence = false;
|
||||
private boolean writingSequence = false;
|
||||
|
||||
private int sequenceIndex = 0;
|
||||
|
||||
/**
|
||||
* Metadata writer for sequence writing
|
||||
*/
|
||||
private TIFFWriter sequenceTiffWriter = null;
|
||||
private TIFFWriter sequenceTIFFWriter = null;
|
||||
|
||||
/**
|
||||
* Position of last IFD Pointer on active sequence writing
|
||||
@ -137,12 +139,12 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
TIFFWriter tiffWriter = new TIFFWriter();
|
||||
tiffWriter.writeTIFFHeader(imageOutput);
|
||||
|
||||
writePage(image, param, tiffWriter, imageOutput.getStreamPosition());
|
||||
writePage(0, image, param, tiffWriter, imageOutput.getStreamPosition());
|
||||
|
||||
imageOutput.flush();
|
||||
}
|
||||
|
||||
private long writePage(IIOImage image, ImageWriteParam param, TIFFWriter tiffWriter, long lastIFDPointerOffset)
|
||||
private long writePage(int imageIndex, IIOImage image, ImageWriteParam param, TIFFWriter tiffWriter, long lastIFDPointerOffset)
|
||||
throws IOException {
|
||||
RenderedImage renderedImage = image.getRenderedImage();
|
||||
|
||||
@ -246,7 +248,9 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
entries.put(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, photometric));
|
||||
|
||||
if (photometric == TIFFBaseline.PHOTOMETRIC_PALETTE && colorModel instanceof IndexColorModel) {
|
||||
entries.put(TIFF.TAG_COLOR_MAP, new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel)));
|
||||
// TODO: Fix consistency between sampleModel.getSampleSize() and colorModel.getPixelSize()...
|
||||
// We should be able to support 1, 2, 4 and 8 bits per sample at least, and probably 3, 5, 6 and 7 too
|
||||
entries.put(TIFF.TAG_COLOR_MAP, new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel, sampleModel.getSampleSize(0))));
|
||||
entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1));
|
||||
}
|
||||
else {
|
||||
@ -353,6 +357,9 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
ImageWriter jpegWriter = writers.next();
|
||||
try {
|
||||
jpegWriter.setOutput(new SubImageOutputStream(imageOutput));
|
||||
ListenerDelegate listener = new ListenerDelegate(imageIndex);
|
||||
jpegWriter.addIIOWriteProgressListener(listener);
|
||||
jpegWriter.addIIOWriteWarningListener(listener);
|
||||
jpegWriter.write(renderedImage);
|
||||
}
|
||||
finally {
|
||||
@ -361,7 +368,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
}
|
||||
else {
|
||||
// Write image data
|
||||
writeImageData(createCompressorStream(renderedImage, param, entries), renderedImage, numBands, bandOffsets, bitOffsets);
|
||||
writeImageData(createCompressorStream(renderedImage, param, entries), imageIndex, renderedImage, numBands, bandOffsets, bitOffsets);
|
||||
}
|
||||
|
||||
long stripByteCount = imageOutput.getStreamPosition() - stripOffset;
|
||||
@ -451,6 +458,9 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
output.length: 12600399
|
||||
*/
|
||||
|
||||
int samplesPerPixel = (Integer) entries.get(TIFF.TAG_SAMPLES_PER_PIXEL).getValue();
|
||||
int bitPerSample = ((short[]) entries.get(TIFF.TAG_BITS_PER_SAMPLE).getValue())[0];
|
||||
|
||||
// Use predictor by default for LZW and ZLib/Deflate
|
||||
// TODO: Unless explicitly disabled in TIFFImageWriteParam
|
||||
int compression = (int) entries.get(TIFF.TAG_COMPRESSION).getValue();
|
||||
@ -489,16 +499,16 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
stream = IIOUtil.createStreamAdapter(imageOutput);
|
||||
stream = new DeflaterOutputStream(stream, new Deflater(deflateSetting), 1024);
|
||||
if (entries.containsKey(TIFF.TAG_PREDICTOR) && entries.get(TIFF.TAG_PREDICTOR).getValue().equals(TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING)) {
|
||||
stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder());
|
||||
stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), samplesPerPixel, bitPerSample, 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.getColorModel().getPixelSize() + 7) / 8));
|
||||
stream = new EncoderStream(stream, new LZWEncoder((image.getTileWidth() * image.getTileHeight() * samplesPerPixel * bitPerSample + 7) / 8));
|
||||
if (entries.containsKey(TIFF.TAG_PREDICTOR) && entries.get(TIFF.TAG_PREDICTOR).getValue().equals(TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING)) {
|
||||
stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder());
|
||||
stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), samplesPerPixel, bitPerSample, imageOutput.getByteOrder());
|
||||
}
|
||||
|
||||
return new DataOutputStream(stream);
|
||||
@ -553,12 +563,12 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
throw new IllegalArgumentException("Can't determine PhotometricInterpretation for color model: " + colorModel);
|
||||
}
|
||||
|
||||
private short[] createColorMap(final IndexColorModel colorModel) {
|
||||
private short[] createColorMap(final IndexColorModel colorModel, final int sampleSize) {
|
||||
// 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()))];
|
||||
short[] colorMap = new short[(int) (3 * Math.pow(2, sampleSize))];
|
||||
|
||||
for (int i = 0; i < colorModel.getMapSize(); i++) {
|
||||
int color = colorModel.getRGB(i);
|
||||
@ -584,14 +594,14 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
return shorts;
|
||||
}
|
||||
|
||||
private void writeImageData(DataOutput stream, RenderedImage renderedImage, int numComponents, int[] bandOffsets, int[] bitOffsets) throws IOException {
|
||||
private void writeImageData(DataOutput stream, int imageIndex, 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);
|
||||
processImageStarted(imageIndex);
|
||||
|
||||
final int minTileY = renderedImage.getMinTileY();
|
||||
final int maxYTiles = minTileY + renderedImage.getNumYTiles();
|
||||
@ -777,7 +787,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
}
|
||||
|
||||
// TODO: Report better progress
|
||||
processImageProgress((100f * yTile) / maxYTiles);
|
||||
processImageProgress((100f * (yTile + 1)) / maxYTiles);
|
||||
}
|
||||
|
||||
if (stream instanceof DataOutputStream) {
|
||||
@ -884,21 +894,22 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
|
||||
@Override
|
||||
public void prepareWriteSequence(IIOMetadata streamMetadata) throws IOException {
|
||||
if (isWritingSequence) {
|
||||
if (writingSequence) {
|
||||
throw new IllegalStateException("sequence writing has already been started!");
|
||||
}
|
||||
|
||||
// Ignore streamMetadata. ByteOrder is determined from OutputStream
|
||||
assertOutput();
|
||||
isWritingSequence = true;
|
||||
sequenceTiffWriter = new TIFFWriter();
|
||||
sequenceTiffWriter.writeTIFFHeader(imageOutput);
|
||||
configureStreamByteOrder(streamMetadata, imageOutput);
|
||||
|
||||
writingSequence = true;
|
||||
sequenceTIFFWriter = new TIFFWriter();
|
||||
sequenceTIFFWriter.writeTIFFHeader(imageOutput);
|
||||
sequenceLastIFDPos = imageOutput.getStreamPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToSequence(IIOImage image, ImageWriteParam param) throws IOException {
|
||||
if (!isWritingSequence) {
|
||||
if (!writingSequence) {
|
||||
throw new IllegalStateException("prepareWriteSequence() must be called before writeToSequence()!");
|
||||
}
|
||||
|
||||
@ -906,17 +917,18 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
imageOutput.flushBefore(sequenceLastIFDPos);
|
||||
}
|
||||
|
||||
sequenceLastIFDPos = writePage(image, param, sequenceTiffWriter, sequenceLastIFDPos);
|
||||
sequenceLastIFDPos = writePage(sequenceIndex++, image, param, sequenceTIFFWriter, sequenceLastIFDPos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endWriteSequence() throws IOException {
|
||||
if (!isWritingSequence) {
|
||||
if (!writingSequence) {
|
||||
throw new IllegalStateException("prepareWriteSequence() must be called before endWriteSequence()!");
|
||||
}
|
||||
|
||||
isWritingSequence = false;
|
||||
sequenceTiffWriter = null;
|
||||
writingSequence = false;
|
||||
sequenceIndex = 0;
|
||||
sequenceTIFFWriter = null;
|
||||
sequenceLastIFDPos = -1;
|
||||
imageOutput.flush();
|
||||
}
|
||||
@ -925,8 +937,9 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
protected void resetMembers() {
|
||||
super.resetMembers();
|
||||
|
||||
isWritingSequence = false;
|
||||
sequenceTiffWriter = null;
|
||||
writingSequence = false;
|
||||
sequenceIndex = 0;
|
||||
sequenceTIFFWriter = null;
|
||||
sequenceLastIFDPos = -1;
|
||||
}
|
||||
|
||||
@ -1046,7 +1059,6 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
|
||||
System.err.println("output.length: " + output.length());
|
||||
|
||||
// TODO: Support writing multipage TIFF
|
||||
// ImageOutputStream stream = ImageIO.createImageOutputStream(output);
|
||||
// try {
|
||||
// writer.setOutput(stream);
|
||||
@ -1068,4 +1080,52 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
|
||||
TIFFImageReader.showIt(read, output.getName());
|
||||
}
|
||||
|
||||
private class ListenerDelegate extends ProgressListenerBase implements IIOWriteWarningListener {
|
||||
private final int imageIndex;
|
||||
|
||||
public ListenerDelegate(final int imageIndex) {
|
||||
this.imageIndex = imageIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void imageComplete(ImageWriter source) {
|
||||
processImageComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void imageProgress(ImageWriter source, float percentageDone) {
|
||||
processImageProgress(percentageDone);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void imageStarted(ImageWriter source, int imageIndex) {
|
||||
processImageStarted(this.imageIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void thumbnailComplete(ImageWriter source) {
|
||||
processThumbnailComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void thumbnailProgress(ImageWriter source, float percentageDone) {
|
||||
processThumbnailProgress(percentageDone);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void thumbnailStarted(ImageWriter source, int imageIndex, int thumbnailIndex) {
|
||||
processThumbnailStarted(this.imageIndex, thumbnailIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeAborted(ImageWriter source) {
|
||||
processWriteAborted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warningOccurred(ImageWriter source, int imageIndex, String warning) {
|
||||
processWarningOccurred(this.imageIndex, warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,10 +36,12 @@ import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
|
||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
||||
import com.twelvemonkeys.imageio.util.ImageWriterAbstractTestCase;
|
||||
import com.twelvemonkeys.io.FastByteArrayOutputStream;
|
||||
import com.twelvemonkeys.io.NullOutputStream;
|
||||
import org.junit.Test;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.imageio.*;
|
||||
import javax.imageio.event.IIOWriteProgressListener;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.metadata.IIOMetadataFormatImpl;
|
||||
import javax.imageio.metadata.IIOMetadataNode;
|
||||
@ -60,6 +62,7 @@ import static com.twelvemonkeys.imageio.plugins.tiff.TIFFImageMetadataTest.creat
|
||||
import static com.twelvemonkeys.imageio.util.ImageReaderAbstractTest.assertRGBEquals;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assume.assumeNotNull;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* TIFFImageWriterTest
|
||||
@ -382,6 +385,50 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteSequenceProgress() throws IOException {
|
||||
BufferedImage[] images = new BufferedImage[] {
|
||||
new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB),
|
||||
new BufferedImage(110, 100, BufferedImage.TYPE_INT_RGB),
|
||||
new BufferedImage(120, 100, BufferedImage.TYPE_INT_RGB)
|
||||
};
|
||||
|
||||
ImageWriter writer = createImageWriter();
|
||||
IIOWriteProgressListener progress = mock(IIOWriteProgressListener.class, "progress");
|
||||
writer.addIIOWriteProgressListener(progress);
|
||||
|
||||
try (ImageOutputStream output = ImageIO.createImageOutputStream(new NullOutputStream())) {
|
||||
writer.setOutput(output);
|
||||
|
||||
try {
|
||||
writer.prepareWriteSequence(null);
|
||||
|
||||
for (int i = 0; i < images.length; i++) {
|
||||
reset(progress);
|
||||
|
||||
ImageWriteParam param = writer.getDefaultWriteParam();
|
||||
|
||||
if (i == images.length - 1) {
|
||||
// Make sure that the JPEG delegation outputs the correct indexes
|
||||
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
|
||||
param.setCompressionType("JPEG");
|
||||
}
|
||||
|
||||
writer.writeToSequence(new IIOImage(images[i], null, null), param);
|
||||
|
||||
verify(progress, times(1)).imageStarted(writer, i);
|
||||
verify(progress, atLeastOnce()).imageProgress(eq(writer), anyFloat());
|
||||
verify(progress, times(1)).imageComplete(writer);
|
||||
}
|
||||
|
||||
writer.endWriteSequence();
|
||||
}
|
||||
catch (IOException e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadWriteRead1BitLZW() throws IOException {
|
||||
// Read original LZW compressed TIFF
|
||||
|
Loading…
x
Reference in New Issue
Block a user