#212 TIFF subsampling fix

This commit is contained in:
Harald Kuhr
2016-11-02 19:06:12 +01:00
parent 3f6bc722bc
commit 34eb084d24
11 changed files with 273 additions and 50 deletions

View File

@@ -52,6 +52,7 @@ import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.io.LittleEndianDataInputStream;
import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder;
import com.twelvemonkeys.xml.XMLSerializer;
import org.w3c.dom.NodeList;
import javax.imageio.*;
@@ -454,7 +455,7 @@ public class TIFFImageReader extends ImageReaderBase {
return ImageTypeSpecifiers.createPacked(cs, 0xF000, 0xF00, 0xF0, 0xF, DataBuffer.TYPE_USHORT, isAlphaPremultiplied);
}
default:
throw new IIOException(String.format("Unsupported SamplesPerPixels/BitsPerSample combination for RGB TIFF (expected 3/8, 4/8, 3/16 or 4/16): %d/%d", samplesPerPixel, bitsPerSample));
throw new IIOException(String.format("Unsupported SamplesPerPixel/BitsPerSample combination for RGB TIFF (expected 3/8, 4/8, 3/16 or 4/16): %d/%d", samplesPerPixel, bitsPerSample));
}
case TIFFBaseline.PHOTOMETRIC_PALETTE:
// Palette
@@ -517,7 +518,7 @@ public class TIFFImageReader extends ImageReaderBase {
default:
throw new IIOException(
String.format("Unsupported SamplesPerPixels/BitsPerSample combination for Separated TIFF (expected 4/8, 4/16, 5/8 or 5/16): %d/%s", samplesPerPixel, bitsPerSample)
String.format("Unsupported SamplesPerPixel/BitsPerSample combination for Separated TIFF (expected 4/8, 4/16, 5/8 or 5/16): %d/%s", samplesPerPixel, bitsPerSample)
);
}
case TIFFExtension.PHOTOMETRIC_CIELAB:
@@ -777,7 +778,7 @@ public class TIFFImageReader extends ImageReaderBase {
// NOTE: We handle strips as tiles of tileWidth == width by tileHeight == rowsPerStrip
// Strips are top/down, tiles are left/right, top/down
int stripTileWidth = width;
long rowsPerStrip = getValueAsLongWithDefault(TIFF.TAG_ROWS_PER_STRIP, (1L << 32) - 1);
long rowsPerStrip = getValueAsLongWithDefault(TIFF.TAG_ROWS_PER_STRIP, (long) Integer.MAX_VALUE);
int stripTileHeight = rowsPerStrip < height ? (int) rowsPerStrip : height;
long[] stripTileOffsets = getValueAsLongArray(TIFF.TAG_TILE_OFFSETS, "TileOffsets", false);
@@ -924,7 +925,7 @@ public class TIFFImageReader extends ImageReaderBase {
}
// Clip the stripTile rowRaster to not exceed the srcRegion
clip.width = Math.min((colsInTile + xSub - 1) / xSub, srcRegion.width);
clip.width = Math.min(colsInTile, srcRegion.width);
Raster clippedRow = clipRowToRect(rowRaster, clip,
param != null ? param.getSourceBands() : null,
param != null ? param.getSourceXSubsampling() : 1);
@@ -992,7 +993,7 @@ public class TIFFImageReader extends ImageReaderBase {
// Read only tiles that lies within region
Rectangle tileRect = new Rectangle(col, row, colsInTile, rowsInTile);
Rectangle intersection = tileRect.intersection( srcRegion );
Rectangle intersection = tileRect.intersection(srcRegion);
if (!intersection.isEmpty()) {
imageInput.seek(stripTileOffsets[i]);
@@ -1000,7 +1001,9 @@ public class TIFFImageReader extends ImageReaderBase {
try (ImageInputStream subStream = new SubImageInputStream(imageInput, length)) {
jpegReader.setInput(subStream);
jpegParam.setSourceRegion(new Rectangle( intersection.x - col, intersection.y - row, intersection.width, intersection.height));
jpegParam.setSourceRegion(new Rectangle(intersection.x - col, intersection.y - row, intersection.width, intersection.height));
jpegParam.setSourceSubsampling(xSub, ySub, 0, 0);
Point offset = new Point((intersection.x - srcRegion.x) / xSub, (intersection.y - srcRegion.y) / ySub);
// TODO: If we have non-standard reference B/W or yCbCr coefficients,
// we might still have to do extra color space conversion...
@@ -1009,7 +1012,7 @@ public class TIFFImageReader extends ImageReaderBase {
}
if (!needsCSConversion) {
jpegParam.setDestinationOffset(new Point(intersection.x - srcRegion.x, intersection.y - srcRegion.y));
jpegParam.setDestinationOffset(offset);
jpegParam.setDestination(destination);
jpegReader.read(0, jpegParam);
}
@@ -1018,7 +1021,7 @@ public class TIFFImageReader extends ImageReaderBase {
// We'll have to use readAsRaster and later apply color space conversion ourselves
Raster raster = jpegReader.readRaster(0, jpegParam);
normalizeColor(interpretation, ((DataBufferByte) raster.getDataBuffer()).getData());
destination.getRaster().setDataElements(intersection.x - srcRegion.x, intersection.y - srcRegion.y, raster);
destination.getRaster().setDataElements(offset.x, offset.y, raster);
}
}
}
@@ -1140,12 +1143,14 @@ public class TIFFImageReader extends ImageReaderBase {
try (ImageInputStream stream = new SubImageInputStream(imageInput, length)) {
jpegReader.setInput(stream);
jpegParam.setSourceRegion(srcRegion);
jpegParam.setSourceSubsampling(xSub, ySub, 0, 0);
if (needsCSConversion == null) {
needsCSConversion = needsCSConversion(interpretation, jpegReader.getImageMetadata(0));
}
if (!needsCSConversion) {
// Single tile, no dest offset needed
jpegParam.setDestination(destination);
jpegReader.read(0, jpegParam);
}
@@ -1242,14 +1247,15 @@ public class TIFFImageReader extends ImageReaderBase {
)))) {
jpegReader.setInput(stream);
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
jpegParam.setDestinationOffset(new Point(col - srcRegion.x, row - srcRegion.y));
jpegParam.setDestination(destination);
jpegParam.setSourceSubsampling(xSub, ySub, 0, 0);
Point offset = new Point(col - srcRegion.x, row - srcRegion.y);
if (needsCSConversion == null) {
needsCSConversion = needsCSConversion(interpretation, jpegReader.getImageMetadata(0));
}
if (!needsCSConversion) {
jpegParam.setDestinationOffset(offset);
jpegParam.setDestination(destination);
jpegReader.read(0, jpegParam);
}
@@ -1258,7 +1264,7 @@ public class TIFFImageReader extends ImageReaderBase {
// We'll have to use readAsRaster and later apply color space conversion ourselves
Raster raster = jpegReader.readRaster(0, jpegParam);
normalizeColor(interpretation, ((DataBufferByte) raster.getDataBuffer()).getData());
destination.getRaster().setDataElements(0, 0, raster);
destination.getRaster().setDataElements(offset.x, offset.y, raster);
}
}
}
@@ -1385,7 +1391,7 @@ public class TIFFImageReader extends ImageReaderBase {
out.writeShort(JPEG.SOI);
out.writeShort(JPEG.SOF0);
out.writeShort(2 + 6 + 3 * bands); // SOF0 len
out.writeByte(8); // bits TODO: Consult bands/transfer type or BitsPerSample for 12/16 bits support
out.writeByte(8); // bits TODO: Consult raster/transfer type or BitsPerSample for 12/16 bits support
out.writeShort(stripTileHeight); // height
out.writeShort(stripTileWidth); // width
out.writeByte(bands); // Number of components
@@ -1447,7 +1453,7 @@ public class TIFFImageReader extends ImageReaderBase {
return raster;
}
return raster.createChild(rect.x / xSub, 0, rect.width / xSub, 1, 0, 0, bands);
return raster.createChild((rect.x + xSub - 1) / xSub, 0, (rect.width + xSub - 1) / xSub, 1, 0, 0, bands);
}
private WritableRaster clipToRect(final WritableRaster raster, final Rectangle rect, final int[] bands) {
@@ -1497,12 +1503,12 @@ public class TIFFImageReader extends ImageReaderBase {
// Subsample horizontal
if (xSub != 1) {
for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + srcRegion.width) / xSub) * numBands; x += numBands) {
for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + colsInTile) / xSub) * numBands; x += numBands) {
System.arraycopy(rowDataByte, x * xSub, rowDataByte, x, numBands);
}
}
destChannel.setDataElements(startCol, (row - srcRegion.y) / ySub, srcChannel);
destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel);
}
// Else skip data
}
@@ -1540,12 +1546,12 @@ public class TIFFImageReader extends ImageReaderBase {
// Subsample horizontal
if (xSub != 1) {
for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + srcRegion.width) / xSub) * numBands; x += numBands) {
for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + colsInTile) / xSub) * numBands; x += numBands) {
System.arraycopy(rowDataShort, x * xSub, rowDataShort, x, numBands);
}
}
destChannel.setDataElements(startCol, row - srcRegion.y, srcChannel);
destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel);
// TODO: Possible speedup ~30%!:
// raster.setDataElements(startCol, row - srcRegion.y, colsInTile, 1, rowDataShort);
}
@@ -1577,12 +1583,12 @@ public class TIFFImageReader extends ImageReaderBase {
// Subsample horizontal
if (xSub != 1) {
for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + srcRegion.width) / xSub) * numBands; x += numBands) {
for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + colsInTile) / xSub) * numBands; x += numBands) {
System.arraycopy(rowDataInt, x * xSub, rowDataInt, x, numBands);
}
}
destChannel.setDataElements(startCol, row - srcRegion.y, srcChannel);
destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel);
}
// Else skip data
}
@@ -1907,22 +1913,34 @@ public class TIFFImageReader extends ImageReaderBase {
case TIFFBaseline.COMPRESSION_PACKBITS:
return new DecoderStream(stream, new PackBitsDecoder(), 1024);
case TIFFExtension.COMPRESSION_LZW:
// NOTE: Needs large buffer for compatibility with certain encoders
return new DecoderStream(stream, LZWDecoder.create(LZWDecoder.isOldBitReversedStream(stream)), Math.max(width * bands, 4096));
case TIFFExtension.COMPRESSION_ZLIB:
// TIFFphotoshop.pdf (aka TIFF specification, supplement 2) says ZLIB (8) and DEFLATE (32946) algorithms are identical
case TIFFExtension.COMPRESSION_DEFLATE:
// TIFF specification, supplement 2 says ZLIB (8) and DEFLATE (32946) algorithms are identical
return new InflaterInputStream(stream, new Inflater(), 1024);
case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE:
return new CCITTFaxDecoderStream(stream, width, compression, getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, 1),0L);
case TIFFExtension.COMPRESSION_CCITT_T4:
return new CCITTFaxDecoderStream(stream, width, compression, getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, 1),getValueAsLongWithDefault(TIFF.TAG_GROUP3OPTIONS, 0L));
case TIFFExtension.COMPRESSION_CCITT_T6:
return new CCITTFaxDecoderStream(stream, width, compression, getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, 1),getValueAsLongWithDefault(TIFF.TAG_GROUP4OPTIONS, 0L));
return new CCITTFaxDecoderStream(stream, width, compression, getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, 1), getCCITTOptions(compression));
default:
throw new IllegalArgumentException("Unsupported TIFF compression: " + compression);
}
}
private long getCCITTOptions(final int compression) throws IIOException {
switch (compression) {
case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE:
return 0L;
case TIFFExtension.COMPRESSION_CCITT_T4:
return getValueAsLongWithDefault(TIFF.TAG_GROUP3OPTIONS, 0L);
case TIFFExtension.COMPRESSION_CCITT_T6:
return getValueAsLongWithDefault(TIFF.TAG_GROUP4OPTIONS, 0L);
default:
throw new IllegalArgumentException("No CCITT options for compression: " + compression);
}
}
private InputStream createUnpredictorStream(final int predictor, final int width, final int samplesPerPixel, final int bitsPerSample, final InputStream stream, final ByteOrder byteOrder) throws IOException {
switch (predictor) {
case TIFFBaseline.PREDICTOR_NONE:
@@ -2155,6 +2173,7 @@ public class TIFFImageReader extends ImageReaderBase {
// param.setSourceRegion(new Rectangle(3, 3, 9, 9));
// param.setDestinationOffset(new Point(50, 150));
// param.setSourceSubsampling(2, 2, 0, 0);
// param.setSourceSubsampling(3, 3, 0, 0);
BufferedImage image = reader.read(imageNo, param);
System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms");
@@ -2171,6 +2190,20 @@ public class TIFFImageReader extends ImageReaderBase {
System.err.println("image: " + image);
// int w = image.getWidth();
// int h = image.getHeight();
//
// int newW = h;
// int newH = w;
//
// AffineTransform xform = AffineTransform.getTranslateInstance((newW - w) / 2.0, (newH - h) / 2.0);
// xform.concatenate(AffineTransform.getQuadrantRotateInstance(3, w / 2.0, h / 2.0));
// AffineTransformOp op = new AffineTransformOp(xform, null);
//
// image = op.filter(image, null);
//
// System.err.println("image: " + image);
//
// File tempFile = File.createTempFile("lzw-", ".bin");
// byte[] data = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
// FileOutputStream stream = new FileOutputStream(tempFile);

View File

@@ -27,7 +27,6 @@ package com.twelvemonkeys.imageio.plugins.tiff;/*
*/
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest;
import org.junit.Ignore;
import org.junit.Test;
import javax.imageio.ImageIO;
@@ -442,4 +441,74 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
assertTrue("no correct guess for PhotometricInterpretation: " + results[i], foundWarning.get());
}
}
@Test
public void testReadWithSubsampleParamPixelsJPEG() throws IOException {
// Tiled "new style" JPEG
ImageReader reader = createReader();
TestData data = new TestData(getClassLoaderResource("/tiff/quad-jpeg.tif"), new Dimension(512, 384));
reader.setInput(data.getInputStream());
ImageReadParam param = reader.getDefaultReadParam();
BufferedImage image = null;
BufferedImage subsampled = null;
try {
image = reader.read(0, param);
param.setSourceSubsampling(2, 2, 0, 0);
subsampled = reader.read(0, param);
}
catch (IOException e) {
failBecause("Image could not be read", e);
}
assertSubsampledImageDataEquals("Subsampled image data does not match expected", image, subsampled, param);
}
@Test
public void testReadWithSubsampleParamPixelsOldJPEG() throws IOException {
ImageReader reader = createReader();
TestData data = new TestData(getClassLoaderResource("/tiff/smallliz.tif"), new Dimension(160, 160));
reader.setInput(data.getInputStream());
ImageReadParam param = reader.getDefaultReadParam();
BufferedImage image = null;
BufferedImage subsampled = null;
try {
image = reader.read(0, param);
param.setSourceSubsampling(2, 2, 0, 0);
subsampled = reader.read(0, param);
}
catch (IOException e) {
failBecause("Image could not be read", e);
}
assertSubsampledImageDataEquals("Subsampled image data does not match expected", image, subsampled, param);
}
@Test
public void testReadWithSubsampleParamPixelsTiled() throws IOException {
ImageReader reader = createReader();
TestData data = new TestData(getClassLoaderResource("/tiff/cramps-tile.tif"), new Dimension(800, 607));
reader.setInput(data.getInputStream());
ImageReadParam param = reader.getDefaultReadParam();
BufferedImage image = null;
BufferedImage subsampled = null;
try {
image = reader.read(0, param);
param.setSourceSubsampling(2, 2, 0, 0);
subsampled = reader.read(0, param);
}
catch (IOException e) {
failBecause("Image could not be read", e);
}
assertSubsampledImageDataEquals("Subsampled image data does not match expected", image, subsampled, param);
}
}