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