JPEG Exif rotation in metadata + support

This commit is contained in:
Harald Kuhr
2020-07-10 22:05:46 +02:00
parent 7e55d7765d
commit 5cc201b46d
31 changed files with 1091 additions and 285 deletions

View File

@@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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<ImageReader> 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 <a href="https://docs.oracle.com/javase/7/docs/api/javax/imageio/metadata/doc-files/standard_metadata.html">Standard (Plug-in Neutral) Metadata Format Specification</a>
*/
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);
}
}
}

View File

@@ -0,0 +1,63 @@
package com.twelvemonkeys.contrib.exif;
import com.twelvemonkeys.contrib.tiff.TIFFUtilities;
/**
* Orientation.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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;
}
}

View File

@@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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));
}
}