Support for WebP in TIFF

Refactored tile reading for delegated formats.
This commit is contained in:
Harald Kuhr 2023-11-10 09:27:40 +01:00
parent 8715b6b696
commit 1292c95040
10 changed files with 317 additions and 140 deletions

View File

@ -30,6 +30,11 @@
<artifactId>imageio-jpeg</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-webp</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-core</artifactId>

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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<ImageReader> 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<ImageReader> 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<ImageReader> 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<ImageReader> 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();
}
}

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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<ImageReader> 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...
}
}
}

View File

@ -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;

View File

@ -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<ImageReader> 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;

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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;
}
}

View File

@ -191,7 +191,9 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
new TestData(getClassLoaderResource("/tiff/planar-yuv420-jpeg-uncompressed.tif"), new Dimension(256, 64)), // YCbCr, JPEG coefficients, uncompressed, striped
new TestData(getClassLoaderResource("/tiff/planar-yuv420-jpeg-lzw.tif"), new Dimension(256, 64)), // YCbCr, JPEG coefficients,LZW compressed, striped
new TestData(getClassLoaderResource("/tiff/planar-yuv410-jpeg-uncompressed.tif"), new Dimension(256, 64)), // YCbCr, JPEG coefficients, uncompressed, striped
new TestData(getClassLoaderResource("/tiff/planar-yuv410-jpeg-lzw.tif"), new Dimension(256, 64)) // YCbCr, JPEG coefficients,LZW compressed, striped
new TestData(getClassLoaderResource("/tiff/planar-yuv410-jpeg-lzw.tif"), new Dimension(256, 64)), // YCbCr, JPEG coefficients,LZW compressed, striped
// WebP compressed
new TestData(getClassLoaderResource("/tiff/webp_lossless_rgba_alpha_fully_opaque.tif"), new Dimension(20, 20)) // RGBA, WebP lossless
);
}

View File

@ -418,8 +418,18 @@ final class WebPImageReader extends ImageReaderBase {
}
types.add(rawImageType);
types.add(ImageTypeSpecifiers.createFromBufferedImageType(header.containsALPH ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB));
types.add(ImageTypeSpecifiers.createFromBufferedImageType(header.containsALPH ? BufferedImage.TYPE_INT_ARGB_PRE : BufferedImage.TYPE_INT_BGR));
if (!header.containsALPH) {
types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB));
types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR));
// We can always decode into types with alpha
types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR));
}
types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB));
types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB_PRE));
types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR_PRE));
return types.iterator();
}

View File

@ -161,6 +161,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>imageio-webp</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>imageio-core</artifactId>