Further clean-up

This commit is contained in:
Harald Kuhr 2023-11-13 19:35:58 +01:00
parent ee424583c4
commit 3623a7c5dd

View File

@ -1176,322 +1176,25 @@ public final class TIFFImageReader extends ImageReaderBase {
case TIFFCustom.COMPRESSION_WEBP: case TIFFCustom.COMPRESSION_WEBP:
case TIFFCustom.COMPRESSION_JBIG: case TIFFCustom.COMPRESSION_JBIG:
case TIFFCustom.COMPRESSION_JPEG2000: case TIFFCustom.COMPRESSION_JPEG2000:
readUsingDelegate(imageIndex, compression, interpretation, width, height, tilesAcross, tilesDown, stripTileWidth, stripTileHeight, srcRegion, stripTileOffsets, stripTileByteCounts, param, destination, samplesInTile); readUsingDelegate(imageIndex, compression, interpretation, width, height, tilesAcross, tilesDown, stripTileWidth, stripTileHeight, srcRegion,
new PlainTileStreamFactory(stripTileOffsets, stripTileByteCounts), param, destination, samplesInTile);
break; break;
case TIFFExtension.COMPRESSION_OLD_JPEG: { case TIFFExtension.COMPRESSION_OLD_JPEG:
// JPEG ('old-style' JPEG, later overridden in Technote2) boolean interChangeFormat = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT, -1) >= 0;
// http://www.remotesensing.org/libtiff/TIFFTechNote2.html
// TODO: Rewrite to use readUsingDelegate with special case handling inside OldJPEGTileDecoder
int srcRow = 0; if (!interChangeFormat) {
Boolean needsCSConversion = null;
// 512/JPEGProc: 1=Baseline, 14=Lossless (with Huffman coding), no default, although 1 is assumed if absent
int mode = getValueAsIntWithDefault(TIFF.TAG_OLD_JPEG_PROC, TIFFExtension.JPEG_PROC_BASELINE);
switch (mode) {
case TIFFExtension.JPEG_PROC_BASELINE:
case TIFFExtension.JPEG_PROC_LOSSLESS:
break; // Supported
default:
throw new IIOException("Unknown TIFF JPEGProcessingMode value: " + mode);
}
ImageReader jpegReader = createJPEGDelegate();
ImageReadParam jpegParam = jpegReader.getDefaultReadParam();
// 513/JPEGInterchangeFormat (may be absent or 0)
int jpegOffset = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT, -1);
// 514/JPEGInterchangeFormatLength (may be absent, or incorrect)
// TODO: We used to issue a warning if the value was incorrect, should we still do that?
int jpegLength = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, -1);
// TODO: 515/JPEGRestartInterval (may be absent)
// Currently ignored (for lossless only)
// 517/JPEGLosslessPredictors
// 518/JPEGPointTransforms
if (jpegOffset > 0) {
if (currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_Q_TABLES) != null
|| currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_DC_TABLES) != null
|| currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_AC_TABLES) != null) {
processWarningOccurred("Old-style JPEG compressed TIFF with JPEGInterchangeFormat encountered. Ignoring JPEG tables.");
}
else {
processWarningOccurred("Old-style JPEG compressed TIFF with JPEGInterchangeFormat encountered.");
}
imageInput.seek(jpegOffset);
// NOTE: Some known TIFF encoder encodes bad JPEGInterchangeFormat tags,
// but has the correct offset to the JPEG stream in the StripOffsets tag.
long realJPEGOffset = jpegOffset;
short expectedSOI = (short) (imageInput.readByte() << 8 | imageInput.readByte());
if (expectedSOI != (short) JPEG.SOI) {
if (stripTileOffsets != null && stripTileOffsets.length == 1) {
imageInput.seek(stripTileOffsets[0]);
expectedSOI = (short) (imageInput.readByte() << 8 | imageInput.readByte());
if (expectedSOI == (short) JPEG.SOI) {
realJPEGOffset = stripTileOffsets[0];
}
}
if (realJPEGOffset != jpegOffset) {
processWarningOccurred("Incorrect JPEGInterchangeFormat tag, using StripOffsets/TileOffsets instead.");
}
else {
processWarningOccurred("Incorrect JPEGInterchangeFormat tag encountered (not a valid SOI marker).");
// We'll fail below, but we don't need to handle this especially
}
}
byte[] jpegHeader;
if (stripTileOffsets == null || stripTileOffsets.length == 1 && realJPEGOffset == stripTileOffsets[0]) {
// In this case, we'll just read everything as a single tile
jpegHeader = new byte[0];
}
else {
// Wang TIFF weirdness, see http://www.eztwain.com/wangtiff.htm
// If the first tile stream starts with SOS, we'll correct offset/length
imageInput.seek(stripTileOffsets[0]);
if ((short) (imageInput.readByte() << 8 | imageInput.readByte()) == (short) JPEG.SOS) {
processWarningOccurred("Incorrect StripOffsets/TileOffsets, points to SOS marker, ignoring offsets/byte counts.");
int len = 2 + (imageInput.readUnsignedByte() << 8 | imageInput.readUnsignedByte());
stripTileOffsets[0] += len;
stripTileByteCounts[0] -= len;
}
// We'll prepend each tile with a JFIF "header" (SOI...SOS)
imageInput.seek(realJPEGOffset);
jpegHeader = new byte[Math.max(0, (int) (stripTileOffsets[0] - realJPEGOffset))];
imageInput.readFully(jpegHeader);
}
// In case of single tile, make sure we read the entire JFIF stream
if (stripTileByteCounts != null && stripTileByteCounts.length == 1 && stripTileByteCounts[0] < jpegLength) {
processWarningOccurred("Incorrect StripByteCounts/TileByteCounts for single tile, using JPEGInterchangeFormatLength instead.");
stripTileByteCounts[0] = jpegLength;
}
// Read data
processImageStarted(imageIndex);
for (int y = 0; y < tilesDown; y++) {
int col = 0;
int rowsInTile = Math.min(stripTileHeight, height - srcRow);
for (int x = 0; x < tilesAcross; x++) {
int colsInTile = Math.min(stripTileWidth, width - col);
int i = y * tilesAcross + x;
// Read only tiles that lies within region
if (new Rectangle(col, srcRow, colsInTile, rowsInTile).intersects(srcRegion)) {
int len = stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Integer.MAX_VALUE;
imageInput.seek(stripTileOffsets != null ? stripTileOffsets[i] : realJPEGOffset);
try (ImageInputStream stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(asList(
new ByteArrayInputStream(jpegHeader),
createStreamAdapter(imageInput, len),
new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI
))))) {
jpegReader.setInput(stream);
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
jpegParam.setSourceSubsampling(xSub, ySub, 0, 0);
Point offset = new Point(col - srcRegion.x, srcRow - srcRegion.y);
if (needsCSConversion == null) {
needsCSConversion = needsCSConversion(compression, interpretation, readJPEGMetadataSafe(jpegReader));
}
if (!needsCSConversion) {
jpegParam.setDestinationOffset(offset);
jpegParam.setDestination(destination);
jpegReader.read(0, jpegParam);
}
else {
// 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
Raster raster = jpegReader.readRaster(0, jpegParam);
normalizeColor(interpretation, samplesInTile, ((DataBufferByte) raster.getDataBuffer()).getData());
destination.getRaster().setDataElements(offset.x, offset.y, raster);
}
}
}
if (abortRequested()) {
break;
}
col += colsInTile;
}
processImageProgress(100f * srcRow / height);
if (abortRequested()) {
processReadAborted();
break;
}
srcRow += rowsInTile;
}
}
else {
// The hard way: Read tables and re-create a full JFIF stream
processWarningOccurred("Old-style JPEG compressed TIFF without JPEGInterchangeFormat encountered. Attempting to re-create JFIF stream."); processWarningOccurred("Old-style JPEG compressed TIFF without JPEGInterchangeFormat encountered. Attempting to re-create JFIF stream.");
// 519/JPEGQTables
// 520/JPEGDCTables
// 521/JPEGACTables
// These fields were originally intended to point to a list of offsets to the quantization tables, one per
// component. Each table consists of 64 BYTES (one for each DCT coefficient in the 8x8 block). The
// quantization tables are stored in zigzag order, and are compatible with the quantization tables
// usually found in a JPEG stream DQT marker.
// The original specification strongly recommended that, within the TIFF file, each component be
// assigned separate tables, and labelled this field as mandatory whenever the JPEGProc field specifies
// a DCT-based process.
// We've seen old-style JPEG in TIFF files where some or all Table offsets, contained the JPEGQTables,
// JPEGDCTables, and JPEGACTables tags are incorrect values beyond EOF. However, these files do always
// seem to contain a useful JPEGInterchangeFormat tag. Therefore, we recommend a careful attempt to read
// the Tables tags only as a last resort, if no table data is found in a JPEGInterchangeFormat stream.
// TODO: If any of the q/dc/ac tables are equal (or have same offset, even if "spec" violation),
// use only the first occurrence, and update selectors in SOF0 and SOS
long[] qTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_Q_TABLES, "JPEGQTables", true);
byte[][] qTables = new byte[qTablesOffsets.length][64];
for (int j = 0; j < qTables.length; j++) {
imageInput.seek(qTablesOffsets[j]);
imageInput.readFully(qTables[j]);
}
long[] dcTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_DC_TABLES, "JPEGDCTables", true);
byte[][] dcTables = new byte[dcTablesOffsets.length][];
for (int j = 0; j < dcTables.length; j++) {
imageInput.seek(dcTablesOffsets[j]);
byte[] lengths = new byte[16];
imageInput.readFully(lengths);
int length = 0;
for (int i = 0; i < 16; i++) {
length += lengths[i] & 0xff;
}
dcTables[j] = new byte[16 + length];
System.arraycopy(lengths, 0, dcTables[j], 0, 16);
imageInput.readFully(dcTables[j], 16, length);
}
long[] acTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_AC_TABLES, "JPEGACTables", true);
byte[][] acTables = new byte[acTablesOffsets.length][];
for (int j = 0; j < acTables.length; j++) {
imageInput.seek(acTablesOffsets[j]);
byte[] lengths = new byte[16];
imageInput.readFully(lengths);
int length = 0;
for (int i = 0; i < 16; i++) {
length += lengths[i] & 0xff;
}
acTables[j] = new byte[16 + length];
System.arraycopy(lengths, 0, acTables[j], 0, 16);
imageInput.readFully(acTables[j], 16, length);
}
long[] yCbCrSubSampling = getValueAsLongArray(TIFF.TAG_YCBCR_SUB_SAMPLING, "YCbCrSubSampling", false);
int subsampling = yCbCrSubSampling != null
? (int) ((yCbCrSubSampling[0] & 0xf) << 4 | yCbCrSubSampling[1] & 0xf)
: 0x22;
// Read data
processImageStarted(imageIndex);
for (int y = 0; y < tilesDown; y++) {
int col = 0;
int rowsInTile = Math.min(stripTileHeight, height - srcRow);
for (int x = 0; x < tilesAcross; x++) {
int colsInTile = Math.min(stripTileWidth, width - col);
int i = y * tilesAcross + x;
// Read only tiles that lies within region
if (new Rectangle(col, srcRow, colsInTile, rowsInTile).intersects(srcRegion)) {
int length = stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE;
imageInput.seek(stripTileOffsets[i]);
// If the tile stream starts with SOS...
if (x == 0 && y == 0) {
if ((short) (imageInput.readByte() << 8 | imageInput.readByte()) == (short) JPEG.SOS) {
imageInput.seek(stripTileOffsets[i] + 14); // TODO: Read from SOS length from stream, in case of gray/CMYK
length -= 14;
}
else {
imageInput.seek(stripTileOffsets[i]);
}
}
try (ImageInputStream stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(
asList(
createJFIFStream(destRaster.getNumBands(), stripTileWidth, stripTileHeight, qTables, dcTables, acTables, subsampling),
createStreamAdapter(imageInput, length),
new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI
)
)))) {
jpegReader.setInput(stream);
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
jpegParam.setSourceSubsampling(xSub, ySub, 0, 0);
Point offset = new Point(col - srcRegion.x, srcRow - srcRegion.y);
if (needsCSConversion == null) {
needsCSConversion = needsCSConversion(compression, interpretation, readJPEGMetadataSafe(jpegReader));
}
if (!needsCSConversion) {
jpegParam.setDestinationOffset(offset);
jpegParam.setDestination(destination);
jpegReader.read(0, jpegParam);
}
else {
// 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
Raster raster = jpegReader.readRaster(0, jpegParam);
normalizeColor(interpretation, samplesInTile, ((DataBufferByte) raster.getDataBuffer()).getData());
destination.getRaster().setDataElements(offset.x, offset.y, raster);
}
}
}
if (abortRequested()) {
break;
}
col += colsInTile;
}
processImageProgress(100f * srcRow / height);
if (abortRequested()) {
processReadAborted();
break;
}
srcRow += rowsInTile;
}
} }
// TODO: Perhaps use the jpegTables param to the tiledecoder instead of re-creating a full JFIF stream...
TileStreamFactory tileStreamFactory = interChangeFormat
? new JIFTileStreamFactory(stripTileOffsets, stripTileByteCounts)
: new JPEGTablesStreamFactory(stripTileOffsets, stripTileByteCounts, stripTileWidth, stripTileHeight, destRaster.getNumBands());
readUsingDelegate(imageIndex, compression, interpretation, width, height, tilesAcross, tilesDown, stripTileWidth, stripTileHeight, srcRegion,
tileStreamFactory, param, destination, samplesInTile);
break; break;
}
// Known, but unsupported compression types
case TIFFCustom.COMPRESSION_NEXT: case TIFFCustom.COMPRESSION_NEXT:
case TIFFCustom.COMPRESSION_CCITTRLEW: case TIFFCustom.COMPRESSION_CCITTRLEW:
case TIFFCustom.COMPRESSION_THUNDERSCAN: case TIFFCustom.COMPRESSION_THUNDERSCAN:
@ -1519,10 +1222,8 @@ public final class TIFFImageReader extends ImageReaderBase {
private void readUsingDelegate(int imageIndex, int compression, int interpretation, int width, int height, private void readUsingDelegate(int imageIndex, int compression, int interpretation, int width, int height,
int tilesAcross, int tilesDown, int stripTileWidth, int stripTileHeight, Rectangle srcRegion, int tilesAcross, int tilesDown, int stripTileWidth, int stripTileHeight, Rectangle srcRegion,
long[] stripTileOffsets, long[] stripTileByteCounts, TileStreamFactory factory,
ImageReadParam param, BufferedImage destination, int samplesInTile) throws IOException { ImageReadParam param, BufferedImage destination, int samplesInTile) throws IOException {
// JPEG ('new-style' JPEG)
// Read data // Read data
try (TileDecoder tileDecoder = createTileDecoder(param, compression, interpretation, tilesAcross * tilesDown, samplesInTile)) { try (TileDecoder tileDecoder = createTileDecoder(param, compression, interpretation, tilesAcross * tilesDown, samplesInTile)) {
processImageStarted(imageIndex); // Better yet, would be to delegate read progress here... processImageStarted(imageIndex); // Better yet, would be to delegate read progress here...
@ -1534,7 +1235,6 @@ public final class TIFFImageReader extends ImageReaderBase {
int rowsInTile = Math.min(stripTileHeight, height - row); int rowsInTile = Math.min(stripTileHeight, height - row);
for (int x = 0; x < tilesAcross; x++) { for (int x = 0; x < tilesAcross; x++) {
int i = y * tilesAcross + x;
int colsInTile = Math.min(stripTileWidth, width - col); int colsInTile = Math.min(stripTileWidth, width - col);
// Read only tiles that lies within region // Read only tiles that lies within region
@ -1542,14 +1242,12 @@ public final class TIFFImageReader extends ImageReaderBase {
Rectangle intersection = tileRect.intersection(srcRegion); Rectangle intersection = tileRect.intersection(srcRegion);
if (!intersection.isEmpty()) { if (!intersection.isEmpty()) {
imageInput.seek(stripTileOffsets[i]); int tileIndex = y * tilesAcross + x;
int length = stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE; try (ImageInputStream tileStream = factory.createTileStream(tileIndex)) {
try (ImageInputStream subStream = new SubImageInputStream(imageInput, length)) {
Point destinationOffset = new Point((intersection.x - srcRegion.x) / param.getSourceXSubsampling(), (intersection.y - srcRegion.y) / param.getSourceYSubsampling()); Point destinationOffset = new Point((intersection.x - srcRegion.x) / param.getSourceXSubsampling(), (intersection.y - srcRegion.y) / param.getSourceYSubsampling());
Rectangle sourceRegion = new Rectangle(intersection.x - col, intersection.y - row, intersection.width, intersection.height); Rectangle sourceRegion = new Rectangle(intersection.x - col, intersection.y - row, intersection.width, intersection.height);
tileDecoder.decodeTile(subStream, sourceRegion, destinationOffset, destination); tileDecoder.decodeTile(tileStream, sourceRegion, destinationOffset, destination);
} }
} }
@ -1571,43 +1269,301 @@ public final class TIFFImageReader extends ImageReaderBase {
} }
} }
} }
static abstract class TileStreamFactory {
final long[] stripTileOffsets;
final long[] stripTileByteCounts;
TileStreamFactory(final long[] stripTileOffsets, final long[] stripTileByteCounts) {
this.stripTileOffsets = stripTileOffsets;
this.stripTileByteCounts = stripTileByteCounts;
}
abstract ImageInputStream createTileStream(int tileIndex) throws IOException;
}
final class PlainTileStreamFactory extends TileStreamFactory {
PlainTileStreamFactory(final long[] stripTileOffsets, final long[] stripTileByteCounts) {
super(stripTileOffsets, stripTileByteCounts);
}
@Override
public ImageInputStream createTileStream(final int tileIndex) throws IOException {
imageInput.seek(stripTileOffsets[tileIndex]);
int length = stripTileByteCounts != null ? (int) stripTileByteCounts[tileIndex] : Short.MAX_VALUE;
return new SubImageInputStream(imageInput, length);
}
}
final class JIFTileStreamFactory extends TileStreamFactory {
private final byte[] jpegHeader;
private long realJPEGOffset;
JIFTileStreamFactory(final long[] stripTileOffsets, final long[] stripTileByteCounts) throws IOException {
super(stripTileOffsets, stripTileByteCounts);
// 513/JPEGInterchangeFormat (may be absent or 0)
int jpegOffset = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT, -1);
// 514/JPEGInterchangeFormatLength (may be absent, or incorrect)
int jpegLength = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, -1);
// TODO: 515/JPEGRestartInterval (may be absent)
// Currently ignored (for lossless only)
// 517/JPEGLosslessPredictors
// 518/JPEGPointTransforms
if (currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_Q_TABLES) != null
|| currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_DC_TABLES) != null
|| currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_AC_TABLES) != null) {
processWarningOccurred("Old-style JPEG compressed TIFF with JPEGInterchangeFormat encountered. Ignoring JPEG tables.");
}
else {
processWarningOccurred("Old-style JPEG compressed TIFF with JPEGInterchangeFormat encountered.");
}
imageInput.seek(jpegOffset);
// NOTE: Some known TIFF encoder encodes bad JPEGInterchangeFormat tags,
// but has the correct offset to the JPEG stream in the StripOffsets tag.
realJPEGOffset = jpegOffset;
short expectedSOI = (short) (imageInput.readByte() << 8 | imageInput.readByte());
if (expectedSOI != (short) JPEG.SOI) {
if (stripTileOffsets != null && stripTileOffsets.length == 1) {
imageInput.seek(stripTileOffsets[0]);
expectedSOI = (short) (imageInput.readByte() << 8 | imageInput.readByte());
if (expectedSOI == (short) JPEG.SOI) {
realJPEGOffset = stripTileOffsets[0];
}
}
if (realJPEGOffset != jpegOffset) {
processWarningOccurred("Incorrect JPEGInterchangeFormat tag, using StripOffsets/TileOffsets instead.");
}
else {
processWarningOccurred("Incorrect JPEGInterchangeFormat tag encountered (not a valid SOI marker).");
// We'll fail below, but we don't need to handle this especially
}
}
if (stripTileOffsets == null || stripTileOffsets.length == 1 && realJPEGOffset == stripTileOffsets[0]) {
// In this case, we'll just read everything as a single tile
jpegHeader = new byte[0];
}
else {
// Wang TIFF weirdness, see http://www.eztwain.com/wangtiff.htm
// If the first tile stream starts with SOS, we'll correct offset/length
imageInput.seek(stripTileOffsets[0]);
if ((short) (imageInput.readByte() << 8 | imageInput.readByte()) == (short) JPEG.SOS) {
processWarningOccurred("Incorrect StripOffsets/TileOffsets, points to SOS marker, ignoring offsets/byte counts.");
int len = 2 + (imageInput.readUnsignedByte() << 8 | imageInput.readUnsignedByte());
stripTileOffsets[0] += len;
stripTileByteCounts[0] -= len;
}
// We'll prepend each tile with a JFIF "header" (SOI...SOS)
imageInput.seek(realJPEGOffset);
jpegHeader = new byte[Math.max(0, (int) (stripTileOffsets[0] - realJPEGOffset))];
imageInput.readFully(jpegHeader);
}
// In case of single tile, make sure we read the entire JFIF stream
if (stripTileByteCounts != null && stripTileByteCounts.length == 1 && stripTileByteCounts[0] < jpegLength) {
processWarningOccurred("Incorrect StripByteCounts/TileByteCounts for single tile, using JPEGInterchangeFormatLength instead.");
stripTileByteCounts[0] = jpegLength;
}
}
@Override
public ImageInputStream createTileStream(final int tileIndex) throws IOException {
long length = stripTileByteCounts != null ? stripTileByteCounts[tileIndex] : Integer.MAX_VALUE;
imageInput.seek(stripTileOffsets != null ? stripTileOffsets[tileIndex] : realJPEGOffset);
return ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(asList(
new ByteArrayInputStream(jpegHeader),
createStreamAdapter(imageInput, length),
new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI
))));
}
}
final class JPEGTablesStreamFactory extends TileStreamFactory {
private final int stripTileWidth;
private final int stripTileHeight;
private final int numBands;
private final int subsampling;
private final byte[][] acTables;
private final byte[][] dcTables;
private final byte[][] qTables;
JPEGTablesStreamFactory(final long[] stripTileOffsets, final long[] stripTileByteCounts, final int stripTileWidth, final int stripTileHeight, final int numBands) throws IOException {
super(stripTileOffsets, stripTileByteCounts);
this.stripTileWidth = stripTileWidth;
this.stripTileHeight = stripTileHeight;
this.numBands = numBands;
// The hard way: Read tables and re-create a full JFIF stream
// 519/JPEGQTables
// 520/JPEGDCTables
// 521/JPEGACTables
// These fields were originally intended to point to a list of offsets to the quantization tables, one per
// component. Each table consists of 64 BYTES (one for each DCT coefficient in the 8x8 block). The
// quantization tables are stored in zigzag order, and are compatible with the quantization tables
// usually found in a JPEG stream DQT marker.
// The original specification strongly recommended that, within the TIFF file, each component be
// assigned separate tables, and labelled this field as mandatory whenever the JPEGProc field specifies
// a DCT-based process.
// We've seen old-style JPEG in TIFF files where some or all Table offsets, contained the JPEGQTables,
// JPEGDCTables, and JPEGACTables tags are incorrect values beyond EOF. However, these files do always
// seem to contain a useful JPEGInterchangeFormat tag. Therefore, we recommend a careful attempt to read
// the Tables tags only as a last resort, if no table data is found in a JPEGInterchangeFormat stream.
// TODO: If any of the q/dc/ac tables are equal (or have same offset, even if "spec" violation),
// use only the first occurrence, and update selectors in SOF0 and SOS
long[] qTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_Q_TABLES, "JPEGQTables", true);
qTables = new byte[qTablesOffsets.length][64];
for (int j = 0; j < qTables.length; j++) {
imageInput.seek(qTablesOffsets[j]);
imageInput.readFully(qTables[j]);
}
long[] dcTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_DC_TABLES, "JPEGDCTables", true);
dcTables = new byte[dcTablesOffsets.length][];
for (int j = 0; j < dcTables.length; j++) {
imageInput.seek(dcTablesOffsets[j]);
byte[] lengths = new byte[16];
imageInput.readFully(lengths);
int length = 0;
for (int i = 0; i < 16; i++) {
length += lengths[i] & 0xff;
}
dcTables[j] = new byte[16 + length];
System.arraycopy(lengths, 0, dcTables[j], 0, 16);
imageInput.readFully(dcTables[j], 16, length);
}
long[] acTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_AC_TABLES, "JPEGACTables", true);
acTables = new byte[acTablesOffsets.length][];
for (int j = 0; j < acTables.length; j++) {
imageInput.seek(acTablesOffsets[j]);
byte[] lengths = new byte[16];
imageInput.readFully(lengths);
int length = 0;
for (int i = 0; i < 16; i++) {
length += lengths[i] & 0xff;
}
acTables[j] = new byte[16 + length];
System.arraycopy(lengths, 0, acTables[j], 0, 16);
imageInput.readFully(acTables[j], 16, length);
}
long[] yCbCrSubSampling = getValueAsLongArray(TIFF.TAG_YCBCR_SUB_SAMPLING, "YCbCrSubSampling", false);
subsampling = yCbCrSubSampling != null
? (int) ((yCbCrSubSampling[0] & 0xf) << 4 | yCbCrSubSampling[1] & 0xf)
: 0x22;
}
@Override
public ImageInputStream createTileStream(final int tileIndex) throws IOException {
long length = stripTileByteCounts != null ? stripTileByteCounts[tileIndex] : Integer.MAX_VALUE;
imageInput.seek(stripTileOffsets[tileIndex]);
// If the tile stream starts with SOS...
if (tileIndex == 0) {
if ((short) (imageInput.readByte() << 8 | imageInput.readByte()) == (short) JPEG.SOS) {
imageInput.seek(stripTileOffsets[tileIndex] + 14); // TODO: Read from SOS length from stream, in case of gray/CMYK
length -= 14;
}
else {
imageInput.seek(stripTileOffsets[tileIndex]);
}
}
return ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(
asList(
createJFIFStream(numBands, stripTileWidth, stripTileHeight, qTables, dcTables, acTables, subsampling),
createStreamAdapter(imageInput, length),
new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI
)
)));
}
}
private TileDecoder createTileDecoder(ImageReadParam param, int compression, final int interpretation, final int numTiles, final int samplesInTile) throws IOException { private TileDecoder createTileDecoder(ImageReadParam param, int compression, final int interpretation, final int numTiles, final int samplesInTile) throws IOException {
try { try {
IIOReadWarningListener warningListener = (source, warning) -> processWarningOccurred(warning); IIOReadWarningListener warningListener = (source, warning) -> processWarningOccurred(warning);
if (compression == TIFFExtension.COMPRESSION_JPEG) { switch (compression) {
// JPEG_TABLES should be a full JPEG 'abbreviated table specification', containing: case TIFFExtension.COMPRESSION_JPEG:
// SOI, DQT, DHT, (optional markers that we ignore)..., EOI // New style JPEG
Entry tablesEntry = currentIFD.getEntryById(TIFF.TAG_JPEG_TABLES); case TIFFExtension.COMPRESSION_OLD_JPEG: {
byte[] jpegTables = tablesEntry != null ? (byte[]) tablesEntry.getValue() : null; // JPEG ('old-style' JPEG, later overridden in Technote2)
// http://www.remotesensing.org/libtiff/TIFFTechNote2.html
Predicate<ImageReader> needsConversion = (reader) -> needsCSConversion(compression, interpretation, readJPEGMetadataSafe(reader)); // JPEG_TABLES should be a full JPEG 'abbreviated table specification', containing:
RasterConverter normalize = (raster) -> normalizeColor(interpretation, samplesInTile, raster); // SOI, DQT, DHT, (optional markers that we ignore)..., EOI
byte[] jpegTables = null;
return new JPEGTileDecoder(warningListener, jpegTables, numTiles, param, needsConversion, normalize); if (compression == TIFFExtension.COMPRESSION_JPEG) {
} Entry tablesEntry = currentIFD.getEntryById(TIFF.TAG_JPEG_TABLES);
else if (compression == TIFFCustom.COMPRESSION_JBIG) { jpegTables = tablesEntry != null ? (byte[]) tablesEntry.getValue() : null;
// TODO: Create interop test suite using third party plugin. }
// LEAD Tools have one sample file: https://leadtools.com/support/forum/resource.ashx?a=545&b=1 else {
// Haven't found any plugins. There is however a JBIG2 plugin... // 512/JPEGProc: 1=Baseline, 14=Lossless (with Huffman coding), no default, although 1 is assumed if absent
return new DelegateTileDecoder(warningListener, "JBIG", param); int mode = getValueAsIntWithDefault(TIFF.TAG_OLD_JPEG_PROC, TIFFExtension.JPEG_PROC_BASELINE);
} switch (mode) {
else if (compression == TIFFCustom.COMPRESSION_JPEG2000) { case TIFFExtension.JPEG_PROC_BASELINE:
// TODO: Create interop test suite using third party plugin case TIFFExtension.JPEG_PROC_LOSSLESS:
// LEAD Tools have one sample file: https://leadtools.com/support/forum/resource.ashx?a=545&b=1 break; // Supported
// The open source JAI JP2K reader decodes this as a fully black image... default:
return new DelegateTileDecoder(warningListener, "JPEG2000", param); throw new IIOException("Unknown TIFF JPEGProcessingMode value: " + mode);
} }
else if (compression == TIFFCustom.COMPRESSION_WEBP) {
return new DelegateTileDecoder(warningListener, "WebP", param); // TODO: Consider re-factoring the super-complicated stream stuff to just using jpegTable also for old style?
}
Predicate<ImageReader> needsConversion = (reader) -> needsCSConversion(compression, interpretation, readJPEGMetadataSafe(reader));
RasterConverter normalize = (raster) -> normalizeColor(interpretation, samplesInTile, raster);
return new JPEGTileDecoder(warningListener, jpegTables, numTiles, param, needsConversion, normalize);
}
case TIFFCustom.COMPRESSION_JBIG:
// TODO: Create interop test suite using third party plugin.
// LEAD Tools have one sample file: https://leadtools.com/support/forum/resource.ashx?a=545&b=1
// Haven't found any plugins. There is however a JBIG2 plugin...
return new DelegateTileDecoder(warningListener, "JBIG", param);
case TIFFCustom.COMPRESSION_JPEG2000:
// TODO: Create interop test suite using third party plugin
// LEAD Tools have one sample file: https://leadtools.com/support/forum/resource.ashx?a=545&b=1
// The open source JAI JP2K reader decodes this as a fully black image...
return new DelegateTileDecoder(warningListener, "JPEG2000", param);
case TIFFCustom.COMPRESSION_WEBP:
return new DelegateTileDecoder(warningListener, "WebP", param);
default:
throw new AssertionError("Unexpected TIFF Compression value: " + compression);
} }
} }
catch (IIOException e) { catch (IIOException e) {
throw new IIOException("Unsupported TIFF Compression value: " + compression, e); throw new IIOException("Unsupported TIFF Compression value: " + compression, e);
} }
throw new IIOException("Unsupported TIFF Compression value: " + compression);
} }
private InputStream createYCbCrUpsamplerStream(int photometricInterpretation, int planarConfiguration, int plane, int transferType, private InputStream createYCbCrUpsamplerStream(int photometricInterpretation, int planarConfiguration, int plane, int transferType,