diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEG.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEG.java index e30a2828..a2af4dc3 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEG.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEG.java @@ -88,7 +88,7 @@ public interface JPEG { // Start of Frame segment markers (SOFn). /** SOF0: Baseline DCT, Huffman coding. */ int SOF0 = 0xFFC0; - /** SOF0: Extended DCT, Huffman coding. */ + /** SOF1: Extended DCT, Huffman coding. */ int SOF1 = 0xFFC1; /** SOF2: Progressive DCT, Huffman coding. */ int SOF2 = 0xFFC2; diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/DelegateTileDecoder.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/DelegateTileDecoder.java index 0909edc9..a3dae3e1 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/DelegateTileDecoder.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/DelegateTileDecoder.java @@ -44,6 +44,10 @@ class DelegateTileDecoder extends TileDecoder { 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); diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/JPEGTables.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/JPEGTables.java deleted file mode 100644 index 2ddc7ac5..00000000 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/JPEGTables.java +++ /dev/null @@ -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 Harald Kuhr - * @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> SEGMENT_IDS = createSegmentIdsMap(); - - private JPEGQTable[] qTables; - private JPEGHuffmanTable[] dcHTables; - private JPEGHuffmanTable[] acHTables; - - private static Map> createSegmentIdsMap() { - Map> segmentIds = new HashMap>(); - segmentIds.put(JPEG.DQT, null); - segmentIds.put(JPEG.DHT, null); - - return Collections.unmodifiableMap(segmentIds); - } - - private final List 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 dc = new ArrayList(); - List ac = new ArrayList(); - - // 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; - } -} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/JPEGTileDecoder.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/JPEGTileDecoder.java index 8e1663a0..2271e464 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/JPEGTileDecoder.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/JPEGTileDecoder.java @@ -16,21 +16,21 @@ import java.util.function.Predicate; * @version $Id: JPEGTileDecoder.java,v 1.0 09/11/2023 haraldk Exp$ */ final class JPEGTileDecoder extends DelegateTileDecoder { - JPEGTileDecoder(final IIOReadWarningListener warningListener, final byte[] jpegTables, final int numTiles, final ImageReadParam originalParam, final Predicate needsConversion, final RasterConverter converter) throws IOException { + JPEGTileDecoder(final IIOReadWarningListener warningListener, final int compression, final byte[] jpegTables, final int numTiles, final ImageReadParam originalParam, final Predicate needsConversion, final RasterConverter converter) throws IOException { super(warningListener, "JPEG", originalParam, needsConversion, converter); if (jpegTables != null) { - // Whatever values I pass the reader as the read param, it never gets the same quality as if - // I just invoke jpegReader.getStreamMetadata(), so we'll do that... - delegate.setInput(new ByteArrayImageInputStream(jpegTables)); - // This initializes the tables and other internal settings for the reader, - // and is actually a feature of JPEG, see abbreviated streams: + // 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) { - warningListener.warningOccurred(delegate, "Missing JPEGTables for tiled/striped TIFF with compression: 7 (JPEG)"); + // 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... } } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java index 5d2b68d7..3fb0c3f9 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java @@ -1189,8 +1189,8 @@ public final class TIFFImageReader extends ImageReaderBase { // TODO: Perhaps use the jpegTables param to the tiledecoder instead of re-creating a full JFIF stream... TileStreamFactory tileStreamFactory = interChangeFormat - ? new JIFTileStreamFactory(stripTileOffsets, stripTileByteCounts) - : new JPEGTablesStreamFactory(stripTileOffsets, stripTileByteCounts, stripTileWidth, stripTileHeight, destRaster.getNumBands()); + ? 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; @@ -1297,17 +1297,18 @@ public final class TIFFImageReader extends ImageReaderBase { } } - final class JIFTileStreamFactory extends TileStreamFactory { + final class OldJPEGInterchangeFormatTileStreamFactory extends TileStreamFactory { private final byte[] jpegHeader; private long realJPEGOffset; - JIFTileStreamFactory(final long[] stripTileOffsets, final long[] stripTileByteCounts) throws IOException { + OldJPEGInterchangeFormatTileStreamFactory(final long[] stripTileOffsets, final long[] stripTileByteCounts) throws IOException { super(stripTileOffsets, stripTileByteCounts); // 513/JPEGInterchangeFormat (may be absent or 0) int jpegOffset = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT, -1); // 514/JPEGInterchangeFormatLength (may be absent, or incorrect) int jpegLength = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, -1); + // TODO: 515/JPEGRestartInterval (may be absent) // Currently ignored (for lossless only) @@ -1329,13 +1330,13 @@ public final class TIFFImageReader extends ImageReaderBase { // but has the correct offset to the JPEG stream in the StripOffsets tag. realJPEGOffset = jpegOffset; - short expectedSOI = (short) (imageInput.readByte() << 8 | imageInput.readByte()); - if (expectedSOI != (short) JPEG.SOI) { + int expectedSOI = ((imageInput.readByte() & 0xFF) << 8 | (imageInput.readByte() & 0xFF)); + if (expectedSOI != JPEG.SOI) { if (stripTileOffsets != null && stripTileOffsets.length == 1) { imageInput.seek(stripTileOffsets[0]); - expectedSOI = (short) (imageInput.readByte() << 8 | imageInput.readByte()); - if (expectedSOI == (short) JPEG.SOI) { + expectedSOI = ((imageInput.readByte() & 0xFF) << 8 | (imageInput.readByte() & 0xFF)); + if (expectedSOI == JPEG.SOI) { realJPEGOffset = stripTileOffsets[0]; } } @@ -1358,9 +1359,14 @@ public final class TIFFImageReader extends ImageReaderBase { // If the first tile stream starts with SOS, we'll correct offset/length imageInput.seek(stripTileOffsets[0]); - if ((short) (imageInput.readByte() << 8 | imageInput.readByte()) == (short) JPEG.SOS) { + if (((imageInput.readByte() & 0xFF) << 8 | (imageInput.readByte() & 0xFF)) == JPEG.SOS) { processWarningOccurred("Incorrect StripOffsets/TileOffsets, points to SOS marker, ignoring offsets/byte counts."); - int len = 2 + (imageInput.readUnsignedByte() << 8 | imageInput.readUnsignedByte()); + int len = 2 + ((imageInput.readByte() & 0xFF) << 8 | (imageInput.readByte() & 0xFF)); + + // TODO: There might be data between tables and the SOS here... + // We forward warnings from the JPEG reading delegate about "Corrupt JPEG data: N extraneous bytes before marker 0xda" (SOS), + // We didn't do this before, as we didn't add a warning listener for Old JPEG/6... + stripTileOffsets[0] += len; stripTileByteCounts[0] -= len; } @@ -1392,16 +1398,14 @@ public final class TIFFImageReader extends ImageReaderBase { } } - final class JPEGTablesStreamFactory extends TileStreamFactory { + final class OldJPEGTablesStreamFactory extends TileStreamFactory { private final int stripTileWidth; private final int stripTileHeight; private final int numBands; private final int subsampling; - private final byte[][] acTables; - private final byte[][] dcTables; - private final byte[][] qTables; + private final int processingMode; - JPEGTablesStreamFactory(final long[] stripTileOffsets, final long[] stripTileByteCounts, final int stripTileWidth, final int stripTileHeight, final int numBands) throws IOException { + 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; @@ -1409,69 +1413,14 @@ public final class TIFFImageReader extends ImageReaderBase { // The hard way: Read tables and re-create a full JFIF stream - // 519/JPEGQTables - // 520/JPEGDCTables - // 521/JPEGACTables - - // These fields were originally intended to point to a list of offsets to the quantization tables, one per - // component. Each table consists of 64 BYTES (one for each DCT coefficient in the 8x8 block). The - // quantization tables are stored in zigzag order, and are compatible with the quantization tables - // usually found in a JPEG stream DQT marker. - - // The original specification strongly recommended that, within the TIFF file, each component be - // assigned separate tables, and labelled this field as mandatory whenever the JPEGProc field specifies - // a DCT-based process. - - // We've seen old-style JPEG in TIFF files where some or all Table offsets, contained the JPEGQTables, - // 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 - // the Tables tags only as a last resort, if no table data is found in a JPEGInterchangeFormat stream. - - // 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 - - long[] qTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_Q_TABLES, "JPEGQTables", true); - qTables = new byte[qTablesOffsets.length][64]; - for (int j = 0; j < qTables.length; j++) { - imageInput.seek(qTablesOffsets[j]); - imageInput.readFully(qTables[j]); - } - - long[] dcTablesOffsets = getValueAsLongArray(TIFF.TAG_OLD_JPEG_DC_TABLES, "JPEGDCTables", true); - 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); - acTables = new byte[acTablesOffsets.length][]; - for (int j = 0; j < acTables.length; j++) { - imageInput.seek(acTablesOffsets[j]); - byte[] lengths = new byte[16]; - - imageInput.readFully(lengths); - - int length = 0; - for (int i = 0; i < 16; i++) { - length += lengths[i] & 0xff; - } - - acTables[j] = new byte[16 + length]; - System.arraycopy(lengths, 0, acTables[j], 0, 16); - imageInput.readFully(acTables[j], 16, length); + // 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); @@ -1488,7 +1437,7 @@ public final class TIFFImageReader extends ImageReaderBase { // If the tile stream starts with SOS... if (tileIndex == 0) { - if ((short) (imageInput.readByte() << 8 | imageInput.readByte()) == (short) JPEG.SOS) { + if (((imageInput.readByte() & 0xFF) << 8 | (imageInput.readByte() & 0xFF)) == JPEG.SOS) { imageInput.seek(stripTileOffsets[tileIndex] + 14); // TODO: Read from SOS length from stream, in case of gray/CMYK length -= 14; } @@ -1499,7 +1448,7 @@ public final class TIFFImageReader extends ImageReaderBase { return ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration( asList( - createJFIFStream(numBands, stripTileWidth, stripTileHeight, qTables, dcTables, acTables, subsampling), + createJFIFStream(numBands, stripTileWidth, stripTileHeight, processingMode, subsampling), createStreamAdapter(imageInput, length), new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI ) @@ -1512,51 +1461,67 @@ public final class TIFFImageReader extends ImageReaderBase { IIOReadWarningListener warningListener = (source, warning) -> processWarningOccurred(warning); switch (compression) { - case TIFFExtension.COMPRESSION_JPEG: - // New style JPEG - case TIFFExtension.COMPRESSION_OLD_JPEG: { + case TIFFExtension.COMPRESSION_OLD_JPEG: // JPEG ('old-style' JPEG, later overridden in Technote2) // http://www.remotesensing.org/libtiff/TIFFTechNote2.html + case TIFFExtension.COMPRESSION_JPEG: + // New style JPEG - // JPEG_TABLES should be a full JPEG 'abbreviated table specification', containing: - // SOI, DQT, DHT, (optional markers that we ignore)..., EOI byte[] jpegTables = null; if (compression == TIFFExtension.COMPRESSION_JPEG) { + // 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 { - // 512/JPEGProc: 1=Baseline, 14=Lossless (with Huffman coding), no default, although 1 is assumed if absent - int mode = getValueAsIntWithDefault(TIFF.TAG_OLD_JPEG_PROC, TIFFExtension.JPEG_PROC_BASELINE); - switch (mode) { - case TIFFExtension.JPEG_PROC_BASELINE: - case TIFFExtension.JPEG_PROC_LOSSLESS: - break; // Supported - default: - throw new IIOException("Unknown TIFF JPEGProcessingMode value: " + mode); - } + if (currentIFD.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT) == null) { + // 519/JPEGQTables + // 520/JPEGDCTables + // 521/JPEGACTables - // TODO: Consider re-factoring the super-complicated stream stuff to just using jpegTable also for old style? + // These fields were originally intended to point to a list of offsets to the quantization tables, one per + // component. Each table consists of 64 BYTES (one for each DCT coefficient in the 8x8 block). The + // quantization tables are stored in zigzag order, and are compatible with the quantization tables + // usually found in a JPEG stream DQT marker. + + // The original specification strongly recommended that, within the TIFF file, each component be + // assigned separate tables, and labelled this field as mandatory whenever the JPEGProc field specifies + // a DCT-based process. + + // We've seen old-style JPEG in TIFF files where some or all Table offsets, contained the JPEGQTables, + // 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 + // 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 needsConversion = (reader) -> needsCSConversion(compression, interpretation, readJPEGMetadataSafe(reader)); RasterConverter normalize = (raster) -> normalizeColor(interpretation, samplesInTile, raster); - return new JPEGTileDecoder(warningListener, jpegTables, numTiles, param, needsConversion, normalize); - } + 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); } @@ -1566,6 +1531,108 @@ public final class TIFFImageReader extends ImageReaderBase { } } + 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), + // use only the first occurrence, and update selectors in SOF0 and SOS + + byte[][] qTables = new byte[qTablesOffsets.length][64]; + for (int j = 0; j < qTables.length; j++) { + imageInput.seek(qTablesOffsets[j]); + imageInput.readFully(qTables[j]); + } + + byte[][] acTables = new byte[acTablesOffsets.length][]; + for (int j = 0; j < acTables.length; j++) { + imageInput.seek(acTablesOffsets[j]); + byte[] lengths = new byte[16]; + + imageInput.readFully(lengths); + + int length = 0; + for (int i = 0; i < 16; i++) { + length += lengths[i] & 0xff; + } + + acTables[j] = new byte[16 + length]; + System.arraycopy(lengths, 0, acTables[j], 0, 16); + imageInput.readFully(acTables[j], 16, length); + } + + 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); + } + + return createJPEGTables(qTables, dcTables, acTables); + } + + private static byte[] createJPEGTables(byte[][] qTables, byte[][] dcTables, byte[][] acTables) throws IOException { + 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 + } + + // 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.EOI); + } + + return stream.toByteArray(); + } + private InputStream createYCbCrUpsamplerStream(int photometricInterpretation, int planarConfiguration, int plane, int transferType, int[] yCbCrSubsampling, int yCbCrPos, int colsInTile, InputStream stream, ByteOrder byteOrder) { if (photometricInterpretation == TIFFExtension.PHOTOMETRIC_YCBCR) { @@ -1765,84 +1832,43 @@ public final class TIFFImageReader extends ImageReaderBase { return nodes.getLength() >= 1 ? (IIOMetadataNode) nodes.item(0) : null; } - private ImageReader createJPEGDelegate() throws IOException { - // We'll just use the default (first) reader - // If it's the TwelveMonkeys one, we will be able to read JPEG Lossless etc. - Iterator readers = ImageIO.getImageReadersByFormatName("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 { + private static InputStream createJFIFStream(int bands, int stripTileWidth, int stripTileHeight, int process, int subsampling) throws IOException { 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 + 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); + out.writeShort(process == 1 ? JPEG.SOF0 : JPEG.SOF3); + 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.writeShort(stripTileHeight); // height + out.writeShort(stripTileWidth); // width + out.writeByte(bands); // Number of components - // 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 + for (int comp = 0; comp < bands; comp++) { + out.writeByte(comp); // Component id + out.writeByte(comp == 0 ? subsampling : 0x11); // h/v subsampling + out.writeByte(comp); // Q table selector TODO: Consider merging if tables are equal + } + + out.writeShort(JPEG.SOS); + out.writeShort(6 + 2 * bands); // SOS length + out.writeByte(bands); // Num comp + + for (int component = 0; component < bands; component++) { + out.writeByte(component); // Comp id + out.writeByte(component == 0 ? component : 0x10 + (component & 0xf)); // dc/ac selector + } + + out.writeByte(0); // Spectral selection start + out.writeByte(0); // Spectral selection end + out.writeByte(0); // Approx high & low } - // 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.writeByte(8); // bits TODO: Consult raster/transfer type or BitsPerSample for 12/16 bits support - out.writeShort(stripTileHeight); // height - out.writeShort(stripTileWidth); // width - out.writeByte(bands); // Number of components - - for (int comp = 0; comp < bands; comp++) { - out.writeByte(comp); // Component id - out.writeByte(comp == 0 ? subsampling : 0x11); // h/v subsampling - out.writeByte(comp); // Q table selector TODO: Consider merging if tables are equal - } - - out.writeShort(JPEG.SOS); - out.writeShort(6 + 2 * bands); // SOS length - out.writeByte(bands); // Num comp - - for (int component = 0; component < bands; component++) { - out.writeByte(component); // Comp id - out.writeByte(component == 0 ? component : 0x10 + (component & 0xf)); // dc/ac selector - } - - out.writeByte(0); // Spectral selection start - out.writeByte(0); // Spectral selection end - out.writeByte(0); // Approx high & low - return stream.createInputStream(); } diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadataTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadataTest.java index 4c4d2b5f..caa79ce8 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadataTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadataTest.java @@ -29,28 +29,6 @@ */ 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.Entry; 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.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. * @@ -305,7 +304,7 @@ public class TIFFImageMetadataTest { @Test 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; diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java index 3d7a510e..b0cfd1a4 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java @@ -94,7 +94,7 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest