#681: Fix for little-endian "packed" USHORT types + rewritten stream handling

This commit is contained in:
Harald Kuhr 2022-06-03 19:23:50 +02:00
parent 84a8ceeb93
commit bcb87c09d2
3 changed files with 95 additions and 77 deletions

View File

@ -49,20 +49,24 @@ import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
import com.twelvemonkeys.imageio.metadata.xmp.XMPReader; import com.twelvemonkeys.imageio.metadata.xmp.XMPReader;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.imageio.stream.DirectImageInputStream;
import com.twelvemonkeys.imageio.stream.SubImageInputStream; import com.twelvemonkeys.imageio.stream.SubImageInputStream;
import com.twelvemonkeys.imageio.util.IIOUtil; import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.imageio.util.ProgressListenerBase; import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.io.FastByteArrayOutputStream; import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.io.FileUtil; import com.twelvemonkeys.io.FileUtil;
import com.twelvemonkeys.io.LittleEndianDataInputStream;
import com.twelvemonkeys.io.enc.DecoderStream; import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder; import com.twelvemonkeys.io.enc.PackBitsDecoder;
import com.twelvemonkeys.lang.StringUtil; import com.twelvemonkeys.lang.StringUtil;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import javax.imageio.*; import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.event.IIOReadWarningListener; import javax.imageio.event.IIOReadWarningListener;
import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.metadata.IIOMetadataNode;
@ -70,16 +74,25 @@ import javax.imageio.plugins.jpeg.JPEGImageReadParam;
import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.awt.*; import java.awt.*;
import java.awt.color.CMMException; import java.awt.color.*;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.image.*; import java.awt.image.*;
import java.io.*; import java.io.ByteArrayInputStream;
import java.io.DataInput;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.zip.Inflater; import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream; import java.util.zip.InflaterInputStream;
@ -940,7 +953,7 @@ public final class TIFFImageReader extends ImageReaderBase {
final int compression = getValueAsIntWithDefault(TIFF.TAG_COMPRESSION, TIFFBaseline.COMPRESSION_NONE); final int compression = getValueAsIntWithDefault(TIFF.TAG_COMPRESSION, TIFFBaseline.COMPRESSION_NONE);
final int predictor = getValueAsIntWithDefault(TIFF.TAG_PREDICTOR, 1); final int predictor = getValueAsIntWithDefault(TIFF.TAG_PREDICTOR, 1);
final int planarConfiguration = getValueAsIntWithDefault(TIFF.TAG_PLANAR_CONFIGURATION, TIFFBaseline.PLANARCONFIG_CHUNKY); final int planarConfiguration = getValueAsIntWithDefault(TIFF.TAG_PLANAR_CONFIGURATION, TIFFBaseline.PLANARCONFIG_CHUNKY);
final int numBands = planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR ? 1 : rawType.getNumBands(); final int samplesInTile = planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR ? 1 : rawType.getNumBands();
// NOTE: We handle strips as tiles of tileWidth == width by tileHeight == rowsPerStrip // NOTE: We handle strips as tiles of tileWidth == width by tileHeight == rowsPerStrip
// Strips are top/down, tiles are left/right, top/down // Strips are top/down, tiles are left/right, top/down
@ -1061,8 +1074,8 @@ public final class TIFFImageReader extends ImageReaderBase {
int fillOrder = getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, TIFFBaseline.FILL_LEFT_TO_RIGHT); int fillOrder = getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, TIFFBaseline.FILL_LEFT_TO_RIGHT);
int bitsPerSample = getBitsPerSample(); int bitsPerSample = getBitsPerSample();
boolean needsBitPadding = bitsPerSample > 16 && bitsPerSample % 16 != 0 || bitsPerSample > 8 && bitsPerSample % 8 != 0 boolean needsBitPadding = bitsPerSample > 16 && bitsPerSample % 16 != 0 || bitsPerSample > 8 && bitsPerSample % 8 != 0
|| numBands == 1 && bitsPerSample == 6 // IndexColorModel or Gray || samplesInTile == 1 && bitsPerSample == 6 // IndexColorModel or Gray
|| numBands == 3 && (bitsPerSample == 2 || bitsPerSample == 4); // RGB/YCbCr/etc. || samplesInTile == 3 && (bitsPerSample == 2 || bitsPerSample == 4); // RGB/YCbCr/etc.
boolean needsAdapter = compression != TIFFBaseline.COMPRESSION_NONE || fillOrder != TIFFBaseline.FILL_LEFT_TO_RIGHT boolean needsAdapter = compression != TIFFBaseline.COMPRESSION_NONE || fillOrder != TIFFBaseline.FILL_LEFT_TO_RIGHT
|| interpretation == TIFFExtension.PHOTOMETRIC_YCBCR || needsBitPadding; || interpretation == TIFFExtension.PHOTOMETRIC_YCBCR || needsBitPadding;
@ -1076,12 +1089,24 @@ public final class TIFFImageReader extends ImageReaderBase {
for (int b = 0; b < bands; b++) { for (int b = 0; b < bands; b++) {
int i = b * tilesDown * tilesAcross + y * tilesAcross + x; int i = b * tilesDown * tilesAcross + y * tilesAcross + x;
// Clip the stripTile rowRaster to not exceed the srcRegion
clip.width = Math.min(colsInTile, srcRegion.width);
Raster clippedRow = clipRowToRect(rowRaster, clip,
param != null ? param.getSourceBands() : null,
param != null ? param.getSourceXSubsampling() : 1);
imageInput.seek(stripTileOffsets[i]); imageInput.seek(stripTileOffsets[i]);
DataInput input; ImageInputStream input;
if (!needsAdapter) { if (!needsAdapter) {
// No need for transformation, fast-forward // No need for transformation, fast-forward
input = imageInput; long byteCount = stripTileHeight * (((long) stripTileWidth * bitsPerSample * samplesInTile + 7L) / 8L);
if (stripTileByteCounts != null && stripTileByteCounts[i] < byteCount) {
processWarningOccurred("strip/tileByteCount < required ( " + byteCount + "):" + stripTileByteCounts[i]);
}
input = new SubImageInputStream(imageInput, byteCount);
} }
else { else {
InputStream adapter = stripTileByteCounts != null InputStream adapter = stripTileByteCounts != null
@ -1094,31 +1119,34 @@ public final class TIFFImageReader extends ImageReaderBase {
int compressedStripTileWidth = planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR && b > 0 && yCbCrSubsampling != null int compressedStripTileWidth = planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR && b > 0 && yCbCrSubsampling != null
? ((stripTileWidth + yCbCrSubsampling[0] - 1) / yCbCrSubsampling[0]) ? ((stripTileWidth + yCbCrSubsampling[0] - 1) / yCbCrSubsampling[0])
: stripTileWidth; : stripTileWidth;
adapter = createDecompressorStream(compression, compressedStripTileWidth, numBands, adapter); adapter = createDecompressorStream(compression, compressedStripTileWidth, samplesInTile, adapter);
adapter = createUnpredictorStream(predictor, compressedStripTileWidth, numBands, bitsPerSample, adapter, imageInput.getByteOrder()); adapter = createUnpredictorStream(predictor, compressedStripTileWidth, samplesInTile, bitsPerSample, adapter, imageInput.getByteOrder());
adapter = createYCbCrUpsamplerStream(interpretation, planarConfiguration, b, rowRaster.getTransferType(), yCbCrSubsampling, yCbCrPos, colsInTile, adapter, imageInput.getByteOrder()); adapter = createYCbCrUpsamplerStream(interpretation, planarConfiguration, b, rowRaster.getTransferType(), yCbCrSubsampling, yCbCrPos, colsInTile, adapter, imageInput.getByteOrder());
if (needsBitPadding) { if (needsBitPadding) {
// We'll pad "odd" bitsPerSample streams to the smallest data type (byte/short/int) larger than the input // We'll pad "odd" bitsPerSample streams to the smallest data type (byte/short/int) larger than the input
adapter = bitsPerSample < 8 adapter = bitsPerSample < 8
? new BitPaddingStream(adapter, 1, numBands * bitsPerSample, colsInTile, imageInput.getByteOrder()) ? new BitPaddingStream(adapter, 1, samplesInTile * bitsPerSample, colsInTile, imageInput.getByteOrder())
: new BitPaddingStream(adapter, numBands, bitsPerSample, colsInTile, imageInput.getByteOrder()); : new BitPaddingStream(adapter, samplesInTile, bitsPerSample, colsInTile, imageInput.getByteOrder());
} }
// According to the spec, short/long/etc should follow order of containing stream // According to the spec, short/long/etc should follow order of containing stream
input = imageInput.getByteOrder() == ByteOrder.BIG_ENDIAN input = new DirectImageInputStream(adapter);
? new DataInputStream(adapter)
: new LittleEndianDataInputStream(adapter);
} }
// Clip the stripTile rowRaster to not exceed the srcRegion try (ImageInputStream stream = input) {
clip.width = Math.min(colsInTile, srcRegion.width); // Temporary set byte order to match the color model for USHORT_4444/555/565/etc...
Raster clippedRow = clipRowToRect(rowRaster, clip, if (rawType.getColorModel() instanceof DirectColorModel && rawType.getColorModel().getTransferType() == DataBuffer.TYPE_USHORT) {
param != null ? param.getSourceBands() : null, stream.setByteOrder(ByteOrder.BIG_ENDIAN);
param != null ? param.getSourceXSubsampling() : 1); }
else {
// ...otherwise keep the order from the parent stream
stream.setByteOrder(imageInput.getByteOrder());
}
// Read a full strip/tile // Read a full strip/tile
readStripTileData(clippedRow, srcRegion, xSub, ySub, b, numBands, interpretation, destRaster, col, srcRow, colsInTile, rowsInTile, input); readStripTileData(clippedRow, srcRegion, xSub, ySub, b, samplesInTile, interpretation, destRaster, col, srcRow, colsInTile, rowsInTile, input);
}
} }
// Need to do color normalization after reading all bands for planar // Need to do color normalization after reading all bands for planar
@ -1221,10 +1249,10 @@ public final class TIFFImageReader extends ImageReaderBase {
// TODO: Refactor + duplicate this for all JPEG-in-TIFF cases // TODO: Refactor + duplicate this for all JPEG-in-TIFF cases
switch (raster.getTransferType()) { switch (raster.getTransferType()) {
case DataBuffer.TYPE_BYTE: case DataBuffer.TYPE_BYTE:
normalizeColor(interpretation, numBands, ((DataBufferByte) raster.getDataBuffer()).getData()); normalizeColor(interpretation, samplesInTile, ((DataBufferByte) raster.getDataBuffer()).getData());
break; break;
case DataBuffer.TYPE_USHORT: case DataBuffer.TYPE_USHORT:
normalizeColor(interpretation, numBands, ((DataBufferUShort) raster.getDataBuffer()).getData()); normalizeColor(interpretation, samplesInTile, ((DataBufferUShort) raster.getDataBuffer()).getData());
break; break;
default: default:
throw new IllegalStateException("Unsupported transfer type: " + raster.getTransferType()); throw new IllegalStateException("Unsupported transfer type: " + raster.getTransferType());
@ -1387,7 +1415,7 @@ public final class TIFFImageReader extends ImageReaderBase {
// Otherwise, it's likely CMYK or some other interpretation we don't need to convert. // Otherwise, it's likely CMYK or some other interpretation we don't need to convert.
// We'll have to use readAsRaster and later apply color space conversion ourselves // We'll have to use readAsRaster and later apply color space conversion ourselves
Raster raster = jpegReader.readRaster(0, jpegParam); Raster raster = jpegReader.readRaster(0, jpegParam);
normalizeColor(interpretation, numBands, ((DataBufferByte) raster.getDataBuffer()).getData()); normalizeColor(interpretation, samplesInTile, ((DataBufferByte) raster.getDataBuffer()).getData());
destination.getRaster().setDataElements(offset.x, offset.y, raster); destination.getRaster().setDataElements(offset.x, offset.y, raster);
} }
} }
@ -1537,7 +1565,7 @@ public final class TIFFImageReader extends ImageReaderBase {
// Otherwise, it's likely CMYK or some other interpretation we don't need to convert. // Otherwise, it's likely CMYK or some other interpretation we don't need to convert.
// We'll have to use readAsRaster and later apply color space conversion ourselves // We'll have to use readAsRaster and later apply color space conversion ourselves
Raster raster = jpegReader.readRaster(0, jpegParam); Raster raster = jpegReader.readRaster(0, jpegParam);
normalizeColor(interpretation, numBands, ((DataBufferByte) raster.getDataBuffer()).getData()); normalizeColor(interpretation, samplesInTile, ((DataBufferByte) raster.getDataBuffer()).getData());
destination.getRaster().setDataElements(offset.x, offset.y, raster); destination.getRaster().setDataElements(offset.x, offset.y, raster);
} }
} }
@ -1787,7 +1815,7 @@ public final class TIFFImageReader extends ImageReaderBase {
private IIOMetadataNode getNode(final IIOMetadataNode parent, final String tagName) { private IIOMetadataNode getNode(final IIOMetadataNode parent, final String tagName) {
NodeList nodes = parent.getElementsByTagName(tagName); NodeList nodes = parent.getElementsByTagName(tagName);
return nodes != null && nodes.getLength() >= 1 ? (IIOMetadataNode) nodes.item(0) : null; return nodes.getLength() >= 1 ? (IIOMetadataNode) nodes.item(0) : null;
} }
private ImageReader createJPEGDelegate() throws IOException { private ImageReader createJPEGDelegate() throws IOException {
@ -1893,7 +1921,7 @@ public final class TIFFImageReader extends ImageReaderBase {
private void readStripTileData(final Raster tileRowRaster, final Rectangle srcRegion, final int xSub, final int ySub, private void readStripTileData(final Raster tileRowRaster, final Rectangle srcRegion, final int xSub, final int ySub,
final int band, final int numBands, final int interpretation, final int band, final int numBands, final int interpretation,
final WritableRaster raster, final int startCol, final int startRow, final WritableRaster raster, final int startCol, final int startRow,
final int colsInTile, final int rowsInTile, final DataInput input) final int colsInTile, final int rowsInTile, final ImageInputStream input)
throws IOException { throws IOException {
DataBuffer dataBuffer = tileRowRaster.getDataBuffer(); DataBuffer dataBuffer = tileRowRaster.getDataBuffer();
@ -1951,7 +1979,7 @@ public final class TIFFImageReader extends ImageReaderBase {
break; // We're done with this tile break; // We're done with this tile
} }
readFully(input, rowDataShort); input.readFully(rowDataShort, 0, rowDataShort.length);
if (row >= srcRegion.y) { if (row >= srcRegion.y) {
normalizeColor(interpretation, numBands, rowDataShort); normalizeColor(interpretation, numBands, rowDataShort);
@ -1979,7 +2007,7 @@ public final class TIFFImageReader extends ImageReaderBase {
break; // We're done with this tile break; // We're done with this tile
} }
readFully(input, rowDataInt); input.readFully(rowDataInt, 0, rowDataInt.length);
if (row >= srcRegion.y) { if (row >= srcRegion.y) {
normalizeColor(interpretation, numBands, rowDataInt); normalizeColor(interpretation, numBands, rowDataInt);
@ -2008,11 +2036,11 @@ public final class TIFFImageReader extends ImageReaderBase {
} }
if (needsWidening) { if (needsWidening) {
readFully(input, rowDataShort); input.readFully(rowDataShort, 0, rowDataShort.length);
toFloat(rowDataShort, rowDataFloat); toFloat(rowDataShort, rowDataFloat);
} }
else { else {
readFully(input, rowDataFloat); input.readFully(rowDataFloat, 0, rowDataFloat.length);
} }
if (row >= srcRegion.y) { if (row >= srcRegion.y) {
@ -2055,45 +2083,6 @@ public final class TIFFImageReader extends ImageReaderBase {
} }
} }
// TODO: Candidate util method (with off/len + possibly byte order)
private void readFully(final DataInput input, final float[] rowDataFloat) throws IOException {
if (input instanceof ImageInputStream) {
ImageInputStream imageInputStream = (ImageInputStream) input;
imageInputStream.readFully(rowDataFloat, 0, rowDataFloat.length);
}
else {
for (int k = 0; k < rowDataFloat.length; k++) {
rowDataFloat[k] = input.readFloat();
}
}
}
// TODO: Candidate util method (with off/len + possibly byte order)
private void readFully(final DataInput input, final int[] rowDataInt) throws IOException {
if (input instanceof ImageInputStream) {
ImageInputStream imageInputStream = (ImageInputStream) input;
imageInputStream.readFully(rowDataInt, 0, rowDataInt.length);
}
else {
for (int k = 0; k < rowDataInt.length; k++) {
rowDataInt[k] = input.readInt();
}
}
}
// TODO: Candidate util method (with off/len + possibly byte order)
private void readFully(final DataInput input, final short[] rowDataShort) throws IOException {
if (input instanceof ImageInputStream) {
ImageInputStream imageInputStream = (ImageInputStream) input;
imageInputStream.readFully(rowDataShort, 0, rowDataShort.length);
}
else {
for (int k = 0; k < rowDataShort.length; k++) {
rowDataShort[k] = input.readShort();
}
}
}
private void normalizeColorPlanar(int photometricInterpretation, WritableRaster raster) throws IIOException { private void normalizeColorPlanar(int photometricInterpretation, WritableRaster raster) throws IIOException {
// TODO: Other transfer types? // TODO: Other transfer types?
if (raster.getTransferType() != DataBuffer.TYPE_BYTE) { if (raster.getTransferType() != DataBuffer.TYPE_BYTE) {

View File

@ -44,7 +44,7 @@ import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.awt.*; import java.awt.*;
import java.awt.color.ColorSpace; import java.awt.color.*;
import java.awt.image.*; import java.awt.image.*;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteOrder; import java.nio.ByteOrder;
@ -56,7 +56,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.AdditionalMatchers.and; import static org.mockito.AdditionalMatchers.and;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ -892,6 +895,32 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
assertSubsampledImageDataEquals("Subsampled image data does not match expected", image, subsampled, param); assertSubsampledImageDataEquals("Subsampled image data does not match expected", image, subsampled, param);
} }
@Test
public void testReadLittleEndian4444ARGB() throws IOException {
ImageReader reader = createReader();
try (ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource("/tiff/little-endian-rgba-4444.tiff"))) {
reader.setInput(stream);
BufferedImage image = null;
try {
image = reader.read(0);
}
catch (IOException e) {
failBecause("Image could not be read", e);
}
assertNotNull(image);
assertEquals(589, image.getWidth());
assertEquals(340, image.getHeight());
assertRGBEquals("Red", 0xffff1111, image.getRGB(124, 42), 4);
assertRGBEquals("Green", 0xff66ee11, image.getRGB(476, 100), 4);
assertRGBEquals("Yellow", 0xffffff00, image.getRGB(312, 186), 4);
assertRGBEquals("Blue", 0xff1155dd, image.getRGB(366, 192), 4);
}
}
@Test @Test
public void testReadUnsupported() throws IOException { public void testReadUnsupported() throws IOException {
ImageReader reader = createReader(); ImageReader reader = createReader();