diff --git a/imageio/imageio-tiff/pom.xml b/imageio/imageio-tiff/pom.xml index 4847421a..83f2130e 100644 --- a/imageio/imageio-tiff/pom.xml +++ b/imageio/imageio-tiff/pom.xml @@ -30,6 +30,11 @@ imageio-jpeg test + + com.twelvemonkeys.imageio + imageio-webp + test + com.twelvemonkeys.imageio imageio-core diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/DelegateTileDecoder.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/DelegateTileDecoder.java new file mode 100644 index 00000000..a24d11c7 --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/DelegateTileDecoder.java @@ -0,0 +1,96 @@ +package com.twelvemonkeys.imageio.plugins.tiff; + +import javax.imageio.IIOException; +import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.event.IIOReadWarningListener; +import javax.imageio.stream.ImageInputStream; +import java.awt.*; +import java.awt.image.*; +import java.io.IOException; +import java.util.Iterator; +import java.util.function.Predicate; + +import static com.twelvemonkeys.lang.Validate.notNull; + +/** + * DelegateTileDecoder. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: DelegateTileDecoder.java,v 1.0 09/11/2023 haraldk Exp$ + */ +class DelegateTileDecoder extends TileDecoder { + + protected final ImageReader delegate; + protected final ImageReadParam param; + + // TODO: Naming... Is this only due to color space conversion? Is it because we need to read raster? + private final Predicate needsConversion; + private final RasterConverter converter; + private Boolean readRasterAndConvert; + + DelegateTileDecoder(final IIOReadWarningListener warningListener, final String format, final ImageReadParam originalParam) throws IOException { + this(warningListener, createDelegate(format), originalParam, imageReader -> false, null); + } + + DelegateTileDecoder(final IIOReadWarningListener warningListener, final String format, final ImageReadParam originalParam, final Predicate needsConversion, final RasterConverter converter) throws IOException { + this(warningListener, createDelegate(format), originalParam, needsConversion, converter); + } + + private DelegateTileDecoder(final IIOReadWarningListener warningListener, final ImageReader delegate, final ImageReadParam originalParam, final Predicate needsConversion, final RasterConverter converter) { + super(warningListener); + + this.delegate = notNull(delegate, "delegate"); + delegate.addIIOReadWarningListener(warningListener); + + param = delegate.getDefaultReadParam(); + param.setSourceSubsampling(originalParam.getSourceXSubsampling(), originalParam.getSourceYSubsampling(), 0, 0); + + this.needsConversion = needsConversion; + this.converter = converter; + } + + private static ImageReader createDelegate(String format) throws IOException { + // We'll just use the default (first) reader + // If it's the TwelveMonkeys one, we will be able to read JPEG Lossless etc. + Iterator readers = ImageIO.getImageReadersByFormatName(format); + if (!readers.hasNext()) { + throw new IIOException("Could not instantiate " + format + "ImageReader"); + } + + return readers.next(); + } + + @Override + void decodeTile(final ImageInputStream input, final Rectangle sourceRegion, final Point destinationOffset, final BufferedImage destination) throws IOException { + delegate.setInput(input); + param.setSourceRegion(sourceRegion); + + if (readRasterAndConvert == null) { + // All tiles in an image will use the same format, test once and cache result + readRasterAndConvert = needsConversion.test(delegate); + } + + if (!readRasterAndConvert) { + // No conversion needed + param.setDestinationOffset(destinationOffset); + param.setDestination(destination); + delegate.read(0, param); + } + 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 = delegate.readRaster(0, param); + converter.convert(raster); + + destination.getRaster().setDataElements(destinationOffset.x, destinationOffset.y, raster); + } + } + + @Override + public void close() { + delegate.dispose(); + } +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/JPEGTileDecoder.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/JPEGTileDecoder.java new file mode 100644 index 00000000..493dfd68 --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/JPEGTileDecoder.java @@ -0,0 +1,37 @@ +package com.twelvemonkeys.imageio.plugins.tiff; + +import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; + +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.event.IIOReadWarningListener; +import java.io.IOException; +import java.util.function.Predicate; + +/** + * JPEGTileDecoder. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGTileDecoder.java,v 1.0 09/11/2023 haraldk Exp$ + */ +class JPEGTileDecoder extends DelegateTileDecoder { + JPEGTileDecoder(final IIOReadWarningListener warningListener, final byte[] jpegTables, final ImageReadParam originalParam, final Predicate needsConversion, final RasterConverter converter) throws IOException { + super(warningListener, "JPEG", originalParam, needsConversion, converter); + + if (jpegTables != null) { + // Whatever values I pass the reader as the read param, it never gets the same quality as if + // I just invoke jpegReader.getStreamMetadata(), so we'll do that... + delegate.setInput(new ByteArrayImageInputStream(jpegTables)); + + // This initializes the tables and other internal settings for the reader, + // and is actually a feature of JPEG, see abbreviated streams: + // http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html#abbrev + delegate.getStreamMetadata(); + } + else { + warningListener.warningOccurred(delegate, "Missing JPEGTables for tiled/striped TIFF with compression: 7 (JPEG)"); + // ...and the JPEG reader will probably choke on missing tables... + } + } +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFCustom.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFCustom.java index 1ebe0513..e31f8ee0 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFCustom.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFCustom.java @@ -54,6 +54,8 @@ interface TIFFCustom { int COMPRESSION_JPEG2000 = 34712; // TODO: Aperio SVS JPEG2000: 33003 (YCbCr) and 33005 (RGB), see http://openslide.org/formats/aperio/ + int COMPRESSION_WEBP = 50001; + // PIXTIFF aka DELL PixTools, see https://community.emc.com/message/515755#515755 /** PIXTIFF proprietary ZIP compression, identical to Deflate/ZLib. */ int COMPRESSION_PIXTIFF_ZIP = 50013; diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java index fcb042ca..44263c22 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java @@ -48,6 +48,7 @@ import com.twelvemonkeys.imageio.metadata.tiff.Rational; import com.twelvemonkeys.imageio.metadata.tiff.TIFF; import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; import com.twelvemonkeys.imageio.metadata.xmp.XMPReader; +import com.twelvemonkeys.imageio.plugins.tiff.TileDecoder.RasterConverter; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import com.twelvemonkeys.imageio.stream.DirectImageInputStream; import com.twelvemonkeys.imageio.stream.SubImageInputStream; @@ -70,7 +71,6 @@ import javax.imageio.ImageTypeSpecifier; import javax.imageio.event.IIOReadWarningListener; import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataNode; -import javax.imageio.plugins.jpeg.JPEGImageReadParam; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import java.awt.*; @@ -93,6 +93,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Set; +import java.util.function.Predicate; import java.util.zip.InflaterInputStream; import static com.twelvemonkeys.imageio.util.IIOUtil.createStreamAdapter; @@ -935,6 +936,10 @@ public final class TIFFImageReader extends ImageReaderBase { int width = getWidth(imageIndex); int height = getHeight(imageIndex); + if (param == null) { + param = getDefaultReadParam(); + } + BufferedImage destination = getDestination(param, getImageTypes(imageIndex), width, height); ImageTypeSpecifier rawType = getRawImageType(imageIndex); checkReadParamBandSettings(param, rawType.getNumBands(), destination.getSampleModel().getNumBands()); @@ -943,10 +948,10 @@ public final class TIFFImageReader extends ImageReaderBase { final Rectangle dstRegion = new Rectangle(); computeRegions(param, width, height, destination, srcRegion, dstRegion); - int xSub = param != null ? param.getSourceXSubsampling() : 1; - int ySub = param != null ? param.getSourceYSubsampling() : 1; + int xSub = param.getSourceXSubsampling(); + int ySub = param.getSourceYSubsampling(); - WritableRaster destRaster = clipToRect(destination.getRaster(), dstRegion, param != null ? param.getDestinationBands() : null); + WritableRaster destRaster = clipToRect(destination.getRaster(), dstRegion, param.getDestinationBands()); final int interpretation = getPhotometricInterpretationWithFallback(); final int compression = getValueAsIntWithDefault(TIFF.TAG_COMPRESSION, TIFFBaseline.COMPRESSION_NONE); @@ -1000,8 +1005,6 @@ public final class TIFFImageReader extends ImageReaderBase { WritableRaster rowRaster = rawType.createBufferedImage(stripTileWidth, 1).getRaster(); Rectangle clip = new Rectangle(srcRegion); - int srcRow = 0; - Boolean needsCSConversion = null; switch (compression) { case TIFFBaseline.COMPRESSION_NONE: @@ -1020,8 +1023,9 @@ public final class TIFFImageReader extends ImageReaderBase { // CCITT modified Huffman case TIFFExtension.COMPRESSION_CCITT_T4: // CCITT Group 3 fax encoding - case TIFFExtension.COMPRESSION_CCITT_T6: + case TIFFExtension.COMPRESSION_CCITT_T6: { // CCITT Group 4 fax encoding + int srcRow = 0; int[] yCbCrSubsampling = null; int yCbCrPos = 1; @@ -1031,7 +1035,7 @@ public final class TIFFImageReader extends ImageReaderBase { if (rowRaster.getNumBands() != 3) { throw new IIOException("TIFF PhotometricInterpretation YCbCr requires SamplesPerPixel == 3: " + rowRaster.getNumBands()); } - if (rowRaster.getTransferType() != DataBuffer.TYPE_BYTE && rowRaster.getTransferType() != DataBuffer.TYPE_USHORT) { + if (rowRaster.getTransferType() != DataBuffer.TYPE_BYTE && rowRaster.getTransferType() != DataBuffer.TYPE_USHORT) { throw new IIOException("TIFF PhotometricInterpretation YCbCr requires BitsPerSample == [8,8,8] or [16,16,16]"); } @@ -1090,10 +1094,7 @@ public final class TIFFImageReader extends ImageReaderBase { // 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); - + Raster clippedRow = clipRowToRect(rowRaster, clip, param.getSourceBands(), param.getSourceXSubsampling()); imageInput.seek(stripTileOffsets[i]); ImageInputStream input; @@ -1109,15 +1110,15 @@ public final class TIFFImageReader extends ImageReaderBase { } else { InputStream adapter = stripTileByteCounts != null - ? createStreamAdapter(imageInput, stripTileByteCounts[i]) - : createStreamAdapter(imageInput); + ? createStreamAdapter(imageInput, stripTileByteCounts[i]) + : createStreamAdapter(imageInput); adapter = createFillOrderStream(fillOrder, adapter); // For subsampled planar, the compressed data will not be full width int compressedStripTileWidth = planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR && b > 0 && yCbCrSubsampling != null - ? ((stripTileWidth + yCbCrSubsampling[0] - 1) / yCbCrSubsampling[0]) - : stripTileWidth; + ? ((stripTileWidth + yCbCrSubsampling[0] - 1) / yCbCrSubsampling[0]) + : stripTileWidth; adapter = createDecompressorStream(compression, compressedStripTileWidth, samplesInTile, adapter); adapter = createUnpredictorStream(predictor, compressedStripTileWidth, samplesInTile, bitsPerSample, adapter, imageInput.getByteOrder()); adapter = createYCbCrUpsamplerStream(interpretation, planarConfiguration, b, rowRaster.getTransferType(), yCbCrSubsampling, yCbCrPos, colsInTile, adapter, imageInput.getByteOrder()); @@ -1125,8 +1126,8 @@ public final class TIFFImageReader extends ImageReaderBase { if (needsBitPadding) { // We'll pad "odd" bitsPerSample streams to the smallest data type (byte/short/int) larger than the input adapter = bitsPerSample < 8 - ? new BitPaddingStream(adapter, 1, samplesInTile * bitsPerSample, colsInTile, imageInput.getByteOrder()) - : new BitPaddingStream(adapter, samplesInTile, bitsPerSample, colsInTile, imageInput.getByteOrder()); + ? new BitPaddingStream(adapter, 1, samplesInTile * 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 @@ -1170,120 +1171,16 @@ public final class TIFFImageReader extends ImageReaderBase { } break; - + } + case TIFFCustom.COMPRESSION_WEBP: case TIFFExtension.COMPRESSION_JPEG: - // JPEG ('new-style' JPEG) - // TODO: Refactor all JPEG reading out to separate JPEG support class? - // TODO: Cache the JPEG reader for later use? Remember to reset to avoid resource leaks - - ImageReader jpegReader = createJPEGDelegate(); - // TODO: Use proper inner class + add case for old JPEG - jpegReader.addIIOReadWarningListener(new IIOReadWarningListener() { - @Override - public void warningOccurred(final ImageReader source, final String warning) { - processWarningOccurred(warning); - } - }); - JPEGImageReadParam jpegParam = (JPEGImageReadParam) jpegReader.getDefaultReadParam(); - - // JPEG_TABLES should be a full JPEG 'abbreviated table specification', containing: - // SOI, DQT, DHT, (optional markers that we ignore)..., EOI - Entry tablesEntry = currentIFD.getEntryById(TIFF.TAG_JPEG_TABLES); - byte[] tablesValue = tablesEntry != null ? (byte[]) tablesEntry.getValue() : null; - if (tablesValue != null) { - // Whatever values I pass the reader as the read param, it never gets the same quality as if - // I just invoke jpegReader.getStreamMetadata(), so we'll do that... - jpegReader.setInput(new ByteArrayImageInputStream(tablesValue)); - - // This initializes the tables and other internal settings for the reader, - // and is actually a feature of JPEG, see abbreviated streams: - // http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html#abbrev - jpegReader.getStreamMetadata(); - } - else if (tilesDown * tilesAcross > 1) { - processWarningOccurred("Missing JPEGTables for tiled/striped TIFF with compression: 7 (JPEG)"); - // ...and the JPEG reader will probably choke on missing tables... - } - - // Read data - processImageStarted(imageIndex); // Better yet, would be to delegate read progress here... - - 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 i = y * tilesAcross + x; - int colsInTile = Math.min(stripTileWidth, width - col); - - // Read only tiles that lies within region - Rectangle tileRect = new Rectangle(col, srcRow, 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)) { - jpegReader.setInput(subStream); - jpegParam.setSourceRegion(new Rectangle(intersection.x - col, intersection.y - srcRow, 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... - 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); - // TODO: Refactor + duplicate this for all JPEG-in-TIFF cases - switch (raster.getTransferType()) { - case DataBuffer.TYPE_BYTE: - normalizeColor(interpretation, samplesInTile, ((DataBufferByte) raster.getDataBuffer()).getData()); - break; - case DataBuffer.TYPE_USHORT: - normalizeColor(interpretation, samplesInTile, ((DataBufferUShort) raster.getDataBuffer()).getData()); - break; - default: - throw new IllegalStateException("Unsupported transfer type: " + raster.getTransferType()); - } - - destination.getRaster().setDataElements(offset.x, offset.y, raster); - } - } - } - - if (abortRequested()) { - break; - } - - col += colsInTile; - } - - processImageProgress(100f * srcRow / height); - - if (abortRequested()) { - processReadAborted(); - break; - } - - srcRow += rowsInTile; - } - + readUsingDelegate(imageIndex, compression, interpretation, width, height, tilesAcross, tilesDown, stripTileWidth, stripTileHeight, srcRegion, stripTileOffsets, stripTileByteCounts, param, destination, samplesInTile); break; - - case TIFFExtension.COMPRESSION_OLD_JPEG: + case TIFFExtension.COMPRESSION_OLD_JPEG: { // JPEG ('old-style' JPEG, later overridden in Technote2) // http://www.remotesensing.org/libtiff/TIFFTechNote2.html + 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); @@ -1295,8 +1192,8 @@ public final class TIFFImageReader extends ImageReaderBase { throw new IIOException("Unknown TIFF JPEGProcessingMode value: " + mode); } - jpegReader = createJPEGDelegate(); - jpegParam = (JPEGImageReadParam) jpegReader.getDefaultReadParam(); + ImageReader jpegReader = createJPEGDelegate(); + ImageReadParam jpegParam = jpegReader.getDefaultReadParam(); // 513/JPEGInterchangeFormat (may be absent or 0) int jpegOffset = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT, -1); @@ -1394,7 +1291,7 @@ public final class TIFFImageReader extends ImageReaderBase { 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 + new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI ))))) { jpegReader.setInput(stream); jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile)); @@ -1436,7 +1333,6 @@ public final class TIFFImageReader extends ImageReaderBase { srcRow += rowsInTile; } - } else { // The hard way: Read tables and re-create a full JFIF stream @@ -1509,8 +1405,8 @@ public final class TIFFImageReader extends ImageReaderBase { long[] yCbCrSubSampling = getValueAsLongArray(TIFF.TAG_YCBCR_SUB_SAMPLING, "YCbCrSubSampling", false); int subsampling = yCbCrSubSampling != null - ? (int) ((yCbCrSubSampling[0] & 0xf) << 4 | yCbCrSubSampling[1] & 0xf) - : 0x22; + ? (int) ((yCbCrSubSampling[0] & 0xf) << 4 | yCbCrSubSampling[1] & 0xf) + : 0x22; // Read data processImageStarted(imageIndex); @@ -1589,7 +1485,7 @@ public final class TIFFImageReader extends ImageReaderBase { } break; - + } // Known, but unsupported compression types case TIFFCustom.COMPRESSION_NEXT: case TIFFCustom.COMPRESSION_CCITTRLEW: @@ -1618,6 +1514,97 @@ public final class TIFFImageReader extends ImageReaderBase { 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, 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; + } + } + } + + private DelegateTileDecoder createTileDecoder(ImageReadParam param, int compression, final int interpretation, final int samplesInTile) throws IOException { + if (compression == TIFFExtension.COMPRESSION_JPEG) { + // JPEG_TABLES should be a full JPEG 'abbreviated table specification', containing: + // SOI, DQT, DHT, (optional markers that we ignore)..., EOI + Entry tablesEntry = currentIFD.getEntryById(TIFF.TAG_JPEG_TABLES); + byte[] tablesValue = tablesEntry != null ? (byte[]) tablesEntry.getValue() : null; + + Predicate needsConversion = (reader) -> needsCSConversion(compression, interpretation, readJPEGMetadataSafe(reader)); + RasterConverter csConverter = (raster) -> { + switch (raster.getTransferType()) { + case DataBuffer.TYPE_BYTE: + normalizeColor(interpretation, samplesInTile, ((DataBufferByte) raster.getDataBuffer()).getData()); + break; + case DataBuffer.TYPE_USHORT: + normalizeColor(interpretation, samplesInTile, ((DataBufferUShort) raster.getDataBuffer()).getData()); + break; + default: + throw new IllegalStateException("Unsupported transfer type: " + raster.getTransferType()); + } + }; + + return new JPEGTileDecoder((source, warning) -> processWarningOccurred(warning), tablesValue, param, needsConversion, csConverter); + } + else if (compression == TIFFCustom.COMPRESSION_JBIG) { + return new DelegateTileDecoder((source, warning) -> processWarningOccurred(warning), "JBIG", param); + } + else if (compression == TIFFCustom.COMPRESSION_JPEG2000) { + return new DelegateTileDecoder((source, warning) -> processWarningOccurred(warning), "JP2K", param); + } + else if (compression == TIFFCustom.COMPRESSION_WEBP) { + return new DelegateTileDecoder((source, warning) -> processWarningOccurred(warning), "WebP", param); + } + + throw new IIOException("Unsupported TIFF Compression value: " + compression); + } + private InputStream createYCbCrUpsamplerStream(int photometricInterpretation, int planarConfiguration, int plane, int transferType, int[] yCbCrSubsampling, int yCbCrPos, int colsInTile, InputStream stream, ByteOrder byteOrder) { if (photometricInterpretation == TIFFExtension.PHOTOMETRIC_YCBCR) { @@ -1650,11 +1637,11 @@ public final class TIFFImageReader extends ImageReaderBase { return false; } - private IIOMetadata readJPEGMetadataSafe(final ImageReader jpegReader) throws IOException { + private IIOMetadata readJPEGMetadataSafe(final ImageReader jpegReader) { try { return jpegReader.getImageMetadata(0); } - catch (IIOException e) { + catch (IOException e) { processWarningOccurred(String.format("Could not read metadata for JPEG compressed TIFF (%s). Colors may look incorrect", e.getMessage())); return null; diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TileDecoder.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TileDecoder.java new file mode 100644 index 00000000..9ed7259c --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TileDecoder.java @@ -0,0 +1,32 @@ +package com.twelvemonkeys.imageio.plugins.tiff; + +import javax.imageio.event.IIOReadWarningListener; +import javax.imageio.stream.ImageInputStream; +import java.awt.*; +import java.awt.image.*; +import java.io.IOException; + +/** + * TileDecoder. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TileDecoder.java,v 1.0 09/11/2023 haraldk Exp$ + */ +abstract class TileDecoder implements AutoCloseable { + + protected final IIOReadWarningListener warningListener; + + public TileDecoder(IIOReadWarningListener warningListener) { + this.warningListener = warningListener; + } + + abstract void decodeTile(ImageInputStream input, Rectangle sourceRegion, Point destinationOffset, BufferedImage destination) throws IOException; + + @Override + public abstract void close(); + + interface RasterConverter { + void convert(Raster raster) throws IOException; + } +} diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java index 0083e61a..3d7a510e 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java @@ -191,7 +191,9 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest${project.version} + + ${project.groupId} + imageio-webp + ${project.version} + + ${project.groupId} imageio-core