diff --git a/README.md b/README.md index 96260664..3c5739fa 100644 --- a/README.md +++ b/README.md @@ -480,12 +480,12 @@ To depend on the JPEG and TIFF plugin using Maven, add the following to your POM com.twelvemonkeys.imageio imageio-jpeg - 3.3.2 + 3.4 com.twelvemonkeys.imageio imageio-tiff - 3.3.2 + 3.4 @@ -493,52 +493,52 @@ To depend on the JPEG and TIFF plugin using Maven, add the following to your POM To depend on the JPEG and TIFF plugin in your IDE or program, add all of the following JARs to your class path: - twelvemonkeys-common-lang-3.3.2.jar - twelvemonkeys-common-io-3.3.2.jar - twelvemonkeys-common-image-3.3.2.jar - twelvemonkeys-imageio-core-3.3.2.jar - twelvemonkeys-imageio-metadata-3.3.2.jar - twelvemonkeys-imageio-jpeg-3.3.2.jar - twelvemonkeys-imageio-tiff-3.3.2.jar + twelvemonkeys-common-lang-3.4.jar + twelvemonkeys-common-io-3.4.jar + twelvemonkeys-common-image-3.4.jar + twelvemonkeys-imageio-core-3.4.jar + twelvemonkeys-imageio-metadata-3.4.jar + twelvemonkeys-imageio-jpeg-3.4.jar + twelvemonkeys-imageio-tiff-3.4.jar ### Links to prebuilt binaries -##### Latest version (3.2.x) +##### Latest version (3.4) Requires Java 7 or later. Common dependencies -* [common-lang-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/common/common-lang/3.3.2/common-lang-3.3.2.jar) -* [common-io-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/common/common-io/3.3.2/common-io-3.3.2.jar) -* [common-image-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/common/common-image/3.3.2/common-image-3.3.2.jar) +* [common-lang-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/common/common-lang/3.4/common-lang-3.4.jar) +* [common-io-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/common/common-io/3.4/common-io-3.4.jar) +* [common-image-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/common/common-image/3.4/common-image-3.4.jar) ImageIO dependencies -* [imageio-core-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-core/3.3.2/imageio-core-3.3.2.jar) -* [imageio-metadata-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-metadata/3.3.2/imageio-metadata-3.3.2.jar) +* [imageio-core-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-core/3.4/imageio-core-3.4.jar) +* [imageio-metadata-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-metadata/3.4/imageio-metadata-3.4.jar) ImageIO plugins -* [imageio-bmp-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-bmp/3.3.2/imageio-bmp-3.3.2.jar) -* [imageio-jpeg-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-jpeg/3.3.2/imageio-jpeg-3.3.2.jar) -* [imageio-tiff-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-tiff/3.3.2/imageio-tiff-3.3.2.jar) -* [imageio-pnm-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-pnm/3.3.2/imageio-pnm-3.3.2.jar) -* [imageio-psd-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-psd/3.3.2/imageio-psd-3.3.2.jar) -* [imageio-hdr-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-hdr/3.3.2/imageio-hdr-3.3.2.jar) -* [imageio-iff-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-iff/3.3.2/imageio-iff-3.3.2.jar) -* [imageio-pcx-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-pcx/3.3.2/imageio-pcx-3.3.2.jar) -* [imageio-pict-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-pict/3.3.2/imageio-pict-3.3.2.jar) -* [imageio-sgi-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-sgi/3.3.2/imageio-sgi-3.3.2.jar) -* [imageio-tga-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-tga/3.3.2/imageio-tga-3.3.2.jar) -* [imageio-icns-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-icns/3.3.2/imageio-icns-3.3.2.jar) -* [imageio-thumbsdb-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-thumbsdb/3.3.2/imageio-thumbsdb-3.3.2.jar) +* [imageio-bmp-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-bmp/3.4/imageio-bmp-3.4.jar) +* [imageio-jpeg-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-jpeg/3.4/imageio-jpeg-3.4.jar) +* [imageio-tiff-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-tiff/3.4/imageio-tiff-3.4.jar) +* [imageio-pnm-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-pnm/3.4/imageio-pnm-3.4.jar) +* [imageio-psd-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-psd/3.4/imageio-psd-3.4.jar) +* [imageio-hdr-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-hdr/3.4/imageio-hdr-3.4.jar) +* [imageio-iff-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-iff/3.4/imageio-iff-3.4.jar) +* [imageio-pcx-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-pcx/3.4/imageio-pcx-3.4.jar) +* [imageio-pict-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-pict/3.4/imageio-pict-3.4.jar) +* [imageio-sgi-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-sgi/3.4/imageio-sgi-3.4.jar) +* [imageio-tga-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-tga/3.4/imageio-tga-3.4.jar) +* [imageio-icns-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-icns/3.4/imageio-icns-3.4.jar) +* [imageio-thumbsdb-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-thumbsdb/3.4/imageio-thumbsdb-3.4.jar) ImageIO plugins requiring 3rd party libs -* [imageio-batik-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-batik/3.3.2/imageio-batik-3.3.2.jar) +* [imageio-batik-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-batik/3.4/imageio-batik-3.4.jar) Photoshop Path support for ImageIO -* [imageio-clippath-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-clippath/3.3.2/imageio-clippath-3.3.2.jar) +* [imageio-clippath-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-clippath/3.4/imageio-clippath-3.4.jar) Servlet support -* [servlet-3.3.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/servlet/servlet/3.3.2/servlet-3.3.2.jar) +* [servlet-3.4.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/servlet/servlet/3.4/servlet-3.4.jar) ##### Old version (3.0.x) diff --git a/contrib/src/main/java/com/twelvemonkeys/contrib/tiff/TIFFUtilities.java b/contrib/src/main/java/com/twelvemonkeys/contrib/tiff/TIFFUtilities.java index b99bfef1..d3d2e264 100644 --- a/contrib/src/main/java/com/twelvemonkeys/contrib/tiff/TIFFUtilities.java +++ b/contrib/src/main/java/com/twelvemonkeys/contrib/tiff/TIFFUtilities.java @@ -45,11 +45,9 @@ import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.IOException; +import java.io.*; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.List; @@ -358,13 +356,21 @@ public final class TIFFUtilities { } } + int compression = -1; + Entry compressionEntry = IFD.getEntryById(TIFF.TAG_COMPRESSION); + if (compressionEntry != null && compressionEntry.getValue() instanceof Number) { + compression = ((Number) compressionEntry.getValue()).shortValue(); + } + boolean rearrangedByteStrips = false; Entry oldJpegData = IFD.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT); Entry oldJpegDataLength = IFD.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); + long[] jpegByteCounts = null; + long[] jpegOffsets = null; if (oldJpegData != null && oldJpegData.valueCount() > 0) { // convert JPEGInterchangeFormat to new-style-JPEG - long[] jpegByteCounts = new long[0]; - long[] jpegOffsets = getValueAsLongArray(oldJpegData); + jpegByteCounts = new long[0]; + jpegOffsets = getValueAsLongArray(oldJpegData); if (oldJpegDataLength != null && oldJpegDataLength.valueCount() > 0) { jpegByteCounts = getValueAsLongArray(oldJpegDataLength); } @@ -388,6 +394,20 @@ public final class TIFFUtilities { newIFD.remove(oldJpegDataLength); rearrangedByteStrips = true; } + else if (offsets.length == 1 && oldJpegDataLength != null && (jpegOffsets[0] < offsets[0]) && (jpegOffsets[0] + jpegByteCounts[0]) > (offsets[0] + byteCounts[0])) { + + // ByteStrip contains only a part of JPEGInterchangeFormat + newOffsets = writeData(jpegOffsets, jpegByteCounts, outputStream); + + newIFD.remove(stripOffsetsEntry); + newIFD.add(new TIFFEntry(useTiles ? TIFF.TAG_TILE_OFFSETS : TIFF.TAG_STRIP_OFFSETS, newOffsets)); + newIFD.remove(stripByteCountsEntry); + newIFD.add(new TIFFEntry(useTiles ? TIFF.TAG_TILE_BYTE_COUNTS : TIFF.TAG_STRIP_BYTE_COUNTS, new int[]{(int) (jpegByteCounts[0])})); + + newIFD.remove(oldJpegData); + newIFD.remove(oldJpegDataLength); + rearrangedByteStrips = true; + } else if (oldJpegDataLength != null) { // multiple bytestrips // search for SOF on first strip and copy to each if needed @@ -423,7 +443,7 @@ public final class TIFFUtilities { byte[] buffer = new byte[(int) byteCounts[i]]; newByteCounts[i] = (int) (jpegInterchangeData.length + byteCounts[i]); stream.readFully(buffer); - if (buffer[0] != 0xff && buffer[1] != 0xda) { + if (buffer[0] != ((byte) 0xff) || buffer[1] != ((byte) 0xda)) { outputStream.write(sosMarker); newByteCounts[i] += sosMarker.length; } @@ -440,7 +460,58 @@ public final class TIFFUtilities { rearrangedByteStrips = true; } } + else if (compression == TIFFExtension.COMPRESSION_OLD_JPEG) { + // old-style but no JPEGInterchangeFormat + long[] yCbCrSubSampling = getValueAsLongArray(IFD.getEntryById(TIFF.TAG_YCBCR_SUB_SAMPLING)); + int subsampling = yCbCrSubSampling != null + ? (int) ((yCbCrSubSampling[0] & 0xf) << 4 | yCbCrSubSampling[1] & 0xf) + : 0x22; + int bands = ((Number) IFD.getEntryById(TIFF.TAG_SAMPLES_PER_PIXEL).getValue()).intValue(); + + int w = ((Number) IFD.getEntryById(TIFF.TAG_IMAGE_WIDTH).getValue()).intValue(); + int h = ((Number) IFD.getEntryById(TIFF.TAG_IMAGE_HEIGHT).getValue()).intValue(); + + int r = ((Number) (useTiles ? IFD.getEntryById(TIFF.TAG_TILE_HEIGTH) : IFD.getEntryById(TIFF.TAG_ROWS_PER_STRIP)).getValue()).intValue(); + int c = useTiles ? ((Number) IFD.getEntryById(TIFF.TAG_TILE_WIDTH).getValue()).intValue() : w; + + newOffsets = new int[offsets.length]; + int[] newByteCounts = new int[byteCounts.length]; + + // No JPEGInterchangeFormat + for (int i = 0; i < offsets.length; i++) { + byte[] start = new byte[2]; + stream.seek(offsets[i]); + stream.readFully(start); + newOffsets[i] = (int) outputStream.getStreamPosition(); + if (start[0] == ((byte) 0xff) && start[1] == ((byte) 0xd8)) { + // full image stream, nothing to do + writeData(stream, outputStream, offsets[i], byteCounts[i]); + } + else if (start[0] == ((byte) 0xff) && start[1] == ((byte) 0xda)) { + // starts with SOS + outputStream.writeShort(JPEG.SOI); + writeSOF0(outputStream, bands, c, r, subsampling); + writeData(stream, outputStream, offsets[i], byteCounts[i]); + outputStream.writeShort(JPEG.EOI); + } + else { + // raw data + outputStream.writeShort(JPEG.SOI); + writeSOF0(outputStream, bands, c, r, subsampling); + writeSOS(outputStream, bands); + writeData(stream, outputStream, offsets[i], byteCounts[i]); + outputStream.writeShort(JPEG.EOI); + } + newByteCounts[i] = ((int) outputStream.getStreamPosition()) - newOffsets[i]; + } + + newIFD.remove(stripOffsetsEntry); + newIFD.add(new TIFFEntry(useTiles ? TIFF.TAG_TILE_OFFSETS : TIFF.TAG_STRIP_OFFSETS, newOffsets)); + newIFD.remove(stripByteCountsEntry); + newIFD.add(new TIFFEntry(useTiles ? TIFF.TAG_TILE_BYTE_COUNTS : TIFF.TAG_STRIP_BYTE_COUNTS, newByteCounts)); + rearrangedByteStrips = true; + } if (!rearrangedByteStrips && stripOffsetsEntry != null && stripByteCountsEntry != null) { newOffsets = writeData(offsets, byteCounts, outputStream); @@ -457,12 +528,21 @@ public final class TIFFUtilities { oldJpegTableQ = IFD.getEntryById(TIFF.TAG_OLD_JPEG_Q_TABLES); oldJpegTableDC = IFD.getEntryById(TIFF.TAG_OLD_JPEG_DC_TABLES); oldJpegTableAC = IFD.getEntryById(TIFF.TAG_OLD_JPEG_AC_TABLES); - if (oldJpegTableQ != null || oldJpegTableDC != null || oldJpegTableAC != null) { + if ((oldJpegTableQ != null) || (oldJpegTableDC != null) || (oldJpegTableAC != null)) { if (IFD.getEntryById(TIFF.TAG_JPEG_TABLES) != null) { throw new IOException("Found old-style and new-style JPEGTables"); } - newIFD.add(mergeTables(oldJpegTableQ, oldJpegTableDC, oldJpegTableAC)); + boolean tablesInStream = jfifContainsTables(oldJpegTableQ, jpegOffsets, jpegByteCounts); + tablesInStream &= jfifContainsTables(oldJpegTableDC, jpegOffsets, jpegByteCounts); + tablesInStream &= jfifContainsTables(oldJpegTableAC, jpegOffsets, jpegByteCounts); + if (!tablesInStream) { + // merge them only to JPEGTables if they are not already contained within the stream + Entry jpegTables = mergeTables(oldJpegTableQ, oldJpegTableDC, oldJpegTableAC); + if (jpegTables != null) { + newIFD.add(jpegTables); + } + } if (oldJpegTableQ != null) { newIFD.remove(oldJpegTableQ); } @@ -474,17 +554,66 @@ public final class TIFFUtilities { } } - Entry compressionEntry = IFD.getEntryById(TIFF.TAG_COMPRESSION); - if(compressionEntry != null) { - Number compression = (Number) compressionEntry.getValue(); - if (compression.shortValue() == TIFFExtension.COMPRESSION_OLD_JPEG) { - newIFD.remove(compressionEntry); - newIFD.add(new TIFFEntry(TIFF.TAG_COMPRESSION, TIFF.TYPE_SHORT, TIFFExtension.COMPRESSION_JPEG)); - } + if (compressionEntry != null && compression == TIFFExtension.COMPRESSION_OLD_JPEG) { + newIFD.remove(compressionEntry); + newIFD.add(new TIFFEntry(TIFF.TAG_COMPRESSION, TIFF.TYPE_SHORT, TIFFExtension.COMPRESSION_JPEG)); } return newIFD; } + //TODO merge/extract from TIFFReader Jpeg/6 stream reconstruction + private void writeSOF0(ImageOutputStream outputStream, int bands, int width, int height, int subsampling) throws IOException { + outputStream.writeShort(JPEG.SOF0); // TODO: Use correct process for data + outputStream.writeShort(2 + 6 + 3 * bands); // SOF0 len + outputStream.writeByte(8); // bits TODO: Consult raster/transfer type or BitsPerSample for 12/16 bits support + outputStream.writeShort(height); // height + outputStream.writeShort(width); // width + outputStream.writeByte(bands); // Number of components + + for (int comp = 0; comp < bands; comp++) { + outputStream.writeByte(comp); // Component id + outputStream.writeByte(comp == 0 ? subsampling : 0x11); // h/v subsampling + outputStream.writeByte(comp); // Q table selector TODO: Consider merging if tables are equal, correct selection if only 1 or 2 valid tables are contained + } + } + + //TODO merge/extract from TIFFReader Jpeg/6 stream reconstruction + private void writeSOS(ImageOutputStream outputStream, int bands) throws IOException { + outputStream.writeShort(JPEG.SOS); + outputStream.writeShort(6 + 2 * bands); // SOS length + outputStream.writeByte(bands); // Num comp + + for (int component = 0; component < bands; component++) { + outputStream.writeByte(component); // Comp id + outputStream.writeByte(component == 0 ? component : 0x10 + (component & 0xf)); // dc/ac selector TODO: correct selection if only 1 or 2 valid tables are contained + } + + outputStream.writeByte(0); // Spectral selection start + outputStream.writeByte(0); // Spectral selection end + outputStream.writeByte(0); // Approx high & low + } + + private void writeData(ImageInputStream input, ImageOutputStream output, long offset, long length) throws IOException { + input.seek(offset); + byte[] buffer = new byte[(int) length]; + stream.readFully(buffer); + output.write(buffer); + } + + private boolean jfifContainsTables(Entry tableEntry, long[] jpegOffsets, long[] jpegLengths) throws IOException { + if (jpegLengths == null || jpegOffsets == null || jpegLengths.length == 0) return false; + if (tableEntry != null) { + long[] tableOffsets = getValueAsLongArray(tableEntry); + for (long offset : tableOffsets) { + if (offset < jpegOffsets[0] || offset > (jpegOffsets[0] + jpegLengths[0])) { + return false; + } + } + } + return true; + } + + //TODO merge/extract from TIFFReader Jpeg/6 stream reconstruction private Entry mergeTables(Entry qEntry, Entry dcEntry, Entry acEntry) throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); @@ -494,12 +623,16 @@ public final class TIFFUtilities { long[] off = getValueAsLongArray(qEntry); byte[] table = new byte[64]; for (int tableId = 0; tableId < off.length; tableId++) { - stream.seek(off[tableId]); - stream.readFully(table); - dos.writeShort(JPEG.DQT); - dos.writeShort(3 + 64); - dos.writeByte(tableId); - dos.write(table); + try { + stream.seek(off[tableId]); + stream.readFully(table); + dos.writeShort(JPEG.DQT); + dos.writeShort(3 + 64); + dos.writeByte(tableId); + dos.write(table); + } catch (EOFException e) { + // invalid table pointer, ignore + } } } @@ -507,30 +640,50 @@ public final class TIFFUtilities { if (dcEntry != null && dcEntry.valueCount() > 0) { long[] off = getValueAsLongArray(dcEntry); for (int tableId = 0; tableId < off.length; tableId++) { - stream.seek(off[tableId]); - byte[] table = readHUFFTable(); - dos.writeShort(JPEG.DHT); - dos.writeShort(3 + table.length); - dos.writeByte(tableId); - dos.write(table); + try { + stream.seek(off[tableId]); + byte[] table = readHUFFTable(); + if (table.length > (16 + 17)) { + // to long, table is invalid, just ignoe + continue; + } + dos.writeShort(JPEG.DHT); + dos.writeShort(3 + table.length); + dos.writeByte(tableId); + dos.write(table); + } catch (EOFException e) { + // invalid table pointer, ignore + } } } if (acEntry != null && acEntry.valueCount() > 0) { long[] off = getValueAsLongArray(acEntry); for (int tableId = 0; tableId < off.length; tableId++) { - stream.seek(off[tableId]); - byte[] table = readHUFFTable(); - dos.writeShort(JPEG.DHT); - dos.writeShort(3 + table.length); - dos.writeByte(16 | tableId); - dos.write(table); + try { + stream.seek(off[tableId]); + byte[] table = readHUFFTable(); + if (table.length > (16 + 256)) { + // to long, table is invalid, just ignoe + continue; + } + dos.writeShort(JPEG.DHT); + dos.writeShort(3 + table.length); + dos.writeByte(16 | tableId); + dos.write(table); + } catch (EOFException e) { + // invalid table pointer, ignore + } } } dos.writeShort(JPEG.EOI); bos.close(); + if (bos.size() == 4) { + // no valid tables, don't add + return null; + } return new TIFFEntry(TIFF.TAG_JPEG_TABLES, TIFF.TYPE_UNDEFINED, bos.toByteArray()); } @@ -540,15 +693,11 @@ public final class TIFFUtilities { stream.readFully(lengths); int numCodes = 0; for (int i = 0; i < lengths.length; i++) { - numCodes += lengths[i]; + numCodes += ((int) lengths[i]) & 0xff; } byte table[] = new byte[16 + numCodes]; System.arraycopy(lengths, 0, table, 0, 16); - int off = 16; - for (int i = 0; i < lengths.length; i++) { - stream.read(table, off, lengths[i]); - off += lengths[i]; - } + stream.readFully(table, 16, numCodes); return table; } @@ -559,7 +708,11 @@ public final class TIFFUtilities { stream.seek(offsets[i]); byte[] buffer = new byte[(int) byteCounts[i]]; - stream.readFully(buffer); + try { + stream.readFully(buffer); + } catch (EOFException e) { + // invalid strip length + } outputStream.write(buffer); } return newOffsets; @@ -571,7 +724,7 @@ public final class TIFFUtilities { if (entry.valueCount() == 1) { // For single entries, this will be a boxed type - value = new long[] {((Number) entry.getValue()).longValue()}; + value = new long[]{((Number) entry.getValue()).longValue()}; } else if (entry.getValue() instanceof short[]) { short[] shorts = (short[]) entry.getValue(); diff --git a/contrib/src/test/java/com/twelvemonkeys/contrib/tiff/TIFFUtilitiesTest.java b/contrib/src/test/java/com/twelvemonkeys/contrib/tiff/TIFFUtilitiesTest.java index 9d33983c..c9a81252 100644 --- a/contrib/src/test/java/com/twelvemonkeys/contrib/tiff/TIFFUtilitiesTest.java +++ b/contrib/src/test/java/com/twelvemonkeys/contrib/tiff/TIFFUtilitiesTest.java @@ -202,36 +202,39 @@ public class TIFFUtilitiesTest { } @Test - public void testMergeBogusInterchangeFormatLength() throws IOException { - String[] testFiles = new String[] { + public void testOldStyleJPEGTransform() throws IOException { + String[] testFiles = new String[]{ "/tiff/old-style-jpeg-bogus-jpeginterchangeformatlength.tif", // InterchangeFormat before StripOffset, length not including StripOffset "/tiff/old-style-jpeg-no-jpeginterchangeformatlength.tif", // missing JPEGInterChangeFormatLength and JPEGInterchangeFormat == StipOffset - "/tiff/old-style-jpeg-multiple-strips.tif" // InterchangeFormat with multiple strips + "/tiff/old-style-jpeg-multiple-strips.tif", // InterchangeFormat with multiple strips + "/contrib/tiff/old-style-jpeg-invalid-tables.tif", // AC/DC Tables are invalid (to long) and lie within the JPEGInterchangeFormat stream + "/contrib/tiff/smallliz.tif", // InterchangeFormat contains whole JPEG, ByteStrip only raw ImageData after SOS + "/contrib/tiff/WangJPEG.tif", // multiple strips, first strip contains SOS + "/contrib/tiff/zackthecat.tif" // No JPEGInterchangeFormat, ByteStrip contains only raw image data }; for (String testFile : testFiles) { - File output = File.createTempFile("imageiotest", ".tif"); - ImageOutputStream outputStream = ImageIO.createImageOutputStream(output); - InputStream inputStream1 = getClassLoaderResource(testFile).openStream(); - ImageInputStream imageInput1 = ImageIO.createImageInputStream(inputStream1); - InputStream inputStream2 = getClassLoaderResource(testFile).openStream(); - ImageInputStream imageInput2 = ImageIO.createImageInputStream(inputStream2); - ArrayList pages = new ArrayList<>(); - pages.addAll(TIFFUtilities.getPages(imageInput1)); - pages.addAll(TIFFUtilities.getPages(imageInput2)); - TIFFUtilities.writePages(outputStream, pages); + try { + File output = File.createTempFile("imageiotest", ".tif"); + ImageOutputStream outputStream = ImageIO.createImageOutputStream(output); + InputStream inputStream = getClassLoaderResource(testFile).openStream(); + ImageInputStream imageInput = ImageIO.createImageInputStream(inputStream); + List pages = TIFFUtilities.getPages(imageInput); + TIFFUtilities.writePages(outputStream, pages); - ImageInputStream testOutput = ImageIO.createImageInputStream(output); - ImageReader reader = ImageIO.getImageReaders(testOutput).next(); - reader.setInput(testOutput); - int numImages = reader.getNumImages(true); - for (int i = 0; i < numImages; i++) { - reader.read(i); + ImageInputStream testOutput = ImageIO.createImageInputStream(output); + ImageReader reader = ImageIO.getImageReaders(testOutput).next(); + reader.setInput(testOutput); + int numImages = reader.getNumImages(true); + for (int i = 0; i < numImages; i++) { + reader.read(i); + } + + imageInput.close(); + outputStream.close(); + } catch (Exception exc) { + throw new IOException(testFile, exc); } - - imageInput1.close(); - imageInput2.close(); - outputStream.close(); } } diff --git a/contrib/src/test/resources/contrib/tiff/WangJPEG.tif b/contrib/src/test/resources/contrib/tiff/WangJPEG.tif new file mode 100644 index 00000000..d1a98ede Binary files /dev/null and b/contrib/src/test/resources/contrib/tiff/WangJPEG.tif differ diff --git a/contrib/src/test/resources/contrib/tiff/old-style-jpeg-invalid-tables.tif b/contrib/src/test/resources/contrib/tiff/old-style-jpeg-invalid-tables.tif new file mode 100644 index 00000000..fdf51209 Binary files /dev/null and b/contrib/src/test/resources/contrib/tiff/old-style-jpeg-invalid-tables.tif differ diff --git a/contrib/src/test/resources/contrib/tiff/smallliz.tif b/contrib/src/test/resources/contrib/tiff/smallliz.tif new file mode 100644 index 00000000..ee9dbb0e Binary files /dev/null and b/contrib/src/test/resources/contrib/tiff/smallliz.tif differ diff --git a/contrib/src/test/resources/contrib/tiff/zackthecat.tif b/contrib/src/test/resources/contrib/tiff/zackthecat.tif new file mode 100644 index 00000000..15185b68 Binary files /dev/null and b/contrib/src/test/resources/contrib/tiff/zackthecat.tif differ diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/AbstractMetadata.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/AbstractMetadata.java index c8752efb..1e8ba89c 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/AbstractMetadata.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/AbstractMetadata.java @@ -130,7 +130,7 @@ public abstract class AbstractMetadata extends IIOMetadata implements Cloneable } throw new IllegalArgumentException( - String.format("Bad format name: \"%s\". Expected one of %s", formatName, Arrays.toString(metadataFormatNames)) + String.format("Unsupported format name: \"%s\". Expected one of %s", formatName, Arrays.toString(metadataFormatNames)) ); } diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java index 8e0ba318..b9deebd8 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java @@ -101,8 +101,16 @@ public final class ColorSpaces { private static final Map cache = new LRUHashMap<>(10); static { - // Force invocation of ProfileDeferralMgr.activateProfiles() to avoid JDK-6986863 - ICC_Profile.getInstance(ColorSpace.CS_sRGB).getData(); + try { + // Force invocation of ProfileDeferralMgr.activateProfiles() to avoid JDK-6986863 + ICC_Profile.getInstance(ColorSpace.CS_sRGB).getData(); + } + catch (Throwable disasters) { + System.err.println("ICC Color Profile not properly activated due to the exception below."); + System.err.println("Expect to see JDK-6986863 in action, and consider filing a bug report to your JRE provider."); + + disasters.printStackTrace(); + } } private ColorSpaces() {} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGLosslessDecoder.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGLosslessDecoder.java index 954349e9..695460c1 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGLosslessDecoder.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGLosslessDecoder.java @@ -68,10 +68,7 @@ final class JPEGLosslessDecoder { private int xLoc; private int yLoc; private int mask; - private int[] outputData; - private int[] outputRedData; - private int[] outputGreenData; - private int[] outputBlueData; + private int[][] outputData; private static final int IDCT_P[] = { 0, 5, 40, 16, 45, 2, 7, 42, @@ -148,8 +145,6 @@ final class JPEGLosslessDecoder { int[][] decode() throws IOException { int current, scanNum = 0; - final int pred[] = new int[10]; - int[][] outputRef; xLoc = 0; yLoc = 0; @@ -218,32 +213,27 @@ final class JPEGLosslessDecoder { xDim = frame.samplesPerLine; yDim = frame.lines; - outputRef = new int[numComp][]; + outputData = new int[numComp][]; - // TODO: Support 4 components (RGBA/YCCA/CMYK/YCCK), others? - if (numComp == 1) { - outputData = new int[xDim * yDim]; - outputRef[0] = outputData; + for (int componentIndex = 0; componentIndex < numComp; ++componentIndex) { + // not a good use of memory, but I had trouble packing bytes into int. some values exceeded 255. + outputData[componentIndex] = new int[xDim * yDim]; } - else { - outputRedData = new int[xDim * yDim]; // not a good use of memory, but I had trouble packing bytes into int. some values exceeded 255. - outputGreenData = new int[xDim * yDim]; - outputBlueData = new int[xDim * yDim]; - outputRef[0] = outputRedData; - outputRef[1] = outputGreenData; - outputRef[2] = outputBlueData; + final int firstValue[] = new int[numComp]; + for (int i = 0; i < numComp; i++) { + firstValue[i] = (1 << (precision - 1)); } + final int pred[] = new int[numComp]; + scanNum++; while (true) { // Decode one scan int temp[] = new int[1]; // to store remainder bits int index[] = new int[1]; - for (int i = 0; i < 10; i++) { - pred[i] = (1 << (precision - 1)); - } + System.arraycopy(firstValue, 0, pred, 0, numComp); if (restartInterval == 0) { current = decode(pred, temp, index); @@ -285,9 +275,10 @@ final class JPEGLosslessDecoder { readNumber(); current = input.readUnsignedShort(); } + // TODO oe: 05.05.2018 Is it correct loop? Content of outputData from previous iteration is always lost. } while ((current != JPEG.EOI) && ((xLoc < xDim) && (yLoc < yDim)) && (scanNum == 0)); - return outputRef; + return outputData; } private void processWarningOccured(String warning) { @@ -341,7 +332,7 @@ final class JPEGLosslessDecoder { return decodeRGB(prev, temp, index); } else { - return -1; + return decodeAny(prev, temp, index); } } @@ -353,6 +344,7 @@ final class JPEGLosslessDecoder { prev[0] = (1 << (frame.samplePrecision - 1)); } else { + final int[] outputData = this.outputData[0]; switch (selection) { case 2: prev[0] = getPreviousY(outputData); @@ -399,6 +391,9 @@ final class JPEGLosslessDecoder { } private int decodeRGB(final int prev[], final int temp[], final int index[]) throws IOException { + final int[] outputRedData = outputData[0]; + final int[] outputGreenData = outputData[1]; + final int[] outputBlueData = outputData[2]; switch (selection) { case 2: prev[0] = getPreviousY(outputRedData); @@ -437,6 +432,43 @@ final class JPEGLosslessDecoder { break; } + return decode0(prev, temp, index); + } + + private int decodeAny(final int prev[], final int temp[], final int index[]) throws IOException { + for (int componentIndex = 0; componentIndex < outputData.length; ++componentIndex) { + final int[] outputData = this.outputData[componentIndex]; + final int previous; + switch (selection) { + case 2: + previous = getPreviousY(outputData); + break; + case 3: + previous = getPreviousXY(outputData); + break; + case 4: + previous = (getPreviousX(outputData) + getPreviousY(outputData)) - getPreviousXY(outputData); + break; + case 5: + previous = getPreviousX(outputData) + ((getPreviousY(outputData) - getPreviousXY(outputData)) >> 1); + break; + case 6: + previous = getPreviousY(outputData) + ((getPreviousX(outputData) - getPreviousXY(outputData)) >> 1); + break; + case 7: + previous = (int) (((long) getPreviousX(outputData) + getPreviousY(outputData)) / 2); + break; + default: + previous = getPreviousX(outputData); + break; + } + prev[componentIndex] = previous; + } + + return decode0(prev, temp, index); + } + + private int decode0(int[] prev, int[] temp, int[] index) throws IOException { int value, actab[], dctab[]; int qtab[]; @@ -694,14 +726,17 @@ final class JPEGLosslessDecoder { if (numComp == 1) { outputSingle(pred); } - else { + else if (numComp == 3) { outputRGB(pred); } + else { + outputAny(pred); + } } private void outputSingle(final int pred[]) { if ((xLoc < xDim) && (yLoc < yDim)) { - outputData[(yLoc * xDim) + xLoc] = mask & pred[0]; + outputData[0][(yLoc * xDim) + xLoc] = mask & pred[0]; xLoc++; if (xLoc >= xDim) { @@ -713,9 +748,25 @@ final class JPEGLosslessDecoder { private void outputRGB(final int pred[]) { if ((xLoc < xDim) && (yLoc < yDim)) { - outputRedData[(yLoc * xDim) + xLoc] = pred[0]; - outputGreenData[(yLoc * xDim) + xLoc] = pred[1]; - outputBlueData[(yLoc * xDim) + xLoc] = pred[2]; + final int index = (yLoc * xDim) + xLoc; + outputData[0][index] = pred[0]; + outputData[1][index] = pred[1]; + outputData[2][index] = pred[2]; + xLoc++; + + if (xLoc >= xDim) { + yLoc++; + xLoc = 0; + } + } + } + + private void outputAny(final int pred[]) { + if ((xLoc < xDim) && (yLoc < yDim)) { + final int index = (yLoc * xDim) + xLoc; + for (int componentIndex = 0; componentIndex < outputData.length; ++componentIndex) { + outputData[componentIndex][index] = pred[componentIndex]; + } xLoc++; if (xLoc >= xDim) { diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFStreamMetadata.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFStreamMetadata.java index 5c4cc80a..5ac7df3f 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFStreamMetadata.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFStreamMetadata.java @@ -39,9 +39,12 @@ import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.stream.ImageOutputStream; import java.nio.ByteOrder; +import java.util.Arrays; import static com.twelvemonkeys.lang.Validate.notNull; +import static java.lang.String.format; import static java.nio.ByteOrder.BIG_ENDIAN; +import static java.util.Arrays.asList; /** * TIFFStreamMetadata. @@ -113,7 +116,7 @@ public final class TIFFStreamMetadata extends IIOMetadata { } } - private ByteOrder getByteOrder(final String value) throws IIOInvalidTreeException { + private ByteOrder getByteOrder(final String value) { switch (value) { case "BIG_ENDIAN": return ByteOrder.BIG_ENDIAN; @@ -138,6 +141,11 @@ public final class TIFFStreamMetadata extends IIOMetadata { } else if (streamMetadata != null) { TIFFStreamMetadata metadata = new TIFFStreamMetadata(); + + Validate.isTrue(asList(streamMetadata.getMetadataFormatNames()).contains(metadata.nativeMetadataFormatName), + format("Unsupported stream metadata format, expected %s: %s", metadata.nativeMetadataFormatName, + Arrays.toString(streamMetadata.getMetadataFormatNames()))); + // Will throw exception if stream format differs from native metadata.mergeTree(metadata.nativeMetadataFormatName, streamMetadata.getAsTree(metadata.nativeMetadataFormatName)); imageOutput.setByteOrder(metadata.byteOrder); diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFStreamMetadataTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFStreamMetadataTest.java index 88400a42..304d856a 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFStreamMetadataTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFStreamMetadataTest.java @@ -40,6 +40,7 @@ import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.stream.ImageOutputStream; import java.nio.ByteOrder; +import static com.twelvemonkeys.imageio.plugins.tiff.TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; import static com.twelvemonkeys.imageio.plugins.tiff.TIFFStreamMetadata.SUN_NATIVE_STREAM_METADATA_FORMAT_NAME; import static java.nio.ByteOrder.BIG_ENDIAN; import static java.nio.ByteOrder.LITTLE_ENDIAN; @@ -85,9 +86,10 @@ public class TIFFStreamMetadataTest { } @Test - public void testConfigureStreamForegin() throws IIOInvalidTreeException { + public void testConfigureStreamForeign() throws IIOInvalidTreeException { ImageOutputStream stream = mock(ImageOutputStream.class); IIOMetadata metadata = mock(IIOMetadata.class); + when(metadata.getMetadataFormatNames()).thenReturn(new String[]{SUN_NATIVE_STREAM_METADATA_FORMAT_NAME, "com_foo_supertiff_9.42"}); when(metadata.getAsTree(eq(SUN_NATIVE_STREAM_METADATA_FORMAT_NAME))).thenReturn(createForeignTree(LITTLE_ENDIAN)); TIFFStreamMetadata.configureStreamByteOrder(metadata, stream); @@ -95,6 +97,24 @@ public class TIFFStreamMetadataTest { verify(stream, only()).setByteOrder(LITTLE_ENDIAN); } + @Test + public void testConfigureStreamImageMetadata() throws IIOInvalidTreeException { + ImageOutputStream stream = mock(ImageOutputStream.class); + IIOMetadata metadata = mock(IIOMetadata.class); + when(metadata.getMetadataFormatNames()).thenReturn(new String[]{SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME}); + + try { + TIFFStreamMetadata.configureStreamByteOrder(metadata, stream); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException expected) { + assertNotNull(expected.getMessage()); + assertTrue(expected.getMessage().toLowerCase().contains("unsupported stream metadata format")); + assertTrue(expected.getMessage().contains("expected " + SUN_NATIVE_STREAM_METADATA_FORMAT_NAME)); + assertTrue(expected.getMessage().contains(SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME)); + } + } + private IIOMetadataNode createForeignTree(ByteOrder order) { IIOMetadataNode root = new IIOMetadataNode(SUN_NATIVE_STREAM_METADATA_FORMAT_NAME); IIOMetadataNode byteOrder = new IIOMetadataNode("ByteOrder");