From 0ab16f7192b7a3492aa058f1b4aedcb5ebab3742 Mon Sep 17 00:00:00 2001 From: snumlautoken Date: Tue, 7 Mar 2023 15:43:09 +0100 Subject: [PATCH] Add Exif orientation rewriter Co-authored-by: YavizGuldalf Co-authored-by: Hasti Mohebali Zadeh Co-authored-by: chenyi Co-authored-by: regusta --- .../jpeg/exif/ExifOrientationRewriter.java | 173 ++++++++++++++++ .../exif/ExifOrientationRewriterTest.java | 185 ++++++++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/exif/ExifOrientationRewriter.java create mode 100644 src/test/java/org/apache/commons/imaging/formats/jpeg/exif/ExifOrientationRewriterTest.java diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/exif/ExifOrientationRewriter.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/exif/ExifOrientationRewriter.java new file mode 100644 index 0000000000..954f40f785 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/exif/ExifOrientationRewriter.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.imaging.formats.jpeg.exif; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.ArrayList; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.Imaging; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.common.bytesource.ByteSourceArray; +import org.apache.commons.imaging.common.bytesource.ByteSourceFile; +import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata; +import org.apache.commons.imaging.formats.tiff.TiffContents; +import org.apache.commons.imaging.formats.tiff.TiffHeader; +import org.apache.commons.imaging.formats.tiff.TiffImageMetadata; +import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; +import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory; +import org.apache.commons.imaging.formats.tiff.write.TiffOutputField; +import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet; + +public class ExifOrientationRewriter { + + public enum Orientation { + HORIZONTAL((short)1), + MIRROR_HORIZONTAL((short)2), + ROTATE_180((short)3), + MIRROR_VERTICAL((short)4), + MIRROR_HORIZONTAL_AND_ROTATE_270((short)5), + ROTATE_90((short)6), + MIRROR_HORIZONTAL_AND_ROTATE_90((short)7), + ROTATE_270((short)8); + + private short val; + + Orientation(short orVal) { + this.val = orVal; + } + + public short getVal() { + return val; + } + } + + private ByteSource fileSrc; + + public ExifOrientationRewriter(File imageFile) { + fileSrc = new ByteSourceFile(imageFile); + } + public ExifOrientationRewriter(byte[] byteArray) { + fileSrc = new ByteSourceArray(byteArray); + } + public ExifOrientationRewriter(ByteSource byteSource) { + fileSrc = byteSource; + } + + /*** + * Get the orientation (enum) of the current image + * Returns horizontal by default + * @return Orientation enum + * @throws IOException + * @throws ImageReadException + * @throws ImageWriteException + */ + public Orientation getExifOrientation() throws IOException, ImageReadException, ImageWriteException { + + final JpegImageMetadata metadata = (JpegImageMetadata) Imaging.getMetadata(this.fileSrc.getAll()); + + if (metadata == null) { + return Orientation.HORIZONTAL; + } + + final TiffImageMetadata exifMetadata = metadata.getExif(); + + if (exifMetadata == null) { + return Orientation.HORIZONTAL; + } + + final TiffOutputSet outputSet = exifMetadata.getOutputSet(); + + TiffOutputDirectory tod = outputSet.getRootDirectory(); + if (tod == null) { + return Orientation.HORIZONTAL; + } + + TiffOutputField tof = tod.findField(TiffTagConstants.TIFF_TAG_ORIENTATION); + if (tof == null) { + return Orientation.HORIZONTAL; + } + + short imageOrientationVal = (short) exifMetadata.getFieldValue(TiffTagConstants.TIFF_TAG_ORIENTATION); + + for (Orientation orientation : Orientation.values()) { + if(orientation.getVal() == imageOrientationVal) { + return orientation; + } + } + + return Orientation.HORIZONTAL; + } + + /** + * A method that sets a new value to the orientation field in the EXIF metadata of a JPEG file. + * @param orientation the value as a enum of the direction to set as the new EXIF orientation + * + */ + public void setExifOrientation(Orientation orientation) throws ImageWriteException, IOException, ImageReadException { + + JpegImageMetadata metadata = (JpegImageMetadata) Imaging.getMetadata(this.fileSrc.getAll()); + + if (metadata == null) { + metadata = new JpegImageMetadata(null, new TiffImageMetadata(new TiffContents(new TiffHeader(ByteOrder.BIG_ENDIAN, 0, 0), new ArrayList<>(), new ArrayList<>()))); + } + + TiffImageMetadata exifMetadata = metadata.getExif(); + + if (exifMetadata == null) { + exifMetadata = new TiffImageMetadata(new TiffContents(new TiffHeader(ByteOrder.BIG_ENDIAN, 0, 0), new ArrayList<>(), new ArrayList<>())); + } + + final TiffOutputSet outputSet = exifMetadata.getOutputSet(); + + TiffOutputDirectory tod = outputSet.getOrCreateRootDirectory(); + tod.removeField(TiffTagConstants.TIFF_TAG_ORIENTATION); + tod.add(TiffTagConstants.TIFF_TAG_ORIENTATION, orientation.getVal()); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new ExifRewriter().updateExifMetadataLossy(this.fileSrc, baos, outputSet); + + this.fileSrc = new ByteSourceArray(baos.toByteArray()); + } + + /** + * @return the ByteSource of the current file + */ + public ByteSource getOutput() { + return fileSrc; + } + + /** + * Writes Bytesource to file with given path + * @param path String of the path in which the file is written + * @throws IOException + */ + public void getOutput(String path) + throws IOException { + final File tempFile = new File(path); + try (FileOutputStream outputStream = new FileOutputStream(tempFile)) { + outputStream.write(fileSrc.getAll()); + } + } + +} diff --git a/src/test/java/org/apache/commons/imaging/formats/jpeg/exif/ExifOrientationRewriterTest.java b/src/test/java/org/apache/commons/imaging/formats/jpeg/exif/ExifOrientationRewriterTest.java new file mode 100644 index 0000000000..febf62420b --- /dev/null +++ b/src/test/java/org/apache/commons/imaging/formats/jpeg/exif/ExifOrientationRewriterTest.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.commons.imaging.formats.jpeg.exif; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; + +import org.apache.commons.imaging.Imaging; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.common.bytesource.ByteSourceFile; +import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata; +import org.apache.commons.imaging.formats.jpeg.exif.ExifOrientationRewriter.Orientation; +import org.apache.commons.imaging.formats.tiff.TiffImageMetadata; +import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; +import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; + +public class ExifOrientationRewriterTest extends ExifBaseTest { + + @Test + public void originalOrientationMatchesRewriterGet() + throws ImageReadException, IOException, ImageWriteException { + + final List images = getImagesWithExifData(); + for (final File imageFile : images) { + final JpegImageMetadata originalMetadata = (JpegImageMetadata) Imaging.getMetadata(imageFile); + assertNotNull(originalMetadata); + + final TiffImageMetadata originalExifMetadata = originalMetadata.getExif(); + assertNotNull(originalExifMetadata); + + short originalOrt = (short) originalExifMetadata.getFieldValue(TiffTagConstants.TIFF_TAG_ORIENTATION); + + ExifOrientationRewriter rewriter = new ExifOrientationRewriter(imageFile); + short ort = rewriter.getExifOrientation().getVal(); + + assertEquals(ort, originalOrt); + } + + } + + @Test + public void rewrittenOrientationMatchesRewriterGet() + throws ImageReadException, IOException, ImageWriteException { + + final List images = getImagesWithExifData(); + for (final File imageFile : images) { + final ByteSource byteSource = new ByteSourceFile(imageFile); + + final JpegImageMetadata originalMetadata = (JpegImageMetadata) Imaging.getMetadata(imageFile); + assertNotNull(originalMetadata); + + final TiffImageMetadata originalExifMetadata = originalMetadata.getExif(); + assertNotNull(originalExifMetadata); + + final TiffOutputSet outputSet = originalExifMetadata.getOutputSet(); + + outputSet.getOrCreateRootDirectory().removeField(TiffTagConstants.TIFF_TAG_ORIENTATION); + outputSet.getOrCreateRootDirectory().add(TiffTagConstants.TIFF_TAG_ORIENTATION, (short) TiffTagConstants.ORIENTATION_VALUE_ROTATE_270_CW); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + new ExifRewriter().updateExifMetadataLossy(byteSource.getAll(), baos, + outputSet); + + final byte[] bytes = baos.toByteArray(); + final File tempFile = Files.createTempFile("inserted" + "_", ".jpg").toFile(); + + FileUtils.writeByteArrayToFile(tempFile, bytes); + + final ExifOrientationRewriter rewriter = new ExifOrientationRewriter(tempFile); + short ort = rewriter.getExifOrientation().getVal(); + + assertEquals(ort, (short) TiffTagConstants.ORIENTATION_VALUE_ROTATE_270_CW); + } + + } + + @Test + public void getWithNullExifReturnsHorizontal() + throws ImageReadException, IOException, ImageWriteException { + + final List images = getImagesWithExifData(); + for (final File imageFile : images) { + final ByteSource byteSource = new ByteSourceFile(imageFile); + final JpegImageMetadata originalMetadata = (JpegImageMetadata) Imaging.getMetadata(imageFile); + assertNotNull(originalMetadata); + + final TiffImageMetadata originalExifMetadata = originalMetadata.getExif(); + assertNotNull(originalExifMetadata); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new ExifRewriter().removeExifMetadata(byteSource, baos); + final byte[] bytes = baos.toByteArray(); + final File tempFile = Files.createTempFile("removed", ".jpg").toFile(); + FileUtils.writeByteArrayToFile(tempFile, bytes); + + assertFalse(hasExifData(tempFile)); + + final ExifOrientationRewriter rewriter = new ExifOrientationRewriter(tempFile); + short ort = rewriter.getExifOrientation().getVal(); + + assertEquals(ort, Orientation.HORIZONTAL.getVal()); + } + + } + + @Test + void testSetExifOrientation() + throws IOException, ImageReadException, ImageWriteException { + + final List images = getImagesWithExifData(); + for (final File imageFile : images) { + ExifOrientationRewriter eor = new ExifOrientationRewriter(imageFile); + ExifOrientationRewriter.Orientation eo = ExifOrientationRewriter.Orientation.ROTATE_180; + eor.setExifOrientation(eo); + + ByteSource bs = eor.getOutput(); + final JpegImageMetadata newMetadata = (JpegImageMetadata) Imaging.getMetadata(bs.getAll()); + final TiffImageMetadata newExifMetadata = newMetadata.getExif(); + + assertNotNull(newExifMetadata, "The new EXIF metadata is null"); + assertEquals(newExifMetadata.getFieldValue(TiffTagConstants.TIFF_TAG_ORIENTATION), Short.valueOf(eo.getVal()), "The orientation field in the EXIF metadata is not set correctly"); + } + } + + @Test + public void setWithNullExifDoesNotThrow() + throws ImageReadException, IOException, ImageWriteException { + + final List images = getImagesWithExifData(); + for (final File imageFile : images) { + final ByteSource byteSource = new ByteSourceFile(imageFile); + final JpegImageMetadata originalMetadata = (JpegImageMetadata) Imaging.getMetadata(imageFile); + assertNotNull(originalMetadata); + + final TiffImageMetadata originalExifMetadata = originalMetadata.getExif(); + assertNotNull(originalExifMetadata); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new ExifRewriter().removeExifMetadata(byteSource, baos); + final byte[] bytes = baos.toByteArray(); + final File tempFile = Files.createTempFile("removed", ".jpg").toFile(); + FileUtils.writeByteArrayToFile(tempFile, bytes); + + assertFalse(hasExifData(tempFile)); + + final ExifOrientationRewriter rewriter = new ExifOrientationRewriter(tempFile); + assertDoesNotThrow(() -> rewriter.setExifOrientation(Orientation.ROTATE_180)); + + ByteSource bs = rewriter.getOutput(); + final JpegImageMetadata newMetadata = (JpegImageMetadata) Imaging.getMetadata(bs.getAll()); + final TiffImageMetadata newExifMetadata = newMetadata.getExif(); + + assertNotNull(newExifMetadata, "The new EXIF metadata is null"); + assertEquals(newExifMetadata.getFieldValue(TiffTagConstants.TIFF_TAG_ORIENTATION), Orientation.ROTATE_180.getVal(), "The orientation field in the EXIF metadata is not set correctly"); + } + } +}