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,34 +1176,137 @@ public final class TIFFImageReader extends ImageReaderBase {
case TIFFCustom.COMPRESSION_WEBP:
case TIFFCustom.COMPRESSION_JBIG:
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;
case TIFFExtension.COMPRESSION_OLD_JPEG: {
// JPEG ('old-style' JPEG, later overridden in Technote2)
// http://www.remotesensing.org/libtiff/TIFFTechNote2.html
// TODO: Rewrite to use readUsingDelegate with special case handling inside OldJPEGTileDecoder
case TIFFExtension.COMPRESSION_OLD_JPEG:
boolean interChangeFormat = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT, -1) >= 0;
int srcRow = 0;
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);
if (!interChangeFormat) {
processWarningOccurred("Old-style JPEG compressed TIFF without JPEGInterchangeFormat encountered. Attempting to re-create JFIF stream.");
}
ImageReader jpegReader = createJPEGDelegate();
ImageReadParam jpegParam = jpegReader.getDefaultReadParam();
// 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;
case TIFFCustom.COMPRESSION_NEXT:
case TIFFCustom.COMPRESSION_CCITTRLEW:
case TIFFCustom.COMPRESSION_THUNDERSCAN:
case TIFFCustom.COMPRESSION_IT8CTPAD:
case TIFFCustom.COMPRESSION_IT8LW:
case TIFFCustom.COMPRESSION_IT8MP:
case TIFFCustom.COMPRESSION_IT8BL:
case TIFFCustom.COMPRESSION_PIXARFILM:
case TIFFCustom.COMPRESSION_PIXARLOG:
case TIFFCustom.COMPRESSION_DCS:
case TIFFCustom.COMPRESSION_SGILOG:
case TIFFCustom.COMPRESSION_SGILOG24:
throw new IIOException("Unsupported TIFF Compression value: " + compression);
default:
throw new IIOException("Unknown TIFF Compression value: " + compression);
}
// TODO: Convert color space from source to destination
processImageComplete();
return destination;
}
private void readUsingDelegate(int imageIndex, int compression, int interpretation, int width, int height,
int tilesAcross, int tilesDown, int stripTileWidth, int stripTileHeight, Rectangle srcRegion,
TileStreamFactory factory,
ImageReadParam param, BufferedImage destination, int samplesInTile) throws IOException {
// Read data
try (TileDecoder tileDecoder = createTileDecoder(param, compression, interpretation, tilesAcross * tilesDown, samplesInTile)) {
processImageStarted(imageIndex); // Better yet, would be to delegate read progress here...
int row = 0;
for (int y = 0; y < tilesDown; y++) {
int col = 0;
int rowsInTile = Math.min(stripTileHeight, height - row);
for (int x = 0; x < tilesAcross; x++) {
int colsInTile = Math.min(stripTileWidth, width - col);
// Read only tiles that lies within region
Rectangle tileRect = new Rectangle(col, row, colsInTile, rowsInTile);
Rectangle intersection = tileRect.intersection(srcRegion);
if (!intersection.isEmpty()) {
int tileIndex = y * tilesAcross + x;
try (ImageInputStream tileStream = factory.createTileStream(tileIndex)) {
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);
tileDecoder.decodeTile(tileStream, sourceRegion, destinationOffset, destination);
}
}
if (abortRequested()) {
break;
}
col += colsInTile;
}
processImageProgress(100f * row / height);
if (abortRequested()) {
processReadAborted();
break;
}
row += rowsInTile;
}
}
}
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)
// 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)
@ -1211,7 +1314,6 @@ public final class TIFFImageReader extends ImageReaderBase {
// 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) {
@ -1225,7 +1327,7 @@ public final class TIFFImageReader extends ImageReaderBase {
// 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;
realJPEGOffset = jpegOffset;
short expectedSOI = (short) (imageInput.readByte() << 8 | imageInput.readByte());
if (expectedSOI != (short) JPEG.SOI) {
@ -1247,8 +1349,6 @@ public final class TIFFImageReader extends ImageReaderBase {
}
}
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];
@ -1276,72 +1376,38 @@ public final class TIFFImageReader extends ImageReaderBase {
processWarningOccurred("Incorrect StripByteCounts/TileByteCounts for single tile, using JPEGInterchangeFormatLength instead.");
stripTileByteCounts[0] = jpegLength;
}
}
// Read data
processImageStarted(imageIndex);
@Override
public ImageInputStream createTileStream(final int tileIndex) throws IOException {
long length = stripTileByteCounts != null ? stripTileByteCounts[tileIndex] : Integer.MAX_VALUE;
for (int y = 0; y < tilesDown; y++) {
int col = 0;
int rowsInTile = Math.min(stripTileHeight, height - srcRow);
imageInput.seek(stripTileOffsets != null ? stripTileOffsets[tileIndex] : realJPEGOffset);
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(
return ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(asList(
new ByteArrayInputStream(jpegHeader),
createStreamAdapter(imageInput, len),
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;
}
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;
col += colsInTile;
}
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;
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.");
// 519/JPEGQTables
// 520/JPEGDCTables
@ -1365,14 +1431,14 @@ public final class TIFFImageReader extends ImageReaderBase {
// 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];
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][];
dcTables = new byte[dcTablesOffsets.length][];
for (int j = 0; j < dcTables.length; j++) {
imageInput.seek(dcTablesOffsets[j]);
@ -1391,7 +1457,7 @@ public final class TIFFImageReader extends ImageReaderBase {
}
long[] acTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_AC_TABLES, "JPEGACTables", true);
byte[][] acTables = new byte[acTablesOffsets.length][];
acTables = new byte[acTablesOffsets.length][];
for (int j = 0; j < acTables.length; j++) {
imageInput.seek(acTablesOffsets[j]);
byte[] lengths = new byte[16];
@ -1409,166 +1475,35 @@ public final class TIFFImageReader extends ImageReaderBase {
}
long[] yCbCrSubSampling = getValueAsLongArray(TIFF.TAG_YCBCR_SUB_SAMPLING, "YCbCrSubSampling", false);
int subsampling = yCbCrSubSampling != null
subsampling = yCbCrSubSampling != null
? (int) ((yCbCrSubSampling[0] & 0xf) << 4 | yCbCrSubSampling[1] & 0xf)
: 0x22;
}
// Read data
processImageStarted(imageIndex);
@Override
public ImageInputStream createTileStream(final int tileIndex) throws IOException {
long length = stripTileByteCounts != null ? stripTileByteCounts[tileIndex] : Integer.MAX_VALUE;
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]);
imageInput.seek(stripTileOffsets[tileIndex]);
// If the tile stream starts with SOS...
if (x == 0 && y == 0) {
if (tileIndex == 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
imageInput.seek(stripTileOffsets[tileIndex] + 14); // TODO: Read from SOS length from stream, in case of gray/CMYK
length -= 14;
}
else {
imageInput.seek(stripTileOffsets[i]);
imageInput.seek(stripTileOffsets[tileIndex]);
}
}
try (ImageInputStream stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(
return ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(
asList(
createJFIFStream(destRaster.getNumBands(), stripTileWidth, stripTileHeight, qTables, dcTables, acTables, subsampling),
createJFIFStream(numBands, 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;
}
}
break;
}
// Known, but unsupported compression types
case TIFFCustom.COMPRESSION_NEXT:
case TIFFCustom.COMPRESSION_CCITTRLEW:
case TIFFCustom.COMPRESSION_THUNDERSCAN:
case TIFFCustom.COMPRESSION_IT8CTPAD:
case TIFFCustom.COMPRESSION_IT8LW:
case TIFFCustom.COMPRESSION_IT8MP:
case TIFFCustom.COMPRESSION_IT8BL:
case TIFFCustom.COMPRESSION_PIXARFILM:
case TIFFCustom.COMPRESSION_PIXARLOG:
case TIFFCustom.COMPRESSION_DCS:
case TIFFCustom.COMPRESSION_SGILOG:
case TIFFCustom.COMPRESSION_SGILOG24:
throw new IIOException("Unsupported TIFF Compression value: " + compression);
default:
throw new IIOException("Unknown TIFF Compression value: " + compression);
}
// TODO: Convert color space from source to destination
processImageComplete();
return destination;
}
private void readUsingDelegate(int imageIndex, int compression, int interpretation, int width, int height,
int tilesAcross, int tilesDown, int stripTileWidth, int stripTileHeight, Rectangle srcRegion,
long[] stripTileOffsets, long[] stripTileByteCounts,
ImageReadParam param, BufferedImage destination, int samplesInTile) throws IOException {
// JPEG ('new-style' JPEG)
// Read data
try (TileDecoder tileDecoder = createTileDecoder(param, compression, interpretation, tilesAcross * tilesDown, samplesInTile)) {
processImageStarted(imageIndex); // Better yet, would be to delegate read progress here...
int row = 0;
for (int y = 0; y < tilesDown; y++) {
int col = 0;
int rowsInTile = Math.min(stripTileHeight, height - row);
for (int x = 0; x < tilesAcross; x++) {
int i = y * tilesAcross + x;
int colsInTile = Math.min(stripTileWidth, width - col);
// Read only tiles that lies within region
Rectangle tileRect = new Rectangle(col, row, colsInTile, rowsInTile);
Rectangle intersection = tileRect.intersection(srcRegion);
if (!intersection.isEmpty()) {
imageInput.seek(stripTileOffsets[i]);
int length = stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE;
try (ImageInputStream subStream = new SubImageInputStream(imageInput, length)) {
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);
tileDecoder.decodeTile(subStream, sourceRegion, destinationOffset, destination);
}
}
if (abortRequested()) {
break;
}
col += colsInTile;
}
processImageProgress(100f * row / height);
if (abortRequested()) {
processReadAborted();
break;
}
row += rowsInTile;
}
)));
}
}
@ -1576,38 +1511,59 @@ public final class TIFFImageReader extends ImageReaderBase {
try {
IIOReadWarningListener warningListener = (source, warning) -> processWarningOccurred(warning);
if (compression == TIFFExtension.COMPRESSION_JPEG) {
switch (compression) {
case TIFFExtension.COMPRESSION_JPEG:
// New style JPEG
case TIFFExtension.COMPRESSION_OLD_JPEG: {
// JPEG ('old-style' JPEG, later overridden in Technote2)
// http://www.remotesensing.org/libtiff/TIFFTechNote2.html
// JPEG_TABLES should be a full JPEG 'abbreviated table specification', containing:
// SOI, DQT, DHT, (optional markers that we ignore)..., EOI
byte[] jpegTables = null;
if (compression == TIFFExtension.COMPRESSION_JPEG) {
Entry tablesEntry = currentIFD.getEntryById(TIFF.TAG_JPEG_TABLES);
byte[] jpegTables = tablesEntry != null ? (byte[]) tablesEntry.getValue() : null;
jpegTables = tablesEntry != null ? (byte[]) tablesEntry.getValue() : null;
}
else {
// 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);
}
// 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);
}
else if (compression == TIFFCustom.COMPRESSION_JBIG) {
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);
}
else if (compression == TIFFCustom.COMPRESSION_JPEG2000) {
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);
}
else if (compression == TIFFCustom.COMPRESSION_WEBP) {
case TIFFCustom.COMPRESSION_WEBP:
return new DelegateTileDecoder(warningListener, "WebP", param);
default:
throw new AssertionError("Unexpected TIFF Compression value: " + compression);
}
}
catch (IIOException 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,