mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-05 04:25:29 -04:00
Support for WebP in TIFF
Refactored tile reading for delegated formats.
This commit is contained in:
parent
8715b6b696
commit
1292c95040
@ -30,6 +30,11 @@
|
|||||||
<artifactId>imageio-jpeg</artifactId>
|
<artifactId>imageio-jpeg</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||||
|
<artifactId>imageio-webp</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||||
<artifactId>imageio-core</artifactId>
|
<artifactId>imageio-core</artifactId>
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -54,6 +54,8 @@ interface TIFFCustom {
|
|||||||
int COMPRESSION_JPEG2000 = 34712;
|
int COMPRESSION_JPEG2000 = 34712;
|
||||||
// TODO: Aperio SVS JPEG2000: 33003 (YCbCr) and 33005 (RGB), see http://openslide.org/formats/aperio/
|
// 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 aka DELL PixTools, see https://community.emc.com/message/515755#515755
|
||||||
/** PIXTIFF proprietary ZIP compression, identical to Deflate/ZLib. */
|
/** PIXTIFF proprietary ZIP compression, identical to Deflate/ZLib. */
|
||||||
int COMPRESSION_PIXTIFF_ZIP = 50013;
|
int COMPRESSION_PIXTIFF_ZIP = 50013;
|
||||||
|
@ -48,6 +48,7 @@ import com.twelvemonkeys.imageio.metadata.tiff.Rational;
|
|||||||
import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
|
import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
|
||||||
import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
|
import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
|
||||||
import com.twelvemonkeys.imageio.metadata.xmp.XMPReader;
|
import com.twelvemonkeys.imageio.metadata.xmp.XMPReader;
|
||||||
|
import com.twelvemonkeys.imageio.plugins.tiff.TileDecoder.RasterConverter;
|
||||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
||||||
import com.twelvemonkeys.imageio.stream.DirectImageInputStream;
|
import com.twelvemonkeys.imageio.stream.DirectImageInputStream;
|
||||||
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
|
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
|
||||||
@ -70,7 +71,6 @@ import javax.imageio.ImageTypeSpecifier;
|
|||||||
import javax.imageio.event.IIOReadWarningListener;
|
import javax.imageio.event.IIOReadWarningListener;
|
||||||
import javax.imageio.metadata.IIOMetadata;
|
import javax.imageio.metadata.IIOMetadata;
|
||||||
import javax.imageio.metadata.IIOMetadataNode;
|
import javax.imageio.metadata.IIOMetadataNode;
|
||||||
import javax.imageio.plugins.jpeg.JPEGImageReadParam;
|
|
||||||
import javax.imageio.spi.ImageReaderSpi;
|
import javax.imageio.spi.ImageReaderSpi;
|
||||||
import javax.imageio.stream.ImageInputStream;
|
import javax.imageio.stream.ImageInputStream;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
@ -93,6 +93,7 @@ import java.util.HashSet;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import java.util.zip.InflaterInputStream;
|
import java.util.zip.InflaterInputStream;
|
||||||
|
|
||||||
import static com.twelvemonkeys.imageio.util.IIOUtil.createStreamAdapter;
|
import static com.twelvemonkeys.imageio.util.IIOUtil.createStreamAdapter;
|
||||||
@ -935,6 +936,10 @@ public final class TIFFImageReader extends ImageReaderBase {
|
|||||||
int width = getWidth(imageIndex);
|
int width = getWidth(imageIndex);
|
||||||
int height = getHeight(imageIndex);
|
int height = getHeight(imageIndex);
|
||||||
|
|
||||||
|
if (param == null) {
|
||||||
|
param = getDefaultReadParam();
|
||||||
|
}
|
||||||
|
|
||||||
BufferedImage destination = getDestination(param, getImageTypes(imageIndex), width, height);
|
BufferedImage destination = getDestination(param, getImageTypes(imageIndex), width, height);
|
||||||
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
|
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
|
||||||
checkReadParamBandSettings(param, rawType.getNumBands(), destination.getSampleModel().getNumBands());
|
checkReadParamBandSettings(param, rawType.getNumBands(), destination.getSampleModel().getNumBands());
|
||||||
@ -943,10 +948,10 @@ public final class TIFFImageReader extends ImageReaderBase {
|
|||||||
final Rectangle dstRegion = new Rectangle();
|
final Rectangle dstRegion = new Rectangle();
|
||||||
computeRegions(param, width, height, destination, srcRegion, dstRegion);
|
computeRegions(param, width, height, destination, srcRegion, dstRegion);
|
||||||
|
|
||||||
int xSub = param != null ? param.getSourceXSubsampling() : 1;
|
int xSub = param.getSourceXSubsampling();
|
||||||
int ySub = param != null ? param.getSourceYSubsampling() : 1;
|
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 interpretation = getPhotometricInterpretationWithFallback();
|
||||||
final int compression = getValueAsIntWithDefault(TIFF.TAG_COMPRESSION, TIFFBaseline.COMPRESSION_NONE);
|
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();
|
WritableRaster rowRaster = rawType.createBufferedImage(stripTileWidth, 1).getRaster();
|
||||||
|
|
||||||
Rectangle clip = new Rectangle(srcRegion);
|
Rectangle clip = new Rectangle(srcRegion);
|
||||||
int srcRow = 0;
|
|
||||||
Boolean needsCSConversion = null;
|
|
||||||
|
|
||||||
switch (compression) {
|
switch (compression) {
|
||||||
case TIFFBaseline.COMPRESSION_NONE:
|
case TIFFBaseline.COMPRESSION_NONE:
|
||||||
@ -1020,8 +1023,9 @@ public final class TIFFImageReader extends ImageReaderBase {
|
|||||||
// CCITT modified Huffman
|
// CCITT modified Huffman
|
||||||
case TIFFExtension.COMPRESSION_CCITT_T4:
|
case TIFFExtension.COMPRESSION_CCITT_T4:
|
||||||
// CCITT Group 3 fax encoding
|
// CCITT Group 3 fax encoding
|
||||||
case TIFFExtension.COMPRESSION_CCITT_T6:
|
case TIFFExtension.COMPRESSION_CCITT_T6: {
|
||||||
// CCITT Group 4 fax encoding
|
// CCITT Group 4 fax encoding
|
||||||
|
int srcRow = 0;
|
||||||
|
|
||||||
int[] yCbCrSubsampling = null;
|
int[] yCbCrSubsampling = null;
|
||||||
int yCbCrPos = 1;
|
int yCbCrPos = 1;
|
||||||
@ -1031,7 +1035,7 @@ public final class TIFFImageReader extends ImageReaderBase {
|
|||||||
if (rowRaster.getNumBands() != 3) {
|
if (rowRaster.getNumBands() != 3) {
|
||||||
throw new IIOException("TIFF PhotometricInterpretation YCbCr requires SamplesPerPixel == 3: " + rowRaster.getNumBands());
|
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]");
|
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 the stripTile rowRaster to not exceed the srcRegion
|
||||||
clip.width = Math.min(colsInTile, srcRegion.width);
|
clip.width = Math.min(colsInTile, srcRegion.width);
|
||||||
Raster clippedRow = clipRowToRect(rowRaster, clip,
|
Raster clippedRow = clipRowToRect(rowRaster, clip, param.getSourceBands(), param.getSourceXSubsampling());
|
||||||
param != null ? param.getSourceBands() : null,
|
|
||||||
param != null ? param.getSourceXSubsampling() : 1);
|
|
||||||
|
|
||||||
imageInput.seek(stripTileOffsets[i]);
|
imageInput.seek(stripTileOffsets[i]);
|
||||||
|
|
||||||
ImageInputStream input;
|
ImageInputStream input;
|
||||||
@ -1109,15 +1110,15 @@ public final class TIFFImageReader extends ImageReaderBase {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
InputStream adapter = stripTileByteCounts != null
|
InputStream adapter = stripTileByteCounts != null
|
||||||
? createStreamAdapter(imageInput, stripTileByteCounts[i])
|
? createStreamAdapter(imageInput, stripTileByteCounts[i])
|
||||||
: createStreamAdapter(imageInput);
|
: createStreamAdapter(imageInput);
|
||||||
|
|
||||||
adapter = createFillOrderStream(fillOrder, adapter);
|
adapter = createFillOrderStream(fillOrder, adapter);
|
||||||
|
|
||||||
// For subsampled planar, the compressed data will not be full width
|
// For subsampled planar, the compressed data will not be full width
|
||||||
int compressedStripTileWidth = planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR && b > 0 && yCbCrSubsampling != null
|
int compressedStripTileWidth = planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR && b > 0 && yCbCrSubsampling != null
|
||||||
? ((stripTileWidth + yCbCrSubsampling[0] - 1) / yCbCrSubsampling[0])
|
? ((stripTileWidth + yCbCrSubsampling[0] - 1) / yCbCrSubsampling[0])
|
||||||
: stripTileWidth;
|
: stripTileWidth;
|
||||||
adapter = createDecompressorStream(compression, compressedStripTileWidth, samplesInTile, adapter);
|
adapter = createDecompressorStream(compression, compressedStripTileWidth, samplesInTile, adapter);
|
||||||
adapter = createUnpredictorStream(predictor, compressedStripTileWidth, samplesInTile, bitsPerSample, adapter, imageInput.getByteOrder());
|
adapter = createUnpredictorStream(predictor, compressedStripTileWidth, samplesInTile, bitsPerSample, adapter, imageInput.getByteOrder());
|
||||||
adapter = createYCbCrUpsamplerStream(interpretation, planarConfiguration, b, rowRaster.getTransferType(), yCbCrSubsampling, yCbCrPos, colsInTile, adapter, imageInput.getByteOrder());
|
adapter = createYCbCrUpsamplerStream(interpretation, planarConfiguration, b, rowRaster.getTransferType(), yCbCrSubsampling, yCbCrPos, colsInTile, adapter, imageInput.getByteOrder());
|
||||||
@ -1125,8 +1126,8 @@ public final class TIFFImageReader extends ImageReaderBase {
|
|||||||
if (needsBitPadding) {
|
if (needsBitPadding) {
|
||||||
// We'll pad "odd" bitsPerSample streams to the smallest data type (byte/short/int) larger than the input
|
// We'll pad "odd" bitsPerSample streams to the smallest data type (byte/short/int) larger than the input
|
||||||
adapter = bitsPerSample < 8
|
adapter = bitsPerSample < 8
|
||||||
? new BitPaddingStream(adapter, 1, samplesInTile * bitsPerSample, colsInTile, imageInput.getByteOrder())
|
? new BitPaddingStream(adapter, 1, samplesInTile * bitsPerSample, colsInTile, imageInput.getByteOrder())
|
||||||
: new BitPaddingStream(adapter, 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
|
// According to the spec, short/long/etc should follow order of containing stream
|
||||||
@ -1170,120 +1171,16 @@ public final class TIFFImageReader extends ImageReaderBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
case TIFFCustom.COMPRESSION_WEBP:
|
||||||
case TIFFExtension.COMPRESSION_JPEG:
|
case TIFFExtension.COMPRESSION_JPEG:
|
||||||
// JPEG ('new-style' JPEG)
|
readUsingDelegate(imageIndex, compression, interpretation, width, height, tilesAcross, tilesDown, stripTileWidth, stripTileHeight, srcRegion, stripTileOffsets, stripTileByteCounts, param, destination, samplesInTile);
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
case TIFFExtension.COMPRESSION_OLD_JPEG: {
|
||||||
case TIFFExtension.COMPRESSION_OLD_JPEG:
|
|
||||||
// JPEG ('old-style' JPEG, later overridden in Technote2)
|
// JPEG ('old-style' JPEG, later overridden in Technote2)
|
||||||
// http://www.remotesensing.org/libtiff/TIFFTechNote2.html
|
// 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
|
// 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);
|
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);
|
throw new IIOException("Unknown TIFF JPEGProcessingMode value: " + mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
jpegReader = createJPEGDelegate();
|
ImageReader jpegReader = createJPEGDelegate();
|
||||||
jpegParam = (JPEGImageReadParam) jpegReader.getDefaultReadParam();
|
ImageReadParam jpegParam = jpegReader.getDefaultReadParam();
|
||||||
|
|
||||||
// 513/JPEGInterchangeFormat (may be absent or 0)
|
// 513/JPEGInterchangeFormat (may be absent or 0)
|
||||||
int jpegOffset = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT, -1);
|
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(
|
try (ImageInputStream stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(asList(
|
||||||
new ByteArrayInputStream(jpegHeader),
|
new ByteArrayInputStream(jpegHeader),
|
||||||
createStreamAdapter(imageInput, len),
|
createStreamAdapter(imageInput, len),
|
||||||
new ByteArrayInputStream(new byte[]{(byte) 0xff, (byte) 0xd9}) // EOI
|
new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI
|
||||||
))))) {
|
))))) {
|
||||||
jpegReader.setInput(stream);
|
jpegReader.setInput(stream);
|
||||||
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
|
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
|
||||||
@ -1436,7 +1333,6 @@ public final class TIFFImageReader extends ImageReaderBase {
|
|||||||
|
|
||||||
srcRow += rowsInTile;
|
srcRow += rowsInTile;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// The hard way: Read tables and re-create a full JFIF stream
|
// 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);
|
long[] yCbCrSubSampling = getValueAsLongArray(TIFF.TAG_YCBCR_SUB_SAMPLING, "YCbCrSubSampling", false);
|
||||||
int subsampling = yCbCrSubSampling != null
|
int subsampling = yCbCrSubSampling != null
|
||||||
? (int) ((yCbCrSubSampling[0] & 0xf) << 4 | yCbCrSubSampling[1] & 0xf)
|
? (int) ((yCbCrSubSampling[0] & 0xf) << 4 | yCbCrSubSampling[1] & 0xf)
|
||||||
: 0x22;
|
: 0x22;
|
||||||
|
|
||||||
// Read data
|
// Read data
|
||||||
processImageStarted(imageIndex);
|
processImageStarted(imageIndex);
|
||||||
@ -1589,7 +1485,7 @@ public final class TIFFImageReader extends ImageReaderBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
// Known, but unsupported compression types
|
// Known, but unsupported compression types
|
||||||
case TIFFCustom.COMPRESSION_NEXT:
|
case TIFFCustom.COMPRESSION_NEXT:
|
||||||
case TIFFCustom.COMPRESSION_CCITTRLEW:
|
case TIFFCustom.COMPRESSION_CCITTRLEW:
|
||||||
@ -1618,6 +1514,97 @@ public final class TIFFImageReader extends ImageReaderBase {
|
|||||||
return destination;
|
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,
|
private InputStream createYCbCrUpsamplerStream(int photometricInterpretation, int planarConfiguration, int plane, int transferType,
|
||||||
int[] yCbCrSubsampling, int yCbCrPos, int colsInTile, InputStream stream, ByteOrder byteOrder) {
|
int[] yCbCrSubsampling, int yCbCrPos, int colsInTile, InputStream stream, ByteOrder byteOrder) {
|
||||||
if (photometricInterpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
|
if (photometricInterpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
|
||||||
@ -1650,11 +1637,11 @@ public final class TIFFImageReader extends ImageReaderBase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IIOMetadata readJPEGMetadataSafe(final ImageReader jpegReader) throws IOException {
|
private IIOMetadata readJPEGMetadataSafe(final ImageReader jpegReader) {
|
||||||
try {
|
try {
|
||||||
return jpegReader.getImageMetadata(0);
|
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()));
|
processWarningOccurred(String.format("Could not read metadata for JPEG compressed TIFF (%s). Colors may look incorrect", e.getMessage()));
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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-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-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-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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Binary file not shown.
@ -418,8 +418,18 @@ final class WebPImageReader extends ImageReaderBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
types.add(rawImageType);
|
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();
|
return types.iterator();
|
||||||
}
|
}
|
||||||
|
@ -161,6 +161,12 @@
|
|||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>${project.groupId}</groupId>
|
||||||
|
<artifactId>imageio-webp</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>${project.groupId}</groupId>
|
<groupId>${project.groupId}</groupId>
|
||||||
<artifactId>imageio-core</artifactId>
|
<artifactId>imageio-core</artifactId>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user