Compare commits

...

5 Commits

Author SHA1 Message Date
Harald Kuhr ce25d0e349 More clean-up 2024-04-11 20:10:33 +02:00
Harald Kuhr e2cc73f276 Fixed #859, Test clean-up, removed unused class. 2023-11-15 10:46:02 +01:00
Harald Kuhr 3623a7c5dd Further clean-up 2023-11-13 19:35:58 +01:00
Harald Kuhr ee424583c4 Clean-up 2023-11-11 15:03:09 +01:00
Harald Kuhr 1292c95040 Support for WebP in TIFF
Refactored tile reading for delegated formats.
2023-11-10 09:27:40 +01:00
15 changed files with 724 additions and 691 deletions
@@ -88,7 +88,7 @@ public interface JPEG {
// Start of Frame segment markers (SOFn). // Start of Frame segment markers (SOFn).
/** SOF0: Baseline DCT, Huffman coding. */ /** SOF0: Baseline DCT, Huffman coding. */
int SOF0 = 0xFFC0; int SOF0 = 0xFFC0;
/** SOF0: Extended DCT, Huffman coding. */ /** SOF1: Extended DCT, Huffman coding. */
int SOF1 = 0xFFC1; int SOF1 = 0xFFC1;
/** SOF2: Progressive DCT, Huffman coding. */ /** SOF2: Progressive DCT, Huffman coding. */
int SOF2 = 0xFFC2; int SOF2 = 0xFFC2;
+5
View File
@@ -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,99 @@
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;
private final Predicate<ImageReader> needsRasterConversion;
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> needsRasterConversion, final RasterConverter converter) throws IOException {
this(warningListener, createDelegate(format), originalParam, needsRasterConversion, converter);
}
private DelegateTileDecoder(final IIOReadWarningListener warningListener, final ImageReader delegate, final ImageReadParam originalParam, final Predicate<ImageReader> needsRasterConversion, final RasterConverter converter) {
super(warningListener);
this.delegate = notNull(delegate, "delegate");
delegate.addIIOReadWarningListener(warningListener);
if (TIFFImageReader.DEBUG) {
System.out.println("tile reading delegate: " + delegate);
}
param = delegate.getDefaultReadParam();
param.setSourceSubsampling(originalParam.getSourceXSubsampling(), originalParam.getSourceYSubsampling(), 0, 0);
this.needsRasterConversion = needsRasterConversion;
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("No ImageReader registered for '" + format + "' format");
}
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 = needsRasterConversion.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();
}
}
@@ -1,159 +0,0 @@
/*
* Copyright (c) 2012, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGQuality;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
import javax.imageio.IIOException;
import javax.imageio.plugins.jpeg.JPEGHuffmanTable;
import javax.imageio.plugins.jpeg.JPEGQTable;
import javax.imageio.stream.ImageInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.*;
/**
* JPEGTables
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: JPEGTables.java,v 1.0 11.05.12 09:13 haraldk Exp$
*/
class JPEGTables {
private static final int DHT_LENGTH = 16;
private static final Map<Integer, List<String>> SEGMENT_IDS = createSegmentIdsMap();
private JPEGQTable[] qTables;
private JPEGHuffmanTable[] dcHTables;
private JPEGHuffmanTable[] acHTables;
private static Map<Integer, List<String>> createSegmentIdsMap() {
Map<Integer, List<String>> segmentIds = new HashMap<Integer, List<String>>();
segmentIds.put(JPEG.DQT, null);
segmentIds.put(JPEG.DHT, null);
return Collections.unmodifiableMap(segmentIds);
}
private final List<JPEGSegment> segments;
public JPEGTables(ImageInputStream input) throws IOException {
segments = JPEGSegmentUtil.readSegments(input, SEGMENT_IDS);
}
public JPEGQTable[] getQTables() throws IOException {
if (qTables == null) {
qTables = JPEGQuality.getQTables(segments);
}
return qTables;
}
private void getHuffmanTables() throws IOException {
if (dcHTables == null || acHTables == null) {
List<JPEGHuffmanTable> dc = new ArrayList<JPEGHuffmanTable>();
List<JPEGHuffmanTable> ac = new ArrayList<JPEGHuffmanTable>();
// JPEG may contain multiple DHT marker segments
for (JPEGSegment segment : segments) {
if (segment.marker() != JPEG.DHT) {
continue;
}
DataInputStream data = new DataInputStream(segment.data());
int read = 0;
// A single DHT marker segment may contain multiple tables
while (read < segment.length()) {
int htInfo = data.read();
read++;
int num = htInfo & 0x0f; // 0-3
int type = htInfo >> 4; // 0 == DC, 1 == AC
if (type > 1) {
throw new IIOException("Bad DHT type: " + type);
}
if (num >= 4) {
throw new IIOException("Bad DHT table index: " + num);
}
else if (type == 0 ? dc.size() > num : ac.size() > num) {
throw new IIOException("Duplicate DHT table index: " + num);
}
// Read lengths as short array
short[] lengths = new short[DHT_LENGTH];
for (int i = 0; i < DHT_LENGTH; i++) {
lengths[i] = (short) data.readUnsignedByte();
}
read += lengths.length;
int sum = 0;
for (short length : lengths) {
sum += length;
}
// Expand table to short array
short[] table = new short[sum];
for (int j = 0; j < sum; j++) {
table[j] = (short) data.readUnsignedByte();
}
JPEGHuffmanTable hTable = new JPEGHuffmanTable(lengths, table);
if (type == 0) {
dc.add(num, hTable);
}
else {
ac.add(num, hTable);
}
read += sum;
}
}
dcHTables = dc.toArray(new JPEGHuffmanTable[dc.size()]);
acHTables = ac.toArray(new JPEGHuffmanTable[ac.size()]);
}
}
public JPEGHuffmanTable[] getDCHuffmanTables() throws IOException {
getHuffmanTables();
return dcHTables;
}
public JPEGHuffmanTable[] getACHuffmanTables() throws IOException {
getHuffmanTables();
return acHTables;
}
}
@@ -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$
*/
final class JPEGTileDecoder extends DelegateTileDecoder {
JPEGTileDecoder(final IIOReadWarningListener warningListener, final int compression, final byte[] jpegTables, final int numTiles, final ImageReadParam originalParam, final Predicate<ImageReader> needsConversion, final RasterConverter converter) throws IOException {
super(warningListener, "JPEG", originalParam, needsConversion, converter);
if (jpegTables != null) {
// 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.setInput(new ByteArrayImageInputStream(jpegTables));
delegate.getStreamMetadata();
}
else if (numTiles > 1) {
// TODO: This is not really a problem as long as we read ALL tiles, but we can't have random access in this case...
if (compression == TIFFExtension.COMPRESSION_JPEG) {
warningListener.warningOccurred(delegate, "Missing JPEGTables for tiled/striped TIFF with compression: 7 (JPEG)");
}
// ...and the JPEG reader might 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;
@@ -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;
@@ -1170,95 +1171,83 @@ public final class TIFFImageReader extends ImageReaderBase {
} }
break; break;
}
case TIFFExtension.COMPRESSION_JPEG: case TIFFExtension.COMPRESSION_JPEG:
// JPEG ('new-style' JPEG) case TIFFCustom.COMPRESSION_WEBP:
// TODO: Refactor all JPEG reading out to separate JPEG support class? case TIFFCustom.COMPRESSION_JBIG:
// TODO: Cache the JPEG reader for later use? Remember to reset to avoid resource leaks case TIFFCustom.COMPRESSION_JPEG2000:
readUsingDelegate(imageIndex, compression, interpretation, width, height, tilesAcross, tilesDown, stripTileWidth, stripTileHeight, srcRegion,
new PlainTileStreamFactory(stripTileOffsets, stripTileByteCounts), param, destination, samplesInTile);
break;
ImageReader jpegReader = createJPEGDelegate(); case TIFFExtension.COMPRESSION_OLD_JPEG:
// TODO: Use proper inner class + add case for old JPEG boolean interChangeFormat = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT, -1) >= 0;
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: if (!interChangeFormat) {
// SOI, DQT, DHT, (optional markers that we ignore)..., EOI processWarningOccurred("Old-style JPEG compressed TIFF without JPEGInterchangeFormat encountered. Attempting to re-create JFIF stream.");
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...
} }
// TODO: Perhaps use the jpegTables param to the tiledecoder instead of re-creating a full JFIF stream...
TileStreamFactory tileStreamFactory = interChangeFormat
? new OldJPEGInterchangeFormatTileStreamFactory(stripTileOffsets, stripTileByteCounts)
: new OldJPEGTablesStreamFactory(stripTileOffsets, stripTileByteCounts, stripTileWidth, stripTileHeight, destRaster.getNumBands());
readUsingDelegate(imageIndex, compression, interpretation, width, height, tilesAcross, tilesDown, stripTileWidth, stripTileHeight, srcRegion,
tileStreamFactory, param, destination, samplesInTile);
break;
case TIFFCustom.COMPRESSION_NEXT:
case TIFFCustom.COMPRESSION_CCITTRLEW:
case TIFFCustom.COMPRESSION_THUNDERSCAN:
case TIFFCustom.COMPRESSION_IT8CTPAD:
case TIFFCustom.COMPRESSION_IT8LW:
case TIFFCustom.COMPRESSION_IT8MP:
case TIFFCustom.COMPRESSION_IT8BL:
case TIFFCustom.COMPRESSION_PIXARFILM:
case TIFFCustom.COMPRESSION_PIXARLOG:
case TIFFCustom.COMPRESSION_DCS:
case TIFFCustom.COMPRESSION_SGILOG:
case TIFFCustom.COMPRESSION_SGILOG24:
throw new IIOException("Unsupported TIFF Compression value: " + compression);
default:
throw new IIOException("Unknown TIFF Compression value: " + compression);
}
// TODO: Convert color space from source to destination
processImageComplete();
return destination;
}
private void readUsingDelegate(int imageIndex, int compression, int interpretation, int width, int height,
int tilesAcross, int tilesDown, int stripTileWidth, int stripTileHeight, Rectangle srcRegion,
TileStreamFactory factory,
ImageReadParam param, BufferedImage destination, int samplesInTile) throws IOException {
// Read data // Read data
try (TileDecoder tileDecoder = createTileDecoder(param, compression, interpretation, tilesAcross * tilesDown, samplesInTile)) {
processImageStarted(imageIndex); // Better yet, would be to delegate read progress here... processImageStarted(imageIndex); // Better yet, would be to delegate read progress here...
int row = 0;
for (int y = 0; y < tilesDown; y++) { for (int y = 0; y < tilesDown; y++) {
int col = 0; int col = 0;
int rowsInTile = Math.min(stripTileHeight, height - srcRow); int rowsInTile = Math.min(stripTileHeight, height - row);
for (int x = 0; x < tilesAcross; x++) { for (int x = 0; x < tilesAcross; x++) {
int i = y * tilesAcross + x;
int colsInTile = Math.min(stripTileWidth, width - col); int colsInTile = Math.min(stripTileWidth, width - col);
// Read only tiles that lies within region // Read only tiles that lies within region
Rectangle tileRect = new Rectangle(col, srcRow, colsInTile, rowsInTile); Rectangle tileRect = new Rectangle(col, row, colsInTile, rowsInTile);
Rectangle intersection = tileRect.intersection(srcRegion); Rectangle intersection = tileRect.intersection(srcRegion);
if (!intersection.isEmpty()) { if (!intersection.isEmpty()) {
imageInput.seek(stripTileOffsets[i]); int tileIndex = y * tilesAcross + x;
int length = stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE; try (ImageInputStream tileStream = factory.createTileStream(tileIndex)) {
Point destinationOffset = new Point((intersection.x - srcRegion.x) / param.getSourceXSubsampling(), (intersection.y - srcRegion.y) / param.getSourceYSubsampling());
try (ImageInputStream subStream = new SubImageInputStream(imageInput, length)) { Rectangle sourceRegion = new Rectangle(intersection.x - col, intersection.y - row, intersection.width, intersection.height);
jpegReader.setInput(subStream); tileDecoder.decodeTile(tileStream, sourceRegion, destinationOffset, destination);
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);
}
} }
} }
@@ -1269,47 +1258,63 @@ public final class TIFFImageReader extends ImageReaderBase {
col += colsInTile; col += colsInTile;
} }
processImageProgress(100f * srcRow / height); processImageProgress(100f * row / height);
if (abortRequested()) { if (abortRequested()) {
processReadAborted(); processReadAborted();
break; break;
} }
srcRow += rowsInTile; row += rowsInTile;
}
}
} }
break; static abstract class TileStreamFactory {
final long[] stripTileOffsets;
final long[] stripTileByteCounts;
case TIFFExtension.COMPRESSION_OLD_JPEG: TileStreamFactory(final long[] stripTileOffsets, final long[] stripTileByteCounts) {
// JPEG ('old-style' JPEG, later overridden in Technote2) this.stripTileOffsets = stripTileOffsets;
// http://www.remotesensing.org/libtiff/TIFFTechNote2.html this.stripTileByteCounts = stripTileByteCounts;
// 512/JPEGProc: 1=Baseline, 14=Lossless (with Huffman coding), no default, although 1 is assumed if absent
int mode = getValueAsIntWithDefault(TIFF.TAG_OLD_JPEG_PROC, TIFFExtension.JPEG_PROC_BASELINE);
switch (mode) {
case TIFFExtension.JPEG_PROC_BASELINE:
case TIFFExtension.JPEG_PROC_LOSSLESS:
break; // Supported
default:
throw new IIOException("Unknown TIFF JPEGProcessingMode value: " + mode);
} }
jpegReader = createJPEGDelegate(); abstract ImageInputStream createTileStream(int tileIndex) throws IOException;
jpegParam = (JPEGImageReadParam) jpegReader.getDefaultReadParam(); }
final class PlainTileStreamFactory extends TileStreamFactory {
PlainTileStreamFactory(final long[] stripTileOffsets, final long[] stripTileByteCounts) {
super(stripTileOffsets, stripTileByteCounts);
}
@Override
public ImageInputStream createTileStream(final int tileIndex) throws IOException {
imageInput.seek(stripTileOffsets[tileIndex]);
int length = stripTileByteCounts != null ? (int) stripTileByteCounts[tileIndex] : Short.MAX_VALUE;
return new SubImageInputStream(imageInput, length);
}
}
final class OldJPEGInterchangeFormatTileStreamFactory extends TileStreamFactory {
private final byte[] jpegHeader;
private long realJPEGOffset;
OldJPEGInterchangeFormatTileStreamFactory(final long[] stripTileOffsets, final long[] stripTileByteCounts) throws IOException {
super(stripTileOffsets, stripTileByteCounts);
// 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);
// 514/JPEGInterchangeFormatLength (may be absent, or incorrect) // 514/JPEGInterchangeFormatLength (may be absent, or incorrect)
// TODO: We used to issue a warning if the value was incorrect, should we still do that?
int jpegLength = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, -1); int jpegLength = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, -1);
// TODO: 515/JPEGRestartInterval (may be absent) // TODO: 515/JPEGRestartInterval (may be absent)
// Currently ignored (for lossless only) // Currently ignored (for lossless only)
// 517/JPEGLosslessPredictors // 517/JPEGLosslessPredictors
// 518/JPEGPointTransforms // 518/JPEGPointTransforms
if (jpegOffset > 0) {
if (currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_Q_TABLES) != null if (currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_Q_TABLES) != null
|| currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_DC_TABLES) != null || currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_DC_TABLES) != null
|| currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_AC_TABLES) != null) { || currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_AC_TABLES) != null) {
@@ -1323,15 +1328,15 @@ public final class TIFFImageReader extends ImageReaderBase {
// NOTE: Some known TIFF encoder encodes bad JPEGInterchangeFormat tags, // NOTE: Some known TIFF encoder encodes bad JPEGInterchangeFormat tags,
// but has the correct offset to the JPEG stream in the StripOffsets tag. // but has the correct offset to the JPEG stream in the StripOffsets tag.
long realJPEGOffset = jpegOffset; realJPEGOffset = jpegOffset;
short expectedSOI = (short) (imageInput.readByte() << 8 | imageInput.readByte()); int expectedSOI = ((imageInput.readByte() & 0xFF) << 8 | (imageInput.readByte() & 0xFF));
if (expectedSOI != (short) JPEG.SOI) { if (expectedSOI != JPEG.SOI) {
if (stripTileOffsets != null && stripTileOffsets.length == 1) { if (stripTileOffsets != null && stripTileOffsets.length == 1) {
imageInput.seek(stripTileOffsets[0]); imageInput.seek(stripTileOffsets[0]);
expectedSOI = (short) (imageInput.readByte() << 8 | imageInput.readByte()); expectedSOI = ((imageInput.readByte() & 0xFF) << 8 | (imageInput.readByte() & 0xFF));
if (expectedSOI == (short) JPEG.SOI) { if (expectedSOI == JPEG.SOI) {
realJPEGOffset = stripTileOffsets[0]; realJPEGOffset = stripTileOffsets[0];
} }
} }
@@ -1345,28 +1350,40 @@ public final class TIFFImageReader extends ImageReaderBase {
} }
} }
byte[] jpegHeader; if (stripTileOffsets == null || stripTileOffsets.length == 1) {
if (stripTileOffsets == null || stripTileOffsets.length == 1 && realJPEGOffset == stripTileOffsets[0]) {
// In this case, we'll just read everything as a single tile // In this case, we'll just read everything as a single tile
jpegHeader = new byte[0]; jpegHeader = new byte[0];
if (stripTileOffsets != null) {
stripTileOffsets[0] = realJPEGOffset;
}
} }
else { else {
// Wang TIFF weirdness, see http://www.eztwain.com/wangtiff.htm
// If the first tile stream starts with SOS, we'll correct offset/length
imageInput.seek(stripTileOffsets[0]); imageInput.seek(stripTileOffsets[0]);
if ((short) (imageInput.readByte() << 8 | imageInput.readByte()) == (short) JPEG.SOS) { if (((imageInput.readByte() & 0xFF) << 8 | (imageInput.readByte() & 0xFF)) == JPEG.SOS) {
// Wang TIFF weirdness, see http://www.eztwain.com/wangtiff.htm
// If the first tile stream starts with SOS, we'll correct offset/length
processWarningOccurred("Incorrect StripOffsets/TileOffsets, points to SOS marker, ignoring offsets/byte counts."); processWarningOccurred("Incorrect StripOffsets/TileOffsets, points to SOS marker, ignoring offsets/byte counts.");
int len = 2 + (imageInput.readUnsignedByte() << 8 | imageInput.readUnsignedByte());
stripTileOffsets[0] += len;
stripTileByteCounts[0] -= len;
}
// We'll prepend each tile with a JFIF "header" (SOI...SOS) int sosLength = 2 + ((imageInput.readByte() & 0xFF) << 8 | (imageInput.readByte() & 0xFF));
// TODO: Validate that values make sense?
// We'll prepend each tile with a JFIF "header" (SOI...
jpegHeader = new byte[Math.max(0, jpegLength + sosLength)];
imageInput.seek(realJPEGOffset); imageInput.seek(realJPEGOffset);
jpegHeader = new byte[Math.max(0, (int) (stripTileOffsets[0] - realJPEGOffset))]; imageInput.readFully(jpegHeader, 0, jpegLength);
imageInput.readFully(jpegHeader); // ...SOS)
imageInput.seek(stripTileOffsets[0]);
imageInput.readFully(jpegHeader, jpegLength, sosLength);
stripTileOffsets[0] += sosLength;
stripTileByteCounts[0] -= sosLength;
}
else {
jpegHeader = new byte[0];
}
} }
// In case of single tile, make sure we read the entire JFIF stream // In case of single tile, make sure we read the entire JFIF stream
@@ -1374,74 +1391,103 @@ public final class TIFFImageReader extends ImageReaderBase {
processWarningOccurred("Incorrect StripByteCounts/TileByteCounts for single tile, using JPEGInterchangeFormatLength instead."); processWarningOccurred("Incorrect StripByteCounts/TileByteCounts for single tile, using JPEGInterchangeFormatLength instead.");
stripTileByteCounts[0] = jpegLength; stripTileByteCounts[0] = jpegLength;
} }
}
// Read data @Override
processImageStarted(imageIndex); public ImageInputStream createTileStream(final int tileIndex) throws IOException {
long length = stripTileByteCounts != null ? stripTileByteCounts[tileIndex] : Integer.MAX_VALUE;
for (int y = 0; y < tilesDown; y++) { imageInput.seek(stripTileOffsets != null ? stripTileOffsets[tileIndex] : realJPEGOffset);
int col = 0;
int rowsInTile = Math.min(stripTileHeight, height - srcRow);
for (int x = 0; x < tilesAcross; x++) { return ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(asList(
int colsInTile = Math.min(stripTileWidth, width - col);
int i = y * tilesAcross + x;
// Read only tiles that lies within region
if (new Rectangle(col, srcRow, colsInTile, rowsInTile).intersects(srcRegion)) {
int len = stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Integer.MAX_VALUE;
imageInput.seek(stripTileOffsets != null ? stripTileOffsets[i] : realJPEGOffset);
try (ImageInputStream stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(asList(
new ByteArrayInputStream(jpegHeader), new ByteArrayInputStream(jpegHeader),
createStreamAdapter(imageInput, len), createStreamAdapter(imageInput, length),
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));
jpegParam.setSourceSubsampling(xSub, ySub, 0, 0);
Point offset = new Point(col - srcRegion.x, srcRow - srcRegion.y);
if (needsCSConversion == null) {
needsCSConversion = needsCSConversion(compression, interpretation, readJPEGMetadataSafe(jpegReader));
}
if (!needsCSConversion) {
jpegParam.setDestinationOffset(offset);
jpegParam.setDestination(destination);
jpegReader.read(0, jpegParam);
}
else {
// Otherwise, it's likely CMYK or some other interpretation we don't need to convert.
// We'll have to use readAsRaster and later apply color space conversion ourselves
Raster raster = jpegReader.readRaster(0, jpegParam);
normalizeColor(interpretation, samplesInTile, ((DataBufferByte) raster.getDataBuffer()).getData());
destination.getRaster().setDataElements(offset.x, offset.y, raster);
}
} }
} }
if (abortRequested()) { final class OldJPEGTablesStreamFactory extends TileStreamFactory {
break; private final int stripTileWidth;
} private final int stripTileHeight;
private final int numBands;
private final int subsampling;
private final int processingMode;
col += colsInTile; OldJPEGTablesStreamFactory(final long[] stripTileOffsets, final long[] stripTileByteCounts, final int stripTileWidth, final int stripTileHeight, final int numBands) throws IOException {
} super(stripTileOffsets, stripTileByteCounts);
this.stripTileWidth = stripTileWidth;
this.stripTileHeight = stripTileHeight;
this.numBands = numBands;
processImageProgress(100f * srcRow / height);
if (abortRequested()) {
processReadAborted();
break;
}
srcRow += rowsInTile;
}
}
else {
// The hard way: Read tables and re-create a full JFIF stream // The hard way: Read tables and re-create a full JFIF stream
processWarningOccurred("Old-style JPEG compressed TIFF without JPEGInterchangeFormat encountered. Attempting to re-create JFIF stream.");
// 512/JPEGProc: 1=Baseline, 14=Lossless (with Huffman coding), no default, although 1 is assumed if absent
processingMode = getValueAsIntWithDefault(TIFF.TAG_OLD_JPEG_PROC, TIFFExtension.JPEG_PROC_BASELINE);
switch (processingMode) {
case TIFFExtension.JPEG_PROC_BASELINE:
case TIFFExtension.JPEG_PROC_LOSSLESS:
break; // Supported
default:
throw new IIOException("Unknown TIFF JPEGProcessingMode value: " + processingMode);
}
long[] yCbCrSubSampling = getValueAsLongArray(TIFF.TAG_YCBCR_SUB_SAMPLING, "YCbCrSubSampling", false);
subsampling = yCbCrSubSampling != null
? (int) ((yCbCrSubSampling[0] & 0xf) << 4 | yCbCrSubSampling[1] & 0xf)
: 0x22;
}
@Override
public ImageInputStream createTileStream(final int tileIndex) throws IOException {
long length = stripTileByteCounts != null ? stripTileByteCounts[tileIndex] : Integer.MAX_VALUE;
imageInput.seek(stripTileOffsets[tileIndex]);
// If the tile stream starts with SOS...
if (tileIndex == 0) {
if (((imageInput.readByte() & 0xFF) << 8 | (imageInput.readByte() & 0xFF)) == JPEG.SOS) {
int sosLength = 2 + ((imageInput.readByte() & 0xFF) << 8 | (imageInput.readByte() & 0xFF));
imageInput.seek(stripTileOffsets[tileIndex] + sosLength);
length -= sosLength;
}
else {
imageInput.seek(stripTileOffsets[tileIndex]);
}
}
return ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(
asList(
createJFIFStream(numBands, stripTileWidth, stripTileHeight, processingMode, subsampling),
createStreamAdapter(imageInput, length),
new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI
)
)));
}
}
private TileDecoder createTileDecoder(ImageReadParam param, int compression, final int interpretation, final int numTiles, final int samplesInTile) throws IOException {
try {
IIOReadWarningListener warningListener = (source, warning) -> processWarningOccurred(warning);
switch (compression) {
case TIFFExtension.COMPRESSION_OLD_JPEG:
// JPEG ('old-style' JPEG, later overridden in Technote2)
// http://www.remotesensing.org/libtiff/TIFFTechNote2.html
case TIFFExtension.COMPRESSION_JPEG:
// New style JPEG
byte[] jpegTables = null;
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);
jpegTables = tablesEntry != null ? (byte[]) tablesEntry.getValue() : null;
}
else {
if (currentIFD.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT) == null) {
// 519/JPEGQTables // 519/JPEGQTables
// 520/JPEGDCTables // 520/JPEGDCTables
// 521/JPEGACTables // 521/JPEGACTables
@@ -1459,37 +1505,63 @@ public final class TIFFImageReader extends ImageReaderBase {
// JPEGDCTables, and JPEGACTables tags are incorrect values beyond EOF. However, these files do always // JPEGDCTables, and JPEGACTables tags are incorrect values beyond EOF. However, these files do always
// seem to contain a useful JPEGInterchangeFormat tag. Therefore, we recommend a careful attempt to read // seem to contain a useful JPEGInterchangeFormat tag. Therefore, we recommend a careful attempt to read
// the Tables tags only as a last resort, if no table data is found in a JPEGInterchangeFormat stream. // the Tables tags only as a last resort, if no table data is found in a JPEGInterchangeFormat stream.
long[] qTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_Q_TABLES, "JPEGQTables", true);
long[] acTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_AC_TABLES, "JPEGACTables", true);
long[] dcTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_DC_TABLES, "JPEGDCTables", true);
jpegTables = createJPEGTables(imageInput, qTablesOffsets, acTablesOffsets, dcTablesOffsets);
}
}
Predicate<ImageReader> needsConversion = (reader) -> needsCSConversion(compression, interpretation, readJPEGMetadataSafe(reader));
RasterConverter normalize = (raster) -> normalizeColor(interpretation, samplesInTile, raster);
return new JPEGTileDecoder(warningListener, compression, jpegTables, numTiles, param, needsConversion, normalize);
case TIFFCustom.COMPRESSION_JBIG:
// TODO: Create interop test suite using third party plugin.
// LEAD Tools have one sample file: https://leadtools.com/support/forum/resource.ashx?a=545&b=1
// Haven't found any plugins. There is however a JBIG2 plugin...
return new DelegateTileDecoder(warningListener, "JBIG", param);
case TIFFCustom.COMPRESSION_JPEG2000:
// TODO: Create interop test suite using third party plugin
// LEAD Tools have one sample file: https://leadtools.com/support/forum/resource.ashx?a=545&b=1
// The open source JAI JP2K reader decodes this as a fully black image...
return new DelegateTileDecoder(warningListener, "JPEG2000", param);
case TIFFCustom.COMPRESSION_WEBP:
return new DelegateTileDecoder(warningListener, "WebP", param);
default:
throw new AssertionError("Unexpected TIFF Compression value: " + compression);
}
}
catch (IIOException e) {
throw new IIOException("Unsupported TIFF Compression value: " + compression, e);
}
}
private static byte[] createJPEGTables(ImageInputStream imageInput, long[] qTablesOffsets, long[] acTablesOffsets, long[] dcTablesOffsets) throws IOException {
// FastByteArrayOutputStream stream = new FastByteArrayOutputStream(
// 2 +
// 5 * qTablesOffsets.length + qTablesOffsets.length * 64 +
// 5 * acTablesOffsets.length + acTablesOffsets.length * dcTables[0].length +
// 5 * dcTablesOffsets.length + dcTablesOffsets.length * acTables[0].length +
// 2
// );
// TODO: Create stream with DQT, DHT directly, instead of first building in-memory tables...
// TODO: If any of the q/dc/ac tables are equal (or have same offset, even if "spec" violation), // TODO: If any of the q/dc/ac tables are equal (or have same offset, even if "spec" violation),
// use only the first occurrence, and update selectors in SOF0 and SOS // use only the first occurrence, and update selectors in SOF0 and SOS
long[] qTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_Q_TABLES, "JPEGQTables", true);
byte[][] qTables = new byte[qTablesOffsets.length][64]; byte[][] qTables = new byte[qTablesOffsets.length][64];
for (int j = 0; j < qTables.length; j++) { for (int j = 0; j < qTables.length; j++) {
imageInput.seek(qTablesOffsets[j]); imageInput.seek(qTablesOffsets[j]);
imageInput.readFully(qTables[j]); imageInput.readFully(qTables[j]);
} }
long[] dcTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_DC_TABLES, "JPEGDCTables", true);
byte[][] dcTables = new byte[dcTablesOffsets.length][];
for (int j = 0; j < dcTables.length; j++) {
imageInput.seek(dcTablesOffsets[j]);
byte[] lengths = new byte[16];
imageInput.readFully(lengths);
int length = 0;
for (int i = 0; i < 16; i++) {
length += lengths[i] & 0xff;
}
dcTables[j] = new byte[16 + length];
System.arraycopy(lengths, 0, dcTables[j], 0, 16);
imageInput.readFully(dcTables[j], 16, length);
}
long[] acTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_AC_TABLES, "JPEGACTables", true);
byte[][] acTables = new byte[acTablesOffsets.length][]; byte[][] acTables = new byte[acTablesOffsets.length][];
for (int j = 0; j < acTables.length; j++) { for (int j = 0; j < acTables.length; j++) {
imageInput.seek(acTablesOffsets[j]); imageInput.seek(acTablesOffsets[j]);
@@ -1507,115 +1579,69 @@ public final class TIFFImageReader extends ImageReaderBase {
imageInput.readFully(acTables[j], 16, length); imageInput.readFully(acTables[j], 16, length);
} }
long[] yCbCrSubSampling = getValueAsLongArray(TIFF.TAG_YCBCR_SUB_SAMPLING, "YCbCrSubSampling", false); byte[][] dcTables = new byte[dcTablesOffsets.length][];
int subsampling = yCbCrSubSampling != null for (int j = 0; j < dcTables.length; j++) {
? (int) ((yCbCrSubSampling[0] & 0xf) << 4 | yCbCrSubSampling[1] & 0xf) imageInput.seek(dcTablesOffsets[j]);
: 0x22; byte[] lengths = new byte[16];
// Read data imageInput.readFully(lengths);
processImageStarted(imageIndex);
for (int y = 0; y < tilesDown; y++) { int length = 0;
int col = 0; for (int i = 0; i < 16; i++) {
int rowsInTile = Math.min(stripTileHeight, height - srcRow); length += lengths[i] & 0xff;
for (int x = 0; x < tilesAcross; x++) {
int colsInTile = Math.min(stripTileWidth, width - col);
int i = y * tilesAcross + x;
// Read only tiles that lies within region
if (new Rectangle(col, srcRow, colsInTile, rowsInTile).intersects(srcRegion)) {
int length = stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE;
imageInput.seek(stripTileOffsets[i]);
// If the tile stream starts with SOS...
if (x == 0 && y == 0) {
if ((short) (imageInput.readByte() << 8 | imageInput.readByte()) == (short) JPEG.SOS) {
imageInput.seek(stripTileOffsets[i] + 14); // TODO: Read from SOS length from stream, in case of gray/CMYK
length -= 14;
}
else {
imageInput.seek(stripTileOffsets[i]);
}
} }
try (ImageInputStream stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration( dcTables[j] = new byte[16 + length];
asList( System.arraycopy(lengths, 0, dcTables[j], 0, 16);
createJFIFStream(destRaster.getNumBands(), stripTileWidth, stripTileHeight, qTables, dcTables, acTables, subsampling), imageInput.readFully(dcTables[j], 16, length);
createStreamAdapter(imageInput, length),
new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI
)
)))) {
jpegReader.setInput(stream);
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
jpegParam.setSourceSubsampling(xSub, ySub, 0, 0);
Point offset = new Point(col - srcRegion.x, srcRow - srcRegion.y);
if (needsCSConversion == null) {
needsCSConversion = needsCSConversion(compression, interpretation, readJPEGMetadataSafe(jpegReader));
} }
if (!needsCSConversion) { return createJPEGTables(qTables, dcTables, acTables);
jpegParam.setDestinationOffset(offset);
jpegParam.setDestination(destination);
jpegReader.read(0, jpegParam);
}
else {
// Otherwise, it's likely CMYK or some other interpretation we don't need to convert.
// We'll have to use readAsRaster and later apply color space conversion ourselves
Raster raster = jpegReader.readRaster(0, jpegParam);
normalizeColor(interpretation, samplesInTile, ((DataBufferByte) raster.getDataBuffer()).getData());
destination.getRaster().setDataElements(offset.x, offset.y, raster);
}
}
} }
if (abortRequested()) { private static byte[] createJPEGTables(byte[][] qTables, byte[][] dcTables, byte[][] acTables) throws IOException {
break; FastByteArrayOutputStream stream = new FastByteArrayOutputStream(
2 +
5 * qTables.length + qTables.length * qTables[0].length +
5 * dcTables.length + dcTables.length * dcTables[0].length +
5 * acTables.length + acTables.length * acTables[0].length +
2
);
try (DataOutputStream out = new DataOutputStream(stream)) {
out.writeShort(JPEG.SOI);
// TODO: Consider merging if tables are equal
for (int tableIndex = 0; tableIndex < qTables.length; tableIndex++) {
byte[] table = qTables[tableIndex];
out.writeShort(JPEG.DQT);
out.writeShort(3 + table.length); // DQT length
out.writeByte(tableIndex); // Q table id
out.write(table); // Table data
} }
col += colsInTile; // TODO: Consider merging if tables are equal
for (int tableIndex = 0; tableIndex < dcTables.length; tableIndex++) {
byte[] table = dcTables[tableIndex];
out.writeShort(JPEG.DHT);
out.writeShort(3 + table.length); // DHT length
out.writeByte(tableIndex & 0xf); // Huffman table id
out.write(table); // Table data
} }
processImageProgress(100f * srcRow / height); // TODO: Consider merging if tables are equal
for (int tableIndex = 0; tableIndex < acTables.length; tableIndex++) {
if (abortRequested()) { byte[] table = acTables[tableIndex];
processReadAborted(); out.writeShort(JPEG.DHT);
break; out.writeShort(3 + table.length); // DHT length
out.writeByte(0x10 + (tableIndex & 0xf)); // Huffman table id
out.write(table); // Table data
} }
srcRow += rowsInTile; out.writeShort(JPEG.EOI);
}
} }
break; return stream.toByteArray();
// Known, but unsupported compression types
case TIFFCustom.COMPRESSION_NEXT:
case TIFFCustom.COMPRESSION_CCITTRLEW:
case TIFFCustom.COMPRESSION_THUNDERSCAN:
case TIFFCustom.COMPRESSION_IT8CTPAD:
case TIFFCustom.COMPRESSION_IT8LW:
case TIFFCustom.COMPRESSION_IT8MP:
case TIFFCustom.COMPRESSION_IT8BL:
case TIFFCustom.COMPRESSION_PIXARFILM:
case TIFFCustom.COMPRESSION_PIXARLOG:
case TIFFCustom.COMPRESSION_DCS:
case TIFFCustom.COMPRESSION_JBIG: // Doable with JBIG plugin?
case TIFFCustom.COMPRESSION_SGILOG:
case TIFFCustom.COMPRESSION_SGILOG24:
case TIFFCustom.COMPRESSION_JPEG2000: // Doable with JPEG2000 plugin?
throw new IIOException("Unsupported TIFF Compression value: " + compression);
default:
throw new IIOException("Unknown TIFF Compression value: " + compression);
}
// TODO: Convert color space from source to destination
processImageComplete();
return destination;
} }
private InputStream createYCbCrUpsamplerStream(int photometricInterpretation, int planarConfiguration, int plane, int transferType, private InputStream createYCbCrUpsamplerStream(int photometricInterpretation, int planarConfiguration, int plane, int transferType,
@@ -1650,11 +1676,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;
@@ -1817,59 +1843,17 @@ public final class TIFFImageReader extends ImageReaderBase {
return nodes.getLength() >= 1 ? (IIOMetadataNode) nodes.item(0) : null; return nodes.getLength() >= 1 ? (IIOMetadataNode) nodes.item(0) : null;
} }
private ImageReader createJPEGDelegate() throws IOException { private static InputStream createJFIFStream(int bands, int stripTileWidth, int stripTileHeight, int process, int subsampling) 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("JPEG");
if (!readers.hasNext()) {
throw new IIOException("Could not instantiate JPEGImageReader");
}
return readers.next();
}
private static InputStream createJFIFStream(int bands, int stripTileWidth, int stripTileHeight, byte[][] qTables, byte[][] dcTables, byte[][] acTables, int subsampling) throws IOException {
FastByteArrayOutputStream stream = new FastByteArrayOutputStream( FastByteArrayOutputStream stream = new FastByteArrayOutputStream(
2 + 2 +
5 * qTables.length + qTables.length * qTables[0].length +
5 * dcTables.length + dcTables.length * dcTables[0].length +
5 * acTables.length + acTables.length * acTables[0].length +
2 + 2 + 6 + 3 * bands + 2 + 2 + 6 + 3 * bands +
8 + 2 * bands 2 + 6 + 2 * bands
); );
DataOutputStream out = new DataOutputStream(stream); try (DataOutputStream out = new DataOutputStream(stream)) {
out.writeShort(JPEG.SOI); out.writeShort(JPEG.SOI);
// TODO: Consider merging if tables are equal out.writeShort(process == 1 ? JPEG.SOF0 : JPEG.SOF3);
for (int tableIndex = 0; tableIndex < qTables.length; tableIndex++) {
byte[] table = qTables[tableIndex];
out.writeShort(JPEG.DQT);
out.writeShort(3 + table.length); // DQT length
out.writeByte(tableIndex); // Q table id
out.write(table); // Table data
}
// TODO: Consider merging if tables are equal
for (int tableIndex = 0; tableIndex < dcTables.length; tableIndex++) {
byte[] table = dcTables[tableIndex];
out.writeShort(JPEG.DHT);
out.writeShort(3 + table.length); // DHT length
out.writeByte(tableIndex & 0xf); // Huffman table id
out.write(table); // Table data
}
// TODO: Consider merging if tables are equal
for (int tableIndex = 0; tableIndex < acTables.length; tableIndex++) {
byte[] table = acTables[tableIndex];
out.writeShort(JPEG.DHT);
out.writeShort(3 + table.length); // DHT length
out.writeByte(0x10 + (tableIndex & 0xf)); // Huffman table id
out.write(table); // Table data
}
out.writeShort(JPEG.SOF0); // TODO: Use correct process for data
out.writeShort(2 + 6 + 3 * bands); // SOF0 len out.writeShort(2 + 6 + 3 * bands); // SOF0 len
out.writeByte(8); // bits TODO: Consult raster/transfer type or BitsPerSample for 12/16 bits support out.writeByte(8); // bits TODO: Consult raster/transfer type or BitsPerSample for 12/16 bits support
out.writeShort(stripTileHeight); // height out.writeShort(stripTileHeight); // height
@@ -1894,6 +1878,7 @@ public final class TIFFImageReader extends ImageReaderBase {
out.writeByte(0); // Spectral selection start out.writeByte(0); // Spectral selection start
out.writeByte(0); // Spectral selection end out.writeByte(0); // Spectral selection end
out.writeByte(0); // Approx high & low out.writeByte(0); // Approx high & low
}
return stream.createInputStream(); return stream.createInputStream();
} }
@@ -2178,6 +2163,19 @@ public final class TIFFImageReader extends ImageReaderBase {
} }
} }
private void normalizeColor(int interpretation, int numBands, Raster raster) throws IOException {
switch (raster.getTransferType()) {
case DataBuffer.TYPE_BYTE:
normalizeColor(interpretation, numBands, ((DataBufferByte) raster.getDataBuffer()).getData());
break;
case DataBuffer.TYPE_USHORT:
normalizeColor(interpretation, numBands, ((DataBufferUShort) raster.getDataBuffer()).getData());
break;
default:
throw new IllegalStateException("Unsupported transfer type: " + raster.getTransferType());
}
}
private void normalizeColor(int photometricInterpretation, int numBands, byte[] data) throws IOException { private void normalizeColor(int photometricInterpretation, int numBands, byte[] data) throws IOException {
switch (photometricInterpretation) { switch (photometricInterpretation) {
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO: case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
@@ -0,0 +1,34 @@
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;
import static com.twelvemonkeys.lang.Validate.notNull;
/**
* 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 = notNull(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;
}
}
@@ -29,28 +29,6 @@
*/ */
package com.twelvemonkeys.imageio.plugins.tiff; package com.twelvemonkeys.imageio.plugins.tiff;
import static com.twelvemonkeys.imageio.plugins.tiff.TIFFImageMetadataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME;
import static org.junit.Assert.*;
import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.imageio.ImageIO;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.spi.IIORegistry;
import javax.imageio.stream.ImageInputStream;
import org.junit.Test;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry; import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.tiff.Rational; import com.twelvemonkeys.imageio.metadata.tiff.Rational;
@@ -60,6 +38,27 @@ import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi; import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi;
import com.twelvemonkeys.lang.StringUtil; import com.twelvemonkeys.lang.StringUtil;
import org.junit.Test;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.imageio.ImageIO;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.spi.IIORegistry;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import static com.twelvemonkeys.imageio.plugins.tiff.TIFFImageMetadataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME;
import static org.junit.Assert.*;
/** /**
* TIFFImageMetadataTest. * TIFFImageMetadataTest.
* *
@@ -305,7 +304,7 @@ public class TIFFImageMetadataTest {
@Test @Test
public void testMergeTreeStandardFormat() throws IOException { public void testMergeTreeStandardFormat() throws IOException {
TIFFImageMetadata metadata = (TIFFImageMetadata) createMetadata("/tiff/zackthecat.tif"); TIFFImageMetadata metadata = (TIFFImageMetadata) createMetadata("/tiff/old-style-jpeg-zackthecat.tif");
String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName; String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName;
@@ -94,7 +94,7 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
new TestData(getClassLoaderResource("/tiff/ycbcr-cat.tif"), new Dimension(250, 325)), // YCbCr, LZW compressed new TestData(getClassLoaderResource("/tiff/ycbcr-cat.tif"), new Dimension(250, 325)), // YCbCr, LZW compressed
new TestData(getClassLoaderResource("/tiff/quad-jpeg.tif"), new Dimension(512, 384)), // YCbCr, JPEG compressed, striped new TestData(getClassLoaderResource("/tiff/quad-jpeg.tif"), new Dimension(512, 384)), // YCbCr, JPEG compressed, striped
new TestData(getClassLoaderResource("/tiff/smallliz.tif"), new Dimension(160, 160)), // YCbCr, Old-Style JPEG compressed (full JFIF stream) new TestData(getClassLoaderResource("/tiff/smallliz.tif"), new Dimension(160, 160)), // YCbCr, Old-Style JPEG compressed (full JFIF stream)
new TestData(getClassLoaderResource("/tiff/zackthecat.tif"), new Dimension(234, 213)), // YCbCr, Old-Style JPEG compressed (tables, no JFIF stream) new TestData(getClassLoaderResource("/tiff/old-style-jpeg-zackthecat.tif"), new Dimension(234, 213)), // YCbCr, Old-Style JPEG compressed (tables, no JFIF stream)
new TestData(getClassLoaderResource("/tiff/test-single-gray-compression-type-2.tiff"), new Dimension(1728, 1146)), // Gray, CCITT type 2 compressed new TestData(getClassLoaderResource("/tiff/test-single-gray-compression-type-2.tiff"), new Dimension(1728, 1146)), // Gray, CCITT type 2 compressed
new TestData(getClassLoaderResource("/tiff/cramps-tile.tif"), new Dimension(800, 607)), // Gray/WhiteIsZero, uncompressed, striped & tiled... new TestData(getClassLoaderResource("/tiff/cramps-tile.tif"), new Dimension(800, 607)), // Gray/WhiteIsZero, uncompressed, striped & tiled...
new TestData(getClassLoaderResource("/tiff/lzw-long-strings-sample.tif"), new Dimension(316, 173)), // RGBA, LZW compressed w/predictor new TestData(getClassLoaderResource("/tiff/lzw-long-strings-sample.tif"), new Dimension(316, 173)), // RGBA, LZW compressed w/predictor
@@ -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
); );
} }
@@ -368,7 +370,7 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
@Test @Test
public void testReadOldStyleWangMultiStrip2() throws IOException { public void testReadOldStyleWangMultiStrip2() throws IOException {
TestData testData = new TestData(getClassLoaderResource("/tiff/662260-color.tif"), new Dimension(1600, 1200)); TestData testData = new TestData(getClassLoaderResource("/tiff/old-style-jpeg-662260-color.tif"), new Dimension(1600, 1200));
try (ImageInputStream stream = testData.getInputStream()) { try (ImageInputStream stream = testData.getInputStream()) {
TIFFImageReader reader = createReader(); TIFFImageReader reader = createReader();
@@ -382,7 +384,7 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
assertNotNull(image); assertNotNull(image);
assertEquals(testData.getDimension(0), new Dimension(image.getWidth(), image.getHeight())); assertEquals(testData.getDimension(0), new Dimension(image.getWidth(), image.getHeight()));
verify(warningListener, atLeastOnce()).warningOccurred(eq(reader), and(contains("Old-style JPEG"), contains("tables"))); verify(warningListener, atLeastOnce()).warningOccurred(eq(reader), and(contains("Old-style JPEG"), contains("tables")));
verify(warningListener, atLeastOnce()).warningOccurred(eq(reader), and(contains("Incorrect StripOffsets/TileOffsets"), contains("SOS marker"))); verify(warningListener, atLeastOnce()).warningOccurred(eq(reader), and(contains("Incorrect StripByteCounts/TileByteCounts"), contains("JPEGInterchangeFormatLength")));
} }
} }
@@ -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();
} }
+6
View File
@@ -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>