diff --git a/contrib/src/main/java/com/twelvemonkeys/contrib/exif/EXIFUtilities.java b/contrib/src/main/java/com/twelvemonkeys/contrib/exif/EXIFUtilities.java new file mode 100644 index 00000000..c896992d --- /dev/null +++ b/contrib/src/main/java/com/twelvemonkeys/contrib/exif/EXIFUtilities.java @@ -0,0 +1,152 @@ +package com.twelvemonkeys.contrib.exif; + +import com.twelvemonkeys.image.ImageUtil; +import com.twelvemonkeys.imageio.ImageReaderBase; +import org.w3c.dom.NodeList; + +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.stream.ImageInputStream; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Iterator; + +import static com.twelvemonkeys.contrib.tiff.TIFFUtilities.applyOrientation; + +/** + * EXIFUtilities. + * + * @author Harald Kuhr + * @version : EXIFUtilities.java,v 1.0 23/06/2020 + */ +public class EXIFUtilities { + /** + * Reads image and metadata, applies Exif orientation to image, and returns everything as an {@code IIOImage}. + * + * @param input a {@code URL} + * @return an {@code IIOImage} containing the correctly oriented image and metadata including rotation info. + * @throws IOException if an error occurs during reading. + */ + public static IIOImage readWithOrientation(final URL input) throws IOException { + try (ImageInputStream stream = ImageIO.createImageOutputStream(input)) { + return readWithOrientation(stream); + } + } + + /** + * Reads image and metadata, applies Exif orientation to image, and returns everything as an {@code IIOImage}. + * + * @param input an {@code InputStream} + * @return an {@code IIOImage} containing the correctly oriented image and metadata including rotation info. + * @throws IOException if an error occurs during reading. + */ + public static IIOImage readWithOrientation(final InputStream input) throws IOException { + try (ImageInputStream stream = ImageIO.createImageOutputStream(input)) { + return readWithOrientation(stream); + } + } + + /** + * Reads image and metadata, applies Exif orientation to image, and returns everything as an {@code IIOImage}. + * + * @param input a {@code File} + * @return an {@code IIOImage} containing the correctly oriented image and metadata including rotation info. + * @throws IOException if an error occurs during reading. + */ + public static IIOImage readWithOrientation(final File input) throws IOException { + try (ImageInputStream stream = ImageIO.createImageOutputStream(input)) { + return readWithOrientation(stream); + } + } + + /** + * Reads image and metadata, applies Exif orientation to image, and returns everything as an {@code IIOImage}. + * + * @param input an {@code ImageInputStream} + * @return an {@code IIOImage} containing the correctly oriented image and metadata including rotation info. + * @throws IOException if an error occurs during reading. + */ + public static IIOImage readWithOrientation(final ImageInputStream input) throws IOException { + Iterator readers = ImageIO.getImageReaders(input); + if (!readers.hasNext()) { + return null; + } + + ImageReader reader = readers.next(); + try { + reader.setInput(input, true, false); + IIOImage image = reader.readAll(0, reader.getDefaultReadParam()); + + BufferedImage bufferedImage = ImageUtil.toBuffered(image.getRenderedImage()); + image.setRenderedImage(applyOrientation(bufferedImage, findImageOrientation(image.getMetadata()).value())); + + return image; + } + finally { + reader.dispose(); + } + } + + /** + * Finds the {@code ImageOrientation} tag, if any, and returns an {@link Orientation} based on its + * {@code value} attribute. + * If no match is found or the tag is not present, {@code Normal} (the default orientation) is returned. + * + * @param metadata an {@code IIOMetadata} object + * @return the {@code Orientation} matching the {@code value} attribute of the {@code ImageOrientation} tag, + * or {@code Normal}, never {@code null}. + * @see Orientation + * @see Standard (Plug-in Neutral) Metadata Format Specification + */ + public static Orientation findImageOrientation(final IIOMetadata metadata) { + if (metadata != null) { + IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + NodeList imageOrientations = root.getElementsByTagName("ImageOrientation"); + + if (imageOrientations != null && imageOrientations.getLength() > 0) { + IIOMetadataNode imageOrientation = (IIOMetadataNode) imageOrientations.item(0); + return Orientation.fromMetadataOrientation(imageOrientation.getAttribute("value")); + } + } + + return Orientation.Normal; + } + + public static void main(String[] args) throws IOException { + for (String arg : args) { + File input = new File(arg); + + // Read everything (similar to ImageReader.readAll(0, null)), but applies the correct image orientation + IIOImage image = readWithOrientation(input); + + // Finds the orientation as defined by the javax_imageio_1.0 format + Orientation orientation = findImageOrientation(image.getMetadata()); + + // Retrieve the image as a BufferedImage. The image is already rotated by the readWithOrientation method + // In this case it will already be a BufferedImage, so using a cast will also do + // (i.e.: BufferedImage bufferedImage = (BufferedImage) image.getRenderedImage()) + BufferedImage bufferedImage = ImageUtil.toBuffered(image.getRenderedImage()); + + // Demo purpose only, show image with orientation details in title + DisplayHelper.showIt(bufferedImage, input.getName() + ": " + orientation.name() + "/" + orientation.value()); + } + } + + // Don't do this... :-) Provided for convenience/demo only! + static abstract class DisplayHelper extends ImageReaderBase { + private DisplayHelper() { + super(null); + } + + protected static void showIt(BufferedImage image, String title) { + ImageReaderBase.showIt(image, title); + } + } +} diff --git a/contrib/src/main/java/com/twelvemonkeys/contrib/exif/Orientation.java b/contrib/src/main/java/com/twelvemonkeys/contrib/exif/Orientation.java new file mode 100644 index 00000000..3340147f --- /dev/null +++ b/contrib/src/main/java/com/twelvemonkeys/contrib/exif/Orientation.java @@ -0,0 +1,63 @@ +package com.twelvemonkeys.contrib.exif; + +import com.twelvemonkeys.contrib.tiff.TIFFUtilities; + +/** + * Orientation. + * + * @author Harald Kuhr + * @version : Orientation.java,v 1.0 10/07/2020 harald.kuhr + */ +public enum Orientation { + Normal(TIFFUtilities.TIFFBaseline.ORIENTATION_TOPLEFT), + FlipH(TIFFUtilities.TIFFExtension.ORIENTATION_TOPRIGHT), + Rotate180(TIFFUtilities.TIFFExtension.ORIENTATION_BOTRIGHT), + FlipV(TIFFUtilities.TIFFExtension.ORIENTATION_BOTLEFT), + FlipVRotate90(TIFFUtilities.TIFFExtension.ORIENTATION_LEFTTOP), + Rotate270(TIFFUtilities.TIFFExtension.ORIENTATION_RIGHTTOP), + FlipHRotate90(TIFFUtilities.TIFFExtension.ORIENTATION_RIGHTBOT), + Rotate90(TIFFUtilities.TIFFExtension.ORIENTATION_LEFTBOT); + + // name as defined in javax.imageio metadata + private final int value; // value as defined in TIFF spec + + Orientation(int value) { + this.value = value; + } + + public int value() { + return value; + } + + public static Orientation fromMetadataOrientation(final String orientationName) { + if (orientationName != null) { + try { + return valueOf(orientationName); + } + catch (IllegalArgumentException e) { + // Not found, try ignore case match, as some metadata implementations are known to return "normal" etc. + String lowerCaseName = orientationName.toLowerCase(); + + for (Orientation orientation : values()) { + if (orientation.name().toLowerCase().equals(lowerCaseName)) { + return orientation; + } + } + } + } + + // Metadata does not have other orientations, default to Normal + return Normal; + } + + public static Orientation fromTIFFOrientation(final int tiffOrientation) { + for (Orientation orientation : values()) { + if (orientation.value() == tiffOrientation) { + return orientation; + } + } + + // No other TIFF orientations possible, default to Normal + return Normal; + } +} diff --git a/contrib/src/test/java/com/twelvemonkeys/contrib/exif/OrientationTest.java b/contrib/src/test/java/com/twelvemonkeys/contrib/exif/OrientationTest.java new file mode 100644 index 00000000..956e8cb1 --- /dev/null +++ b/contrib/src/test/java/com/twelvemonkeys/contrib/exif/OrientationTest.java @@ -0,0 +1,75 @@ +package com.twelvemonkeys.contrib.exif; + +import org.junit.Test; + +import static com.twelvemonkeys.contrib.exif.Orientation.*; +import static org.junit.Assert.assertEquals; + +/** + * OrientationTest. + * + * @author Harald Kuhr + * @author last modified by : harald.kuhr$ + * @version : OrientationTest.java,v 1.0 10/07/2020 harald.kuhr Exp$ + */ +public class OrientationTest { + @Test + public void testFromMetadataOrientationNull() { + assertEquals(Normal, Orientation.fromMetadataOrientation(null)); + } + + @Test + public void testFromMetadataOrientation() { + assertEquals(Normal, Orientation.fromMetadataOrientation("Normal")); + assertEquals(Rotate90, Orientation.fromMetadataOrientation("Rotate90")); + assertEquals(Rotate180, Orientation.fromMetadataOrientation("Rotate180")); + assertEquals(Rotate270, Orientation.fromMetadataOrientation("Rotate270")); + assertEquals(FlipH, Orientation.fromMetadataOrientation("FlipH")); + assertEquals(FlipV, Orientation.fromMetadataOrientation("FlipV")); + assertEquals(FlipHRotate90, Orientation.fromMetadataOrientation("FlipHRotate90")); + assertEquals(FlipVRotate90, Orientation.fromMetadataOrientation("FlipVRotate90")); + } + + @Test + public void testFromMetadataOrientationIgnoreCase() { + assertEquals(Normal, Orientation.fromMetadataOrientation("normal")); + assertEquals(Rotate90, Orientation.fromMetadataOrientation("rotate90")); + assertEquals(Rotate180, Orientation.fromMetadataOrientation("ROTATE180")); + assertEquals(Rotate270, Orientation.fromMetadataOrientation("ROTATE270")); + assertEquals(FlipH, Orientation.fromMetadataOrientation("FLIPH")); + assertEquals(FlipV, Orientation.fromMetadataOrientation("flipv")); + assertEquals(FlipHRotate90, Orientation.fromMetadataOrientation("FLIPhrotate90")); + assertEquals(FlipVRotate90, Orientation.fromMetadataOrientation("fLiPVRotAte90")); + } + + @Test + public void testFromMetadataOrientationUnknown() { + assertEquals(Normal, Orientation.fromMetadataOrientation("foo")); + assertEquals(Normal, Orientation.fromMetadataOrientation("90")); + assertEquals(Normal, Orientation.fromMetadataOrientation("randomStringWithNumbers180")); + } + + @Test + public void testFromTIFFOrientation() { + assertEquals(Normal, Orientation.fromTIFFOrientation(1)); + assertEquals(FlipH, Orientation.fromTIFFOrientation(2)); + assertEquals(Rotate180, Orientation.fromTIFFOrientation(3)); + assertEquals(FlipV, Orientation.fromTIFFOrientation(4)); + assertEquals(FlipVRotate90, Orientation.fromTIFFOrientation(5)); + assertEquals(Rotate270, Orientation.fromTIFFOrientation(6)); + assertEquals(FlipHRotate90, Orientation.fromTIFFOrientation(7)); + assertEquals(Rotate90, Orientation.fromTIFFOrientation(8)); + } + + @Test + public void testFromTIFFOrientationUnknown() { + assertEquals(Normal, Orientation.fromTIFFOrientation(-1)); + assertEquals(Normal, Orientation.fromTIFFOrientation(0)); + assertEquals(Normal, Orientation.fromTIFFOrientation(9)); + for (int i = 10; i < 1024; i++) { + assertEquals(Normal, Orientation.fromTIFFOrientation(i)); + } + assertEquals(Normal, Orientation.fromTIFFOrientation(Integer.MAX_VALUE)); + assertEquals(Normal, Orientation.fromTIFFOrientation(Integer.MIN_VALUE)); + } +} \ No newline at end of file diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/HuffmanTable.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/HuffmanTable.java index de01a2cf..8181cc48 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/HuffmanTable.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/HuffmanTable.java @@ -35,27 +35,27 @@ package com.twelvemonkeys.imageio.plugins.jpeg; import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; import javax.imageio.IIOException; +import javax.imageio.plugins.jpeg.JPEGHuffmanTable; import java.io.DataInput; import java.io.IOException; final class HuffmanTable extends Segment { - private final int l[][][] = new int[4][2][16]; - private final int th[] = new int[4]; // 1: this table is present - final int v[][][][] = new int[4][2][16][200]; // tables - final int[][] tc = new int[4][2]; // 1: this table is present + private final short[][][] l = new short[4][2][16]; + private final short[][][][] v = new short[4][2][16][200]; // tables + private final boolean[][] tc = new boolean[4][2]; // 1: this table is present - static final int MSB = 0x80000000; + private static final int MSB = 0x80000000; private HuffmanTable() { super(JPEG.DHT); } - void buildHuffTables(final int[][][] HuffTab) throws IOException { + void buildHuffTables(final int[][][] huffTab) throws IOException { for (int t = 0; t < 4; t++) { for (int c = 0; c < 2; c++) { - if (tc[t][c] != 0) { - buildHuffTable(HuffTab[t][c], l[t][c], v[t][c]); + if (tc[t][c]) { + buildHuffTable(huffTab[t][c], l[t][c], v[t][c]); } } } @@ -68,7 +68,7 @@ final class HuffmanTable extends Segment { // V[i][j] Huffman Value (length=i) // Effect: // build up HuffTab[t][c] using L and V. - private void buildHuffTable(final int tab[], final int L[], final int V[][]) throws IOException { + private void buildHuffTable(final int[] tab, final short[] L, final short[][] V) throws IOException { int temp = 256; int k = 0; @@ -112,7 +112,7 @@ final class HuffmanTable extends Segment { for (int t = 0; t < tc.length; t++) { for (int c = 0; c < tc[t].length; c++) { - if (tc[t][c] != 0) { + if (tc[t][c]) { if (builder.length() > 4) { builder.append(", "); } @@ -149,11 +149,10 @@ final class HuffmanTable extends Segment { throw new IIOException("Unexpected JPEG Huffman Table class (> 2): " + c); } - table.th[t] = 1; - table.tc[t][c] = 1; + table.tc[t][c] = true; for (int i = 0; i < 16; i++) { - table.l[t][c][i] = data.readUnsignedByte(); + table.l[t][c][i] = (short) data.readUnsignedByte(); count++; } @@ -162,7 +161,7 @@ final class HuffmanTable extends Segment { if (count > length) { throw new IIOException("JPEG Huffman Table format error"); } - table.v[t][c][i][j] = data.readUnsignedByte(); + table.v[t][c][i][j] = (short) data.readUnsignedByte(); count++; } } @@ -174,4 +173,41 @@ final class HuffmanTable extends Segment { return table; } + + public boolean isPresent(int tableId, int tableClass) { + return tc[tableId][tableClass]; + } + + private short[] lengths(int tableId, int tableClass) { + // TODO: Consider stripping the 0s? + return l[tableId][tableClass]; + } + + private short[] tables(int tableId, int tableClass) { + // Find sum of lengths + short[] lengths = lengths(tableId, tableClass); + + int sumOfLengths = 0; + for (int length : lengths) { + sumOfLengths += length; + } + + // Flatten the tables + short[] tables = new short[sumOfLengths]; + + int pos = 0; + for (int i = 0; i < 16; i++) { + short[] table = v[tableId][tableClass][i]; + short length = lengths[i]; + + System.arraycopy(table, 0, tables, pos, length); + pos += length; + } + + return tables; + } + + JPEGHuffmanTable toNativeTable(int tableId, int tableClass) { + return new JPEGHuffmanTable(lengths(tableId, tableClass), tables(tableId, tableClass)); + } } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java index d6d39316..6da5f96a 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java @@ -113,7 +113,7 @@ final class JFXXThumbnailReader extends ThumbnailReader { } } - cachedThumbnail = pixelsExposed ? null : new SoftReference(thumbnail); + cachedThumbnail = pixelsExposed ? null : new SoftReference<>(thumbnail); return thumbnail; } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10Metadata.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10Metadata.java index c50e0aff..9da588f0 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10Metadata.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10Metadata.java @@ -30,10 +30,16 @@ package com.twelvemonkeys.imageio.plugins.jpeg; import com.twelvemonkeys.imageio.AbstractMetadata; +import com.twelvemonkeys.imageio.metadata.CompoundDirectory; +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.Entry; import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; +import com.twelvemonkeys.imageio.metadata.tiff.TIFF; import org.w3c.dom.Node; +import javax.imageio.IIOException; import javax.imageio.metadata.IIOMetadataNode; +import java.awt.color.ICC_Profile; import java.util.List; /** @@ -45,14 +51,30 @@ import java.util.List; */ class JPEGImage10Metadata extends AbstractMetadata { - // TODO: Clean up. Consider just making the meta data classes we were trying to avoid in the first place.... + // TODO: Create our own native format, which is simply markerSequence from the Sun format, with the segments as-is, in sequence... + // + add special case for app segments, containing appXX + identifier (ie. to or private final List segments; - JPEGImage10Metadata(List segments) { + private final Frame frame; + private final JFIF jfif; + private final AdobeDCT adobeDCT; + private final JFXX jfxx; + private final ICC_Profile embeddedICCProfile; + + private final CompoundDirectory exif; + + // TODO: Consider moving all the metadata stuff from the reader, over here... + JPEGImage10Metadata(final List segments, Frame frame, JFIF jfif, JFXX jfxx, ICC_Profile embeddedICCProfile, AdobeDCT adobeDCT, final CompoundDirectory exif) { super(true, JPEGImage10MetadataCleaner.JAVAX_IMAGEIO_JPEG_IMAGE_1_0, null, null, null); this.segments = segments; + this.frame = frame; + this.jfif = jfif; + this.adobeDCT = adobeDCT; + this.jfxx = jfxx; + this.embeddedICCProfile = embeddedICCProfile; + this.exif = exif; } @Override @@ -60,16 +82,53 @@ class JPEGImage10Metadata extends AbstractMetadata { IIOMetadataNode root = new IIOMetadataNode(JPEGImage10MetadataCleaner.JAVAX_IMAGEIO_JPEG_IMAGE_1_0); IIOMetadataNode jpegVariety = new IIOMetadataNode("JPEGvariety"); - root.appendChild(jpegVariety); - // TODO: If we have JFIF, append in JPEGvariety, but can't happen for lossless + boolean isJFIF = jfif != null; + if (isJFIF) { + IIOMetadataNode app0JFIF = new IIOMetadataNode("app0JFIF"); + app0JFIF.setAttribute("majorVersion", Integer.toString(jfif.majorVersion)); + app0JFIF.setAttribute("minorVersion", Integer.toString(jfif.minorVersion)); + app0JFIF.setAttribute("resUnits", Integer.toString(jfif.units)); + app0JFIF.setAttribute("Xdensity", Integer.toString(jfif.xDensity)); + app0JFIF.setAttribute("Ydensity", Integer.toString(jfif.yDensity)); + + app0JFIF.setAttribute("thumbWidth", Integer.toString(jfif.xThumbnail)); + app0JFIF.setAttribute("thumbHeight", Integer.toString(jfif.yThumbnail)); + + jpegVariety.appendChild(app0JFIF); + + // Due to format oddity, add JFXX and app2ICC as subnodes here... + // ...and ignore them below, if added... + apendJFXX(app0JFIF); + appendICCProfile(app0JFIF); + } + + root.appendChild(jpegVariety); + + appendMarkerSequence(root, segments, isJFIF); + + return root; + } + + private void appendMarkerSequence(IIOMetadataNode root, List segments, boolean isJFIF) { IIOMetadataNode markerSequence = new IIOMetadataNode("markerSequence"); root.appendChild(markerSequence); for (Segment segment : segments) switch (segment.marker) { - // SOF3 is the only one supported by now + case JPEG.SOF0: + case JPEG.SOF1: + case JPEG.SOF2: case JPEG.SOF3: + case JPEG.SOF5: + case JPEG.SOF6: + case JPEG.SOF7: + case JPEG.SOF9: + case JPEG.SOF10: + case JPEG.SOF11: + case JPEG.SOF13: + case JPEG.SOF14: + case JPEG.SOF15: Frame sofSegment = (Frame) segment; IIOMetadataNode sof = new IIOMetadataNode("sof"); @@ -96,13 +155,13 @@ class JPEGImage10Metadata extends AbstractMetadata { HuffmanTable huffmanTable = (HuffmanTable) segment; IIOMetadataNode dht = new IIOMetadataNode("dht"); - // Uses fixed tables... for (int i = 0; i < 4; i++) { - for (int j = 0; j < 2; j++) { - if (huffmanTable.tc[i][j] != 0) { + for (int c = 0; c < 2; c++) { + if (huffmanTable.isPresent(i, c)) { IIOMetadataNode dhtable = new IIOMetadataNode("dhtable"); - dhtable.setAttribute("class", String.valueOf(j)); + dhtable.setAttribute("class", String.valueOf(c)); dhtable.setAttribute("htableId", String.valueOf(i)); + dhtable.setUserObject(huffmanTable.toNativeTable(i, c)); dht.appendChild(dhtable); } } @@ -112,8 +171,28 @@ class JPEGImage10Metadata extends AbstractMetadata { break; case JPEG.DQT: - markerSequence.appendChild(new IIOMetadataNode("dqt")); - // TODO: + QuantizationTable quantizationTable = (QuantizationTable) segment; + IIOMetadataNode dqt = new IIOMetadataNode("dqt"); + + for (int i = 0; i < 4; i++) { + if (quantizationTable.isPresent(i)) { + IIOMetadataNode dqtable = new IIOMetadataNode("dqtable"); + dqtable.setAttribute("elementPrecision", quantizationTable.precision(i) != 16 ? "0" : "1"); // 0 = 8 bits, 1 = 16 bits + dqtable.setAttribute("qtableId", Integer.toString(i)); + dqtable.setUserObject(quantizationTable.toNativeTable(i)); + dqt.appendChild(dqtable); + } + } + markerSequence.appendChild(dqt); + + break; + + case JPEG.DRI: + RestartInterval restartInterval = (RestartInterval) segment; + IIOMetadataNode dri = new IIOMetadataNode("dri"); + dri.setAttribute("interval", Integer.toString(restartInterval.interval)); + markerSequence.appendChild(dri); + break; case JPEG.SOS: @@ -144,6 +223,25 @@ class JPEGImage10Metadata extends AbstractMetadata { break; + case JPEG.APP0: + if (segment instanceof JFIF) { + // Either already added, or we'll ignore it anyway... + break; + } + else if (isJFIF && segment instanceof JFXX) { + // Already added + break; + } + + // Else, fall through to unknown segment + + case JPEG.APP2: + if (isJFIF && segment instanceof ICCProfile) { + // Already added + break; + } + // Else, fall through to unknown segment + case JPEG.APP14: if (segment instanceof AdobeDCT) { AdobeDCT adobe = (AdobeDCT) segment; @@ -165,32 +263,149 @@ class JPEGImage10Metadata extends AbstractMetadata { break; } + } - return root; + private void appendICCProfile(IIOMetadataNode app0JFIF) { + if (embeddedICCProfile != null) { + IIOMetadataNode app2ICC = new IIOMetadataNode("app2ICC"); + app2ICC.setUserObject(embeddedICCProfile); + + app0JFIF.appendChild(app2ICC); + } + } + + private void apendJFXX(IIOMetadataNode app0JFIF) { + if (jfxx != null) { + IIOMetadataNode jfxxNode = new IIOMetadataNode("JFXX"); + app0JFIF.appendChild(jfxxNode); + + IIOMetadataNode app0JFXX = new IIOMetadataNode("app0JFXX"); + app0JFXX.setAttribute("extensionCode", Integer.toString(jfxx.extensionCode)); + jfxxNode.appendChild(app0JFXX); + + switch (jfxx.extensionCode) { + case JFXX.JPEG: + IIOMetadataNode thumbJPEG = new IIOMetadataNode("JFIFthumbJPEG"); + thumbJPEG.appendChild(new IIOMetadataNode("markerSequence")); + // TODO: Insert segments in marker sequence... +// List segments = JPEGSegmentUtil.readSegments(new ByteArrayImageInputStream(jfxx.thumbnail), JPEGSegmentUtil.ALL_SEGMENTS); + // Convert to Segment as in JPEGImageReader... +// appendMarkerSequence(thumbJPEG, segments, false); + + app0JFXX.appendChild(thumbJPEG); + + break; + + case JFXX.INDEXED: + IIOMetadataNode thumbPalette = new IIOMetadataNode("JFIFthumbPalette"); + thumbPalette.setAttribute("thumbWidth", Integer.toString(jfxx.thumbnail[0] & 0xFF)); + thumbPalette.setAttribute("thumbHeight", Integer.toString(jfxx.thumbnail[1] & 0xFF)); + app0JFXX.appendChild(thumbPalette); + break; + + case JFXX.RGB: + IIOMetadataNode thumbRGB = new IIOMetadataNode("JFIFthumbRGB"); + thumbRGB.setAttribute("thumbWidth", Integer.toString(jfxx.thumbnail[0] & 0xFF)); + thumbRGB.setAttribute("thumbHeight", Integer.toString(jfxx.thumbnail[1] & 0xFF)); + app0JFXX.appendChild(thumbRGB); + break; + } + } } @Override protected IIOMetadataNode getStandardChromaNode() { IIOMetadataNode chroma = new IIOMetadataNode("Chroma"); - for (Segment segment : segments) { - if (segment instanceof Frame) { - Frame sofSegment = (Frame) segment; - IIOMetadataNode colorSpaceType = new IIOMetadataNode("ColorSpaceType"); - colorSpaceType.setAttribute("name", sofSegment.componentsInFrame() == 1 ? "GRAY" : "RGB"); // TODO YCC, YCCK, CMYK etc - chroma.appendChild(colorSpaceType); + IIOMetadataNode colorSpaceType = new IIOMetadataNode("ColorSpaceType"); + colorSpaceType.setAttribute("name", getColorSpaceType()); + chroma.appendChild(colorSpaceType); - IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels"); - numChannels.setAttribute("value", String.valueOf(sofSegment.componentsInFrame())); - chroma.appendChild(numChannels); - - break; - } - } + IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels"); + numChannels.setAttribute("value", String.valueOf(frame.componentsInFrame())); + chroma.appendChild(numChannels); return chroma; } + private String getColorSpaceType() { + try { + JPEGColorSpace csType = JPEGImageReader.getSourceCSType(jfif, adobeDCT, frame); + + switch (csType) { + case Gray: + case GrayA: + return "GRAY"; + case YCbCr: + case YCbCrA: + return "YCbCr"; + case RGB: + case RGBA: + return "RGB"; + case PhotoYCC: + case PhotoYCCA: + return "PhotoYCC"; + case YCCK: + return "YCCK"; + case CMYK: + return "CMYK"; + default: + + } + } + catch (IIOException ignore) { + } + + return Integer.toString(frame.componentsInFrame(), 16) + "CLR"; + } + + private boolean hasAlpha() { + try { + JPEGColorSpace csType = JPEGImageReader.getSourceCSType(jfif, adobeDCT, frame); + + switch (csType) { + case GrayA: + case YCbCrA: + case RGBA: + case PhotoYCCA: + return true; + default: + + } + } + catch (IIOException ignore) { + } + + return false; + } + + private boolean isLossess() { + switch (frame.marker) { + case JPEG.SOF3: + case JPEG.SOF7: + case JPEG.SOF11: + case JPEG.SOF15: + return true; + default: + return false; + } + } + + @Override + protected IIOMetadataNode getStandardTransparencyNode() { + if (hasAlpha()) { + IIOMetadataNode transparency = new IIOMetadataNode("Transparency"); + + IIOMetadataNode alpha = new IIOMetadataNode("Alpha"); + alpha.setAttribute("value", "nonpremultipled"); + transparency.appendChild(alpha); + + return transparency; + } + + return null; + } + @Override protected IIOMetadataNode getStandardCompressionNode() { IIOMetadataNode compression = new IIOMetadataNode("Compression"); @@ -200,7 +415,7 @@ class JPEGImage10Metadata extends AbstractMetadata { compression.appendChild(compressionTypeName); IIOMetadataNode lossless = new IIOMetadataNode("Lossless"); - lossless.setAttribute("value", "TRUE"); // TODO: For lossless only + lossless.setAttribute("value", isLossess() ? "TRUE" : "FALSE"); compression.appendChild(lossless); IIOMetadataNode numProgressiveScans = new IIOMetadataNode("NumProgressiveScans"); @@ -215,12 +430,67 @@ class JPEGImage10Metadata extends AbstractMetadata { IIOMetadataNode dimension = new IIOMetadataNode("Dimension"); IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation"); - imageOrientation.setAttribute("value", "normal"); // TODO + imageOrientation.setAttribute("value", getExifOrientation(exif)); dimension.appendChild(imageOrientation); + if (jfif != null) { + // Aspect ratio + float xDensity = Math.max(1, jfif.xDensity); + float yDensity = Math.max(1, jfif.yDensity); + float aspectRatio = jfif.units == 0 ? xDensity / yDensity : yDensity / xDensity; + + IIOMetadataNode pixelAspectRatio = new IIOMetadataNode("PixelAspectRatio"); + pixelAspectRatio.setAttribute("value", Float.toString(aspectRatio)); + dimension.insertBefore(pixelAspectRatio, imageOrientation); // Keep order + + if (jfif.units != 0) { + // Pixel size + float scale = jfif.units == 1 ? 25.4F : 10.0F; // DPI or DPcm + + IIOMetadataNode horizontalPixelSize = new IIOMetadataNode("HorizontalPixelSize"); + horizontalPixelSize.setAttribute("value", Float.toString(scale / xDensity)); + dimension.appendChild(horizontalPixelSize); + + IIOMetadataNode verticalPixelSize = new IIOMetadataNode("VerticalPixelSize"); + verticalPixelSize.setAttribute("value", Float.toString(scale / yDensity)); + dimension.appendChild(verticalPixelSize); + } + } + return dimension; } + private String getExifOrientation(Directory exif) { + if (exif != null) { + Entry orientationEntry = exif.getEntryById(TIFF.TAG_ORIENTATION); + + if (orientationEntry != null) { + switch (((Number) orientationEntry.getValue()).intValue()) { + case 2: + return "FlipH"; + case 3: + return "Rotate180"; + case 4: + return "FlipV"; + case 5: + return "FlipVRotate90"; + case 6: + return "Rotate270"; + case 7: + return "FlipHRotate90"; + case 8: + return "Rotate90"; + case 0: + case 1: + default: + // Fall-through + } + } + } + + return "Normal"; + } + @Override protected IIOMetadataNode getStandardTextNode() { IIOMetadataNode text = new IIOMetadataNode("Text"); @@ -235,6 +505,10 @@ class JPEGImage10Metadata extends AbstractMetadata { } } + // TODO: Add the following from Exif (as in TIFFMetadata) + // DocumentName, ImageDescription, Make, Model, PageName, Software, Artist, HostComputer, InkNames, Copyright: + // /Text/TextEntry@keyword = field name, /Text/TextEntry@value = field value. + return text.hasChildNodes() ? text : null; } } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index 9fd5c2b2..48b82388 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -113,9 +113,6 @@ public final class JPEGImageReader extends ImageReaderBase { /** Internal constant for referring all APP segments */ static final int ALL_APP_MARKERS = -1; - /** Segment identifiers for the JPEG segments we care about reading. */ - private static final Map> SEGMENT_IDENTIFIERS = JPEGSegmentUtil.ALL_SEGMENTS; - /** Our JPEG reading delegate */ private final ImageReader delegate; @@ -534,7 +531,7 @@ public final class JPEGImageReader extends ImageReaderBase { return image; } - private JPEGColorSpace getSourceCSType(final JFIF jfif, final AdobeDCT adobeDCT, final Frame startOfFrame) throws IIOException { + static JPEGColorSpace getSourceCSType(final JFIF jfif, final AdobeDCT adobeDCT, final Frame startOfFrame) throws IIOException { // Adapted from libjpeg jdapimin.c: // Guess the input colorspace // (Wish JPEG committee had provided a real way to specify this...) @@ -717,7 +714,7 @@ public final class JPEGImageReader extends ImageReaderBase { private void initHeader(final int imageIndex) throws IOException { if (imageIndex < 0) { - throw new IllegalArgumentException("imageIndex < 0: " + imageIndex); + throw new IndexOutOfBoundsException("imageIndex < 0: " + imageIndex); } if (imageIndex == currentStreamIndex) { @@ -837,7 +834,7 @@ public final class JPEGImageReader extends ImageReaderBase { try { imageInput.seek(streamOffsets.get(currentStreamIndex)); - return JPEGSegmentUtil.readSegments(imageInput, SEGMENT_IDENTIFIERS); + return JPEGSegmentUtil.readSegments(imageInput, JPEGSegmentUtil.ALL_SEGMENTS); } catch (IIOException | IllegalArgumentException ignore) { if (DEBUG) { @@ -1218,38 +1215,9 @@ public final class JPEGImageReader extends ImageReaderBase { @Override public IIOMetadata getImageMetadata(int imageIndex) throws IOException { - // checkBounds needed, as we catch the IndexOutOfBoundsException below. - checkBounds(imageIndex); initHeader(imageIndex); - IIOMetadata imageMetadata; - - if (isLossless()) { - return new JPEGImage10Metadata(segments); - } - else { - try { - imageMetadata = delegate.getImageMetadata(0); - } - catch (IndexOutOfBoundsException knownIssue) { - // TMI-101: com.sun.imageio.plugins.jpeg.JPEGBuffer doesn't do proper sanity check of input data. - throw new IIOException("Corrupt JPEG data: Bad segment length", knownIssue); - } - catch (NegativeArraySizeException knownIssue) { - // Most likely from com.sun.imageio.plugins.jpeg.SOSMarkerSegment - throw new IIOException("Corrupt JPEG data: Bad component count", knownIssue); - } - - if (imageMetadata != null && Arrays.asList(imageMetadata.getMetadataFormatNames()).contains(JPEGImage10MetadataCleaner.JAVAX_IMAGEIO_JPEG_IMAGE_1_0)) { - if (metadataCleaner == null) { - metadataCleaner = new JPEGImage10MetadataCleaner(this); - } - - return metadataCleaner.cleanMetadata(imageMetadata); - } - } - - return imageMetadata; + return new JPEGImage10Metadata(segments, getSOF(), getJFIF(), getJFXX(), getEmbeddedICCProfile(true), getAdobeDCT(), getExif()); } @Override 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 695460c1..01656649 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 @@ -39,6 +39,7 @@ import javax.imageio.IIOException; import javax.imageio.stream.ImageInputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; final class JPEGLosslessDecoder { @@ -51,12 +52,12 @@ final class JPEGLosslessDecoder { private final QuantizationTable quantTable; private Scan scan; - private final int HuffTab[][][] = new int[4][2][MAX_HUFFMAN_SUBTREE * 256]; - private final int IDCT_Source[] = new int[64]; - private final int nBlock[] = new int[10]; // number of blocks in the i-th Comp in a scan - private final int[] acTab[] = new int[10][]; // ac HuffTab for the i-th Comp in a scan - private final int[] dcTab[] = new int[10][]; // dc HuffTab for the i-th Comp in a scan - private final int[] qTab[] = new int[10][]; // quantization table for the i-th Comp in a scan + private final int[][][] HuffTab = new int[4][2][MAX_HUFFMAN_SUBTREE * 256]; + private final int[] IDCT_Source = new int[64]; + private final int[] nBlock = new int[10]; // number of blocks in the i-th Comp in a scan + private final int[][] acTab = new int[10][]; // ac HuffTab for the i-th Comp in a scan + private final int[][] dcTab = new int[10][]; // dc HuffTab for the i-th Comp in a scan + private final int[][] qTab = new int[10][]; // quantization table for the i-th Comp in a scan private boolean restarting; private int marker; @@ -70,7 +71,7 @@ final class JPEGLosslessDecoder { private int mask; private int[][] outputData; - private static final int IDCT_P[] = { + private static final int[] IDCT_P = { 0, 5, 40, 16, 45, 2, 7, 42, 21, 56, 8, 61, 18, 47, 1, 4, 41, 23, 58, 13, 32, 24, 37, 10, @@ -80,16 +81,6 @@ final class JPEGLosslessDecoder { 50, 55, 25, 36, 11, 62, 14, 35, 28, 49, 52, 27, 38, 30, 51, 54 }; - private static final int TABLE[] = { - 0, 1, 5, 6, 14, 15, 27, 28, - 2, 4, 7, 13, 16, 26, 29, 42, - 3, 8, 12, 17, 25, 30, 41, 43, - 9, 11, 18, 24, 31, 40, 44, 53, - 10, 19, 23, 32, 39, 45, 52, 54, - 20, 22, 33, 38, 46, 51, 55, 60, - 21, 34, 37, 47, 50, 56, 59, 61, - 35, 36, 48, 49, 57, 58, 62, 63 - }; private static final int RESTART_MARKER_BEGIN = 0xFFD0; private static final int RESTART_MARKER_END = 0xFFD7; @@ -158,7 +149,7 @@ final class JPEGLosslessDecoder { huffTable.buildHuffTables(HuffTab); } - quantTable.enhanceTables(TABLE); + quantTable.enhanceTables(); current = input.readUnsignedShort(); @@ -185,11 +176,10 @@ final class JPEGLosslessDecoder { selection = scan.spectralSelStart; final Scan.Component[] scanComps = scan.components; - final int[][] quantTables = quantTable.quantTables; for (int i = 0; i < numComp; i++) { Frame.Component component = getComponentSpec(components, scanComps[i].scanCompSel); - qTab[i] = quantTables[component.qtSel]; + qTab[i] = quantTable.qTable(component.qtSel); nBlock[i] = component.vSub * component.hSub; int dcTabSel = scanComps[i].dcTabSel; @@ -220,18 +210,18 @@ final class JPEGLosslessDecoder { outputData[componentIndex] = new int[xDim * yDim]; } - final int firstValue[] = new int[numComp]; + final int[] firstValue = new int[numComp]; for (int i = 0; i < numComp; i++) { firstValue[i] = (1 << (precision - 1)); } - final int pred[] = new int[numComp]; + 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]; + int[] temp = new int[1]; // to store remainder bits + int[] index = new int[1]; System.arraycopy(firstValue, 0, pred, 0, numComp); @@ -288,7 +278,7 @@ final class JPEGLosslessDecoder { private boolean useACForDC(final int dcTabSel) { if (isLossless()) { for (HuffmanTable huffTable : huffTables) { - if (huffTable.tc[dcTabSel][0] == 0 && huffTable.tc[dcTabSel][1] != 0) { + if (!huffTable.isPresent(dcTabSel, 0) && huffTable.isPresent(dcTabSel, 1)) { return true; } } @@ -324,7 +314,7 @@ final class JPEGLosslessDecoder { return Scan.read(input, length); } - private int decode(final int prev[], final int temp[], final int index[]) throws IOException { + private int decode(final int[] prev, final int[] temp, final int[] index) throws IOException { if (numComp == 1) { return decodeSingle(prev, temp, index); } @@ -336,7 +326,7 @@ final class JPEGLosslessDecoder { } } - private int decodeSingle(final int prev[], final int temp[], final int index[]) throws IOException { + private int decodeSingle(final int[] prev, final int[] temp, final int[] index) throws IOException { // At the beginning of the first line and // at the beginning of each restart interval the prediction value of 2P – 1 is used, where P is the input precision. if (restarting) { @@ -390,7 +380,7 @@ final class JPEGLosslessDecoder { return 0; } - private int decodeRGB(final int prev[], final int temp[], final int index[]) throws IOException { + 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]; @@ -435,7 +425,7 @@ final class JPEGLosslessDecoder { return decode0(prev, temp, index); } - private int decodeAny(final int prev[], final int temp[], final int index[]) throws IOException { + 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; @@ -469,17 +459,17 @@ final class JPEGLosslessDecoder { } private int decode0(int[] prev, int[] temp, int[] index) throws IOException { - int value, actab[], dctab[]; - int qtab[]; + int value; + int[] actab; + int[] dctab; + int[] qtab; for (int ctrC = 0; ctrC < numComp; ctrC++) { qtab = qTab[ctrC]; actab = acTab[ctrC]; dctab = dcTab[ctrC]; for (int i = 0; i < nBlock[ctrC]; i++) { - for (int k = 0; k < IDCT_Source.length; k++) { - IDCT_Source[k] = 0; - } + Arrays.fill(IDCT_Source, 0); value = getHuffmanValue(dctab, temp, index); @@ -545,7 +535,7 @@ final class JPEGLosslessDecoder { // and marker_index=9 // If marker_index=9 then index is always > 8, or HuffmanValue() // will not be called - private int getHuffmanValue(final int table[], final int temp[], final int index[]) throws IOException { + private int getHuffmanValue(final int[] table, final int[] temp, final int[] index) throws IOException { int code, input; final int mask = 0xFFFF; @@ -603,7 +593,7 @@ final class JPEGLosslessDecoder { return code & 0xFF; } - private int getn(final int[] pred, final int n, final int temp[], final int index[]) throws IOException { + private int getn(final int[] pred, final int n, final int[] temp, final int[] index) throws IOException { int result; final int one = 1; final int n_one = -1; @@ -688,7 +678,7 @@ final class JPEGLosslessDecoder { return result; } - private int getPreviousX(final int data[]) { + private int getPreviousX(final int[] data) { if (xLoc > 0) { return data[((yLoc * xDim) + xLoc) - 1]; } @@ -700,7 +690,7 @@ final class JPEGLosslessDecoder { } } - private int getPreviousXY(final int data[]) { + private int getPreviousXY(final int[] data) { if ((xLoc > 0) && (yLoc > 0)) { return data[(((yLoc - 1) * xDim) + xLoc) - 1]; } @@ -709,7 +699,7 @@ final class JPEGLosslessDecoder { } } - private int getPreviousY(final int data[]) { + private int getPreviousY(final int[] data) { if (yLoc > 0) { return data[((yLoc - 1) * xDim) + xLoc]; } @@ -722,7 +712,7 @@ final class JPEGLosslessDecoder { return (xLoc == (xDim - 1)) && (yLoc == (yDim - 1)); } - private void output(final int pred[]) { + private void output(final int[] pred) { if (numComp == 1) { outputSingle(pred); } @@ -734,7 +724,7 @@ final class JPEGLosslessDecoder { } } - private void outputSingle(final int pred[]) { + private void outputSingle(final int[] pred) { if ((xLoc < xDim) && (yLoc < yDim)) { outputData[0][(yLoc * xDim) + xLoc] = mask & pred[0]; xLoc++; @@ -746,7 +736,7 @@ final class JPEGLosslessDecoder { } } - private void outputRGB(final int pred[]) { + private void outputRGB(final int[] pred) { if ((xLoc < xDim) && (yLoc < yDim)) { final int index = (yLoc * xDim) + xLoc; outputData[0][index] = pred[0]; @@ -761,7 +751,7 @@ final class JPEGLosslessDecoder { } } - private void outputAny(final int pred[]) { + 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) { diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/QuantizationTable.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/QuantizationTable.java index 67c14ba0..847a8d0e 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/QuantizationTable.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/QuantizationTable.java @@ -35,35 +35,42 @@ package com.twelvemonkeys.imageio.plugins.jpeg; import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; import javax.imageio.IIOException; +import javax.imageio.plugins.jpeg.JPEGQTable; import java.io.DataInput; import java.io.IOException; final class QuantizationTable extends Segment { - private final int precision[] = new int[4]; // Quantization precision 8 or 16 - private final int[] tq = new int[4]; // 1: this table is presented + private static final int[] ZIGZAG = { + 0, 1, 5, 6, 14, 15, 27, 28, + 2, 4, 7, 13, 16, 26, 29, 42, + 3, 8, 12, 17, 25, 30, 41, 43, + 9, 11, 18, 24, 31, 40, 44, 53, + 10, 19, 23, 32, 39, 45, 52, 54, + 20, 22, 33, 38, 46, 51, 55, 60, + 21, 34, 37, 47, 50, 56, 59, 61, + 35, 36, 48, 49, 57, 58, 62, 63 + }; - final int quantTables[][] = new int[4][64]; // Tables + private final int[] precision = new int[4]; // Quantization precision 8 or 16 + private final boolean[] tq = new boolean[4]; // 1: this table is present + + private final int[][] quantTables = new int[4][64]; // Tables QuantizationTable() { super(JPEG.DQT); - - tq[0] = 0; - tq[1] = 0; - tq[2] = 0; - tq[3] = 0; } - // TODO: Get rid of table param, make it a member? - void enhanceTables(final int[] table) throws IOException { + // TODO: Consider creating a copy for the decoder here, as we need to keep the original values for the metadata + void enhanceTables() { for (int t = 0; t < 4; t++) { - if (tq[t] != 0) { - enhanceQuantizationTable(quantTables[t], table); + if (tq[t]) { + enhanceQuantizationTable(quantTables[t], ZIGZAG); } } } - private void enhanceQuantizationTable(final int qtab[], final int[] table) { + private void enhanceQuantizationTable(final int[] qtab, final int[] table) { for (int i = 0; i < 8; i++) { qtab[table[ i]] *= 90; qtab[table[(4 * 8) + i]] *= 90; @@ -122,7 +129,7 @@ final class QuantizationTable extends Segment { throw new IIOException("Unexpected JPEG Quantization Table precision: " + table.precision[t]); } - table.tq[t] = 1; + table.tq[t] = true; if (table.precision[t] == 8) { for (int i = 0; i < 64; i++) { @@ -152,4 +159,28 @@ final class QuantizationTable extends Segment { return table; } + + public boolean isPresent(int tabelId) { + return tq[tabelId]; + } + + int precision(int tableId) { + return precision[tableId]; + } + + int[] qTable(int tabelId) { + return quantTables[tabelId]; + } + + JPEGQTable toNativeTable(int tableId) { + // TODO: Should de-zigzag (ie. "natural order") while reading + // TODO: ...and make sure the table isn't "enhanced"... + int[] qTable = new int[quantTables[tableId].length]; + + for (int i = 0; i < qTable.length; i++) { + qTable[i] = quantTables[tableId][ZIGZAG[i]]; + } + + return new JPEGQTable(qTable); + } } diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java index 6f180b2f..a0344b57 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java @@ -31,8 +31,9 @@ package com.twelvemonkeys.imageio.plugins.jpeg; import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.xml.XMLSerializer; import org.hamcrest.core.IsInstanceOf; -import org.junit.Ignore; import org.junit.Test; import org.mockito.internal.matchers.GreaterThan; import org.w3c.dom.Element; @@ -182,23 +183,27 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest ycbcr = Arrays.asList( + // This reports RGB in standard metadata, while the data is really YCbCr. + // Exif files are always YCbCr AFAIK. + new TestData(getClassLoaderResource("/jpeg/exif-jpeg-thumbnail-sony-dsc-p150-inverted-colors.jpg"), new Dimension(2437, 1662)), + // Not Exif, but same issue: SOF comp ids are JFIF standard 1-3 and + // *should* be interpreted as YCbCr but isn't. + // Possible fix for this, is to insert a fake JFIF segment, as this image + // conforms to the JFIF spec (but it won't work for the Exif samples) + new TestData(getClassLoaderResource("/jpeg/no-jfif-ycbcr.jpg"), new Dimension(310, 206)) + ); + + JPEGImageReader reader = createReader(); + + try { + for (TestData broken : ycbcr) { + reader.setInput(broken.getInputStream()); + + IIOMetadata metadata = reader.getImageMetadata(0); + + IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + NodeList colorSpaceTypes = root.getElementsByTagName("ColorSpaceType"); + assertEquals(1, colorSpaceTypes.getLength()); + IIOMetadataNode csType = (IIOMetadataNode) colorSpaceTypes.item(0); + assertEquals("YCbCr", csType.getAttribute("name")); + } + } + finally { + reader.dispose(); + } + } + + @Test + public void testGetExifOrientationFromMetadata() throws IOException { + JPEGImageReader reader = createReader(); + + // TODO: Find better sample data. Should have an uppercase F ;-) + // Test all 9 mutations + missing Exif + List expectedOrientations = Arrays.asList("Normal", "Normal", "FlipH", "Rotate180", "FlipV", "FlipVRotate90", "Rotate270", "FlipHRotate90", "Rotate90"); + try { + for (int i = 0; i < 9; i++) { + try (ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource(String.format("/exif/Landscape_%d.jpg", i)))) { + reader.setInput(stream); + + IIOMetadata metadata = reader.getImageMetadata(0); + IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + + NodeList orientationNodes = root.getElementsByTagName("ImageOrientation"); + assertEquals(1, orientationNodes.getLength()); + + IIOMetadataNode orientationNode = (IIOMetadataNode) orientationNodes.item(0); + String orientationValue = orientationNode.getAttribute("value"); + assertEquals(expectedOrientations.get(i), orientationValue); + } + } + } + finally { + reader.dispose(); + } + } + + @Test + public void testBrokenReadRasterAfterGetMetadataException() { // See issue #107, from PDFBox team JPEGImageReader reader = createReader(); @@ -497,7 +595,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest(0)); @@ -1379,6 +1480,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest differ", message, expectedTree.getNodeName()), expectedAttributes.getLength(), actualAttributes.getLength()); for (int i = 0; i < expectedAttributes.getLength(); i++) { Node item = expectedAttributes.item(i); - assertEquals(String.format("%s: \"%s\" attribute for <%s> differ", message, item.getNodeName(), expectedTree.getNodeName()), item.getNodeValue(), actualAttributes.getNamedItem(item.getNodeName()).getNodeValue()); + String nodeValue = item.getNodeValue(); + + // NOTE: com.sun...JPEGMetadata javax_imageio_1.0 format bug: Uses "normal" instead of "Normal" ImageOrientation + if ("ImageOrientation".equals(expectedTree.getNodeName()) && "value".equals(item.getNodeName())) { + nodeValue = StringUtil.capitalize(nodeValue); + } + + assertEquals(String.format("%s: \"%s\" attribute for <%s> differ", message, item.getNodeName(), expectedTree.getNodeName()), nodeValue, actualAttributes.getNamedItem(item.getNodeName()).getNodeValue()); } // Test for equal user objects. @@ -1460,6 +1570,11 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest expectedChildren = sortNodes(expectedTree.getChildNodes()); List actualChildren = sortNodes(actualTree.getChildNodes()); diff --git a/imageio/imageio-jpeg/src/test/resources/exif/LICENSE b/imageio/imageio-jpeg/src/test/resources/exif/LICENSE new file mode 100644 index 00000000..978ee2ae --- /dev/null +++ b/imageio/imageio-jpeg/src/test/resources/exif/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010 Dave Perrett, http://recursive-design.com/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Landscape_0.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_0.jpg new file mode 100644 index 00000000..8518c82b Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_0.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Landscape_1.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_1.jpg new file mode 100644 index 00000000..fda18823 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_1.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Landscape_2.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_2.jpg new file mode 100644 index 00000000..d2605f81 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_2.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Landscape_3.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_3.jpg new file mode 100644 index 00000000..f5080523 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_3.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Landscape_4.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_4.jpg new file mode 100644 index 00000000..d73dee8f Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_4.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Landscape_5.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_5.jpg new file mode 100644 index 00000000..975d8588 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_5.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Landscape_6.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_6.jpg new file mode 100644 index 00000000..b579b7f9 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_6.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Landscape_7.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_7.jpg new file mode 100644 index 00000000..b1e919cf Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_7.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Landscape_8.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_8.jpg new file mode 100644 index 00000000..c381db10 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Landscape_8.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Portrait_0.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_0.jpg new file mode 100644 index 00000000..aa9632e5 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_0.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Portrait_1.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_1.jpg new file mode 100644 index 00000000..dcb57c53 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_1.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Portrait_2.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_2.jpg new file mode 100644 index 00000000..8c3adf7a Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_2.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Portrait_3.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_3.jpg new file mode 100644 index 00000000..5a5544f2 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_3.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Portrait_4.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_4.jpg new file mode 100644 index 00000000..9eb2a6a1 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_4.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Portrait_5.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_5.jpg new file mode 100644 index 00000000..905169aa Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_5.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Portrait_6.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_6.jpg new file mode 100644 index 00000000..8fc576e0 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_6.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Portrait_7.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_7.jpg new file mode 100644 index 00000000..cfa04d66 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_7.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/Portrait_8.jpg b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_8.jpg new file mode 100644 index 00000000..b2a50d6e Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/exif/Portrait_8.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/exif/README.markdown b/imageio/imageio-jpeg/src/test/resources/exif/README.markdown new file mode 100644 index 00000000..38c655f3 --- /dev/null +++ b/imageio/imageio-jpeg/src/test/resources/exif/README.markdown @@ -0,0 +1,82 @@ +EXIF Orientation-flag example images +==================================== + +Example images using each of the EXIF orientation flags (0-to-8), in both landscape and portrait orientations. + +[See here](http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/) for more information. + + +Generating your own images +-------------------------- + +If you would like to generate test images based on your own photos, you can use the `generate.rb` script included in the `generator` folder. + +The instructions below assume you are running on macOS - if not, you will need to install the Ghostscript fonts (`brew install gs`) some other way. + +To install the dependencies: + +``` +> brew install gs exiftool imagemagick@6 +> cd generator +> gem install bundler +> bundle install +``` + +To generate test images: + +``` +> cd generator +> ./generate.rb path/to/image.jpg +``` + +This will create images `image_0.jpg` through to `image_8.jpg`. + + +Re-generating sample images +--------------------------- + +Simply run `make` to regenerate the included sample images. This will download random portrait and landscape orientation images from [unsplash.com](https://unsplash.com/) and generate sample images for each of them. + +Generating these images depends on having the generator dependencies installed - see the *Generating your own images* section for instructions on installing dependencies. + + +Credits +------- + +* The sample landscape image is by [Pierre Bouillot](https://unsplash.com/photos/v15iOM6pWgI). +* The sample portrait image is by [John Salvino](https://unsplash.com/photos/1PPpwrTNkJI). + + +Change history +-------------- + +* **Version 2.0.0 (2017-08-05)** : Add a script to generate example images from the command line. +* **Version 1.0.2 (2017-03-06)** : Remove Apple Copyrighted ICC profile from orientations 2-8 (thanks @mans0954!). +* **Version 1.0.1 (2013-03-10)** : Add MIT license and some contact details. +* **Version 1.0.0 (2012-07-28)** : 1.0 release. + + +Contributing +------------ + +Once you've made your commits: + +1. [Fork](http://help.github.com/fork-a-repo/) exif-orientation-examples +2. Create a topic branch - `git checkout -b my_branch` +3. Push to your branch - `git push origin my_branch` +4. Create a [Pull Request](http://help.github.com/pull-requests/) from your branch +5. That's it! + + +Author +------ + +Dave Perrett :: hello@daveperrett.com :: [@daveperrett](http://twitter.com/daveperrett) + + +Copyright +--------- + +These images are licensed under the [MIT License](http://opensource.org/licenses/MIT). + +Copyright (c) 2010 Dave Perrett. See [License](https://github.com/recurser/exif-orientation-examples/blob/master/LICENSE) for details. diff --git a/imageio/imageio-jpeg/src/test/resources/exif/VERSION b/imageio/imageio-jpeg/src/test/resources/exif/VERSION new file mode 100644 index 00000000..38f77a65 --- /dev/null +++ b/imageio/imageio-jpeg/src/test/resources/exif/VERSION @@ -0,0 +1 @@ +2.0.1