View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.jpeg.exif;
18  
19  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertNotNull;
22  import static org.junit.jupiter.api.Assertions.assertTrue;
23  
24  import java.io.BufferedOutputStream;
25  import java.io.File;
26  import java.io.IOException;
27  import java.io.OutputStream;
28  import java.nio.file.Files;
29  import java.security.SecureRandom;
30  import java.util.ArrayList;
31  import java.util.List;
32  import java.util.stream.Stream;
33  
34  import org.apache.commons.imaging.Imaging;
35  import org.apache.commons.imaging.ImagingException;
36  import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata;
37  import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
38  import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
39  import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
40  import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory;
41  import org.apache.commons.imaging.formats.tiff.write.TiffOutputField;
42  import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
43  import org.apache.commons.io.FileUtils;
44  import org.junit.jupiter.api.AfterEach;
45  import org.junit.jupiter.params.ParameterizedTest;
46  import org.junit.jupiter.params.provider.MethodSource;
47  import org.opentest4j.TestSkippedException;
48  
49  /**
50   * Read and write EXIF data, and verify that it's identical, and no data corruption occurred.
51   */
52  public class ExifRewriterRoundtripTest extends AbstractExifTest {
53  
54      /**
55       * Test data.
56       * @return test data.
57       * @throws Exception if it fails to read the images with EXIF.
58       */
59      public static Stream<File> data() throws Exception {
60          return getImagesWithExifData().stream();
61      }
62  
63      private final SecureRandom random = new SecureRandom();
64  
65      private File duplicateFile;
66  
67      private void assertTiffEquals(final TiffOutputSet sourceTiffOutputSet, final TiffOutputSet duplicateTiffOutputSet) {
68          final List<TiffOutputDirectory> sourceDirectories = sourceTiffOutputSet.getDirectories();
69          final List<TiffOutputDirectory> duplicatedDirectories = duplicateTiffOutputSet.getDirectories();
70  
71          assertEquals(sourceDirectories.size(), duplicatedDirectories.size(), "The TiffOutputSets have different numbers of directories.");
72  
73          for (int i = 0; i < sourceDirectories.size(); i++) {
74              final TiffOutputDirectory sourceDirectory = sourceDirectories.get(i);
75              final TiffOutputDirectory duplicateDirectory = duplicatedDirectories.get(i);
76  
77              assertEquals(sourceDirectory.getType(), duplicateDirectory.getType(), "Directory type mismatch.");
78  
79              final List<TiffOutputField> sourceFields = sourceDirectory.getFields();
80              final List<TiffOutputField> duplicateFields = duplicateDirectory.getFields();
81  
82              final boolean fieldCountMatches = sourceFields.size() == duplicateFields.size();
83  
84              if (!fieldCountMatches) {
85                  /*
86                   * Note that offset fields are not written again if a re-order makes
87                   * them unnecessary. The TiffWriter does not write in the same order,
88                   * but tries to optimize it.
89                   */
90                  final List<Integer> sourceTags = new ArrayList<>();
91                  final List<Integer> duplicatedTags = new ArrayList<>();
92  
93                  for (final TiffOutputField field : sourceFields) {
94                      sourceTags.add(field.tag);
95                  }
96  
97                  for (final TiffOutputField field : duplicateFields) {
98                      duplicatedTags.add(field.tag);
99                  }
100 
101                 final List<Integer> missingTags = new ArrayList<>(sourceTags);
102                 missingTags.removeAll(duplicatedTags);
103                 missingTags.remove((Integer) ExifTagConstants.EXIF_TAG_EXIF_OFFSET.tag);
104                 missingTags.remove((Integer) ExifTagConstants.EXIF_TAG_GPSINFO.tag);
105                 missingTags.remove((Integer) ExifTagConstants.EXIF_TAG_INTEROP_OFFSET.tag);
106                 missingTags.remove((Integer) TiffTagConstants.TIFF_TAG_JPEG_INTERCHANGE_FORMAT.tag);
107 
108                 assertTrue(missingTags.isEmpty(), "Missing tags: " + missingTags);
109             }
110 
111             for (final TiffOutputField sourceField : sourceFields) {
112                 final boolean isOffsetField =
113                         sourceField.tag == ExifTagConstants.EXIF_TAG_EXIF_OFFSET.tag ||
114                                 sourceField.tag == ExifTagConstants.EXIF_TAG_GPSINFO.tag ||
115                                 sourceField.tag == ExifTagConstants.EXIF_TAG_INTEROP_OFFSET.tag ||
116                                 sourceField.tag == TiffTagConstants.TIFF_TAG_JPEG_INTERCHANGE_FORMAT.tag;
117 
118                 /*
119                  * Ignore offset fields. They may not be needed after rewrite
120                  * and their value changes anyway.
121                  */
122                 if (isOffsetField) {
123                     continue;
124                 }
125 
126                 final TiffOutputField duplicateField = duplicateDirectory.findField(sourceField.tag);
127 
128                 assertNotNull(duplicateField, "Field is missing: " + sourceField.tagInfo);
129 
130                 assertEquals(sourceField.tag, duplicateField.tag, "TiffOutputField tag mismatch.");
131                 assertEquals(sourceField.abstractFieldType, duplicateField.abstractFieldType, "TiffOutputField fieldType mismatch.");
132                 assertEquals(sourceField.count, duplicateField.count, "TiffOutputField count mismatch.");
133 
134                 assertArrayEquals(sourceField.getData(), duplicateField.getData(), "Bytes are different for field: " + sourceField);
135             }
136         }
137     }
138 
139     private void copyToDuplicateFile(final File sourceFile, final TiffOutputSet duplicateTiffOutputSet) throws IOException {
140         final ExifRewriter exifRewriter = new ExifRewriter();
141 
142         duplicateFile = createTempFile();
143 
144         try (OutputStream duplicateOutputStream = new BufferedOutputStream(Files.newOutputStream(duplicateFile.toPath()))) {
145             exifRewriter.updateExifMetadataLossless(sourceFile, duplicateOutputStream, duplicateTiffOutputSet);
146         }
147     }
148 
149     private File createTempFile() {
150         final String tempDir = FileUtils.getTempDirectoryPath();
151         final String tempFileName = this.getClass().getName() + "-" + random.nextLong() + ".tmp";
152 
153         return new File(tempDir, tempFileName);
154     }
155 
156     private TiffOutputSet duplicateTiffOutputSet(final TiffOutputSet sourceTiffOutputSet) throws ImagingException {
157         final TiffOutputSet duplicateTiffOutputSet = new TiffOutputSet(
158                 sourceTiffOutputSet.byteOrder
159         );
160 
161         for (final TiffOutputDirectory tiffOutputDirectory : sourceTiffOutputSet) {
162             duplicateTiffOutputSet.addDirectory(tiffOutputDirectory);
163         }
164 
165         return duplicateTiffOutputSet;
166     }
167 
168     private JpegImageMetadata getJpegImageMetadata(final File sourceFile) throws IOException {
169         final JpegImageMetadata jpegImageMetadata = (JpegImageMetadata) Imaging.getMetadata(sourceFile);
170 
171         if (jpegImageMetadata == null) {
172             throw new TestSkippedException();
173         }
174 
175         return jpegImageMetadata;
176     }
177 
178     private TiffImageMetadata getTiffImageMetadata(final JpegImageMetadata sourceJpegImageMetadata) {
179         final TiffImageMetadata tiffImageMetadata = sourceJpegImageMetadata.getExif();
180 
181         if (tiffImageMetadata == null) {
182             throw new TestSkippedException();
183         }
184 
185         return tiffImageMetadata;
186     }
187 
188     private TiffOutputSet getTiffOutputSet(final TiffImageMetadata sourceTiffImageMetadata) throws ImagingException {
189         final TiffOutputSet tiffOutputSet = sourceTiffImageMetadata.getOutputSet();
190 
191         if (tiffOutputSet == null) {
192             throw new TestSkippedException();
193         }
194 
195         return tiffOutputSet;
196     }
197 
198     @AfterEach
199     void tearDown() {
200         if (duplicateFile != null && duplicateFile.exists()) {
201             duplicateFile.delete();
202             duplicateFile.deleteOnExit();
203         }
204     }
205 
206     /**
207      * Does the round-trip test, by loading EXIF data, and comparing it with the
208      * data from a duplicated file.
209      *
210      * @param sourceFile the input file.
211      * @throws Exception if it fails to read the test image or create the duplicated file.
212      */
213     @ParameterizedTest
214     @MethodSource("data")
215     public void updateExifMetadataLossless_copyWithoutChanges_TiffOutputSetsAreIdentical(final File sourceFile) throws Exception {
216         /*
217          * Load EXIF data from source file, skipping over any test images without any EXIF data
218          */
219         final JpegImageMetadata sourceJpegImageMetadata = getJpegImageMetadata(sourceFile);
220         final TiffImageMetadata sourceTiffImageMetadata = getTiffImageMetadata(sourceJpegImageMetadata);
221         final TiffOutputSet sourceTiffOutputSet = getTiffOutputSet(sourceTiffImageMetadata);
222 
223         /*
224          * Copy the TiffOutputSet to a duplicate TiffOutputSet
225          */
226         TiffOutputSet duplicateTiffOutputSet = duplicateTiffOutputSet(sourceTiffOutputSet);
227 
228         /*
229          * Compare the two TiffOutputSets
230          */
231         assertTiffEquals(sourceTiffOutputSet, duplicateTiffOutputSet);
232 
233         /*
234          * Copy the file to a duplicate file, using updateExifMetadataLossless and the duplicate TiffOutputSet
235          */
236         copyToDuplicateFile(sourceFile, duplicateTiffOutputSet);
237 
238         /*
239          * Load EXIF data from duplicate file
240          */
241         final JpegImageMetadata duplicateJpegImageMetadata = getJpegImageMetadata(duplicateFile);
242         final TiffImageMetadata duplicateTiffImageMetadata = getTiffImageMetadata(duplicateJpegImageMetadata);
243         duplicateTiffOutputSet = duplicateTiffImageMetadata.getOutputSet();
244 
245         /*
246          * Compare the source TiffOutputSet to the one loaded from the duplicate file. This fails!
247          */
248         assertTiffEquals(sourceTiffOutputSet, duplicateTiffOutputSet);
249     }
250 }