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  
18  package org.apache.commons.imaging.formats.jpeg.exif;
19  
20  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertFalse;
23  import static org.junit.jupiter.api.Assertions.assertNotNull;
24  import static org.junit.jupiter.api.Assertions.assertTrue;
25  
26  import java.io.ByteArrayOutputStream;
27  import java.io.File;
28  import java.io.IOException;
29  import java.io.OutputStream;
30  import java.util.ArrayList;
31  import java.util.Collections;
32  import java.util.HashMap;
33  import java.util.HashSet;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.Set;
37  
38  import org.apache.commons.imaging.ImageReadException;
39  import org.apache.commons.imaging.ImageWriteException;
40  import org.apache.commons.imaging.Imaging;
41  import org.apache.commons.imaging.common.ImageMetadata.ImageMetadataItem;
42  import org.apache.commons.imaging.common.bytesource.ByteSource;
43  import org.apache.commons.imaging.common.bytesource.ByteSourceArray;
44  import org.apache.commons.imaging.common.bytesource.ByteSourceFile;
45  import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata;
46  import org.apache.commons.imaging.formats.jpeg.JpegUtils;
47  import org.apache.commons.imaging.formats.tiff.TiffField;
48  import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
49  import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType;
50  import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
51  import org.apache.commons.imaging.internal.Debug;
52  import org.apache.commons.io.FileUtils;
53  import org.junit.jupiter.api.Assertions;
54  import org.junit.jupiter.api.Test;
55  
56  public class ExifRewriteTest extends ExifBaseTest {
57      // public ExifRewriteTest(String name)
58      // {
59      // super(name);
60      // }
61  
62      @Test
63      public void testRemove() throws Exception {
64          final List<File> images = getImagesWithExifData();
65          for (final File imageFile : images) {
66  
67              Debug.debug("imageFile", imageFile);
68  
69              final boolean ignoreImageData = isPhilHarveyTestImage(imageFile);
70              if (ignoreImageData) {
71                  continue;
72              }
73  
74              final ByteSource byteSource = new ByteSourceFile(imageFile);
75              Debug.debug("Source Segments:");
76              new JpegUtils().dumpJFIF(byteSource);
77  
78              {
79                  final JpegImageMetadata metadata = (JpegImageMetadata) Imaging.getMetadata(imageFile);
80                  Assertions.assertNotNull(metadata);
81              }
82  
83              {
84                  final ByteArrayOutputStream baos = new ByteArrayOutputStream();
85                  new ExifRewriter().removeExifMetadata(byteSource, baos);
86                  final byte[] bytes = baos.toByteArray();
87                  final File tempFile = File.createTempFile("test", ".jpg");
88                  Debug.debug("tempFile", tempFile);
89                  FileUtils.writeByteArrayToFile(tempFile, bytes);
90  
91                  Debug.debug("Output Segments:");
92                  new JpegUtils().dumpJFIF(new ByteSourceArray(bytes));
93  
94                  assertFalse(hasExifData(tempFile));
95              }
96          }
97      }
98  
99      @Test
100     public void testInsert() throws Exception {
101         final List<File> images = getImagesWithExifData();
102         for (final File imageFile : images) {
103 
104             Debug.debug("imageFile", imageFile);
105 
106             final boolean ignoreImageData = isPhilHarveyTestImage(imageFile);
107             if (ignoreImageData) {
108                 continue;
109             }
110 
111             final ByteSource byteSource = new ByteSourceFile(imageFile);
112             Debug.debug("Source Segments:");
113             new JpegUtils().dumpJFIF(byteSource);
114 
115             final JpegImageMetadata originalMetadata = (JpegImageMetadata) Imaging.getMetadata(imageFile);
116             assertNotNull(originalMetadata);
117 
118             final TiffImageMetadata oldExifMetadata = originalMetadata.getExif();
119             assertNotNull(oldExifMetadata);
120 
121             ByteSource stripped;
122             {
123                 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
124                 new ExifRewriter().removeExifMetadata(byteSource, baos);
125                 final byte[] bytes = baos.toByteArray();
126                 final File tempFile = File.createTempFile("removed", ".jpg");
127                 Debug.debug("tempFile", tempFile);
128                 FileUtils.writeByteArrayToFile(tempFile, bytes);
129 
130                 Debug.debug("Output Segments:");
131                 stripped = new ByteSourceArray(bytes);
132                 new JpegUtils().dumpJFIF(stripped);
133 
134                 assertFalse(hasExifData(tempFile));
135             }
136 
137             {
138                 final TiffOutputSet outputSet = oldExifMetadata.getOutputSet();
139                 // outputSet.dump();
140 
141                 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
142 
143                 new ExifRewriter().updateExifMetadataLossy(stripped, baos,
144                         outputSet);
145 
146                 final byte[] bytes = baos.toByteArray();
147                 final File tempFile = File.createTempFile("inserted" + "_", ".jpg");
148                 Debug.debug("tempFile", tempFile);
149                 FileUtils.writeByteArrayToFile(tempFile, bytes);
150 
151                 Debug.debug("Output Segments:");
152                 new JpegUtils().dumpJFIF(new ByteSourceArray(bytes));
153 
154                 // assertTrue(!hasExifData(tempFile));
155 
156                 final JpegImageMetadata newMetadata = (JpegImageMetadata) Imaging.getMetadata(tempFile);
157                 assertNotNull(newMetadata);
158                 final TiffImageMetadata newExifMetadata = newMetadata.getExif();
159                 assertNotNull(newExifMetadata);
160                 // newMetadata.dump();
161 
162                 compare(imageFile, oldExifMetadata, newExifMetadata);
163             }
164 
165         }
166     }
167 
168     private interface Rewriter {
169         void rewrite(ByteSource byteSource, OutputStream os,
170                 TiffOutputSet outputSet) throws ImageReadException,
171                 IOException, ImageWriteException;
172     }
173 
174     private void rewrite(final Rewriter rewriter, final String name) throws IOException,
175             ImageReadException {
176         final List<File> images = getImagesWithExifData();
177         for (final File imageFile : images) {
178 
179             try {
180 
181                 Debug.debug("imageFile", imageFile);
182 
183                 final boolean ignoreImageData = isPhilHarveyTestImage(imageFile);
184                 if (ignoreImageData) {
185                     continue;
186                 }
187 
188                 final ByteSource byteSource = new ByteSourceFile(imageFile);
189                 Debug.debug("Source Segments:");
190                 new JpegUtils().dumpJFIF(byteSource);
191 
192                 final JpegImageMetadata oldMetadata = (JpegImageMetadata) Imaging.getMetadata(imageFile);
193                 if (null == oldMetadata) {
194                     continue;
195                 }
196                 assertNotNull(oldMetadata);
197 
198                 final TiffImageMetadata oldExifMetadata = oldMetadata.getExif();
199                 if (null == oldExifMetadata) {
200                     continue;
201                 }
202                 assertNotNull(oldExifMetadata);
203                 oldMetadata.dump();
204 
205                 // TiffImageMetadata tiffImageMetadata = metadata.getExif();
206                 // Photoshop photoshop = metadata.getPhotoshop();
207 
208                 final TiffOutputSet outputSet = oldExifMetadata.getOutputSet();
209                 // outputSet.dump();
210 
211                 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
212                 rewriter.rewrite(byteSource, baos, outputSet);
213                 final byte[] bytes = baos.toByteArray();
214                 final File tempFile = File.createTempFile(name + "_", ".jpg");
215                 Debug.debug("tempFile", tempFile);
216                 FileUtils.writeByteArrayToFile(tempFile, bytes);
217 
218                 Debug.debug("Output Segments:");
219                 new JpegUtils().dumpJFIF(new ByteSourceArray(bytes));
220 
221                 // assertTrue(!hasExifData(tempFile));
222 
223                 final JpegImageMetadata newMetadata = (JpegImageMetadata) Imaging.getMetadata(tempFile);
224                 assertNotNull(newMetadata);
225                 final TiffImageMetadata newExifMetadata = newMetadata.getExif();
226                 assertNotNull(newExifMetadata);
227                 // newMetadata.dump();
228 
229                 compare(imageFile, oldExifMetadata, newExifMetadata);
230             } catch (final IOException | ImageReadException e) {
231                 Debug.debug("imageFile", imageFile.getAbsoluteFile());
232                 Debug.debug(e);
233                 throw e;
234             } catch (final ImageWriteException e) {
235                 Debug.debug("imageFile", imageFile.getAbsoluteFile());
236                 Debug.debug(e);
237             }
238 
239         }
240     }
241 
242     @Test
243     public void testRewriteLossy() throws Exception {
244         final Rewriter rewriter = (byteSource, os, outputSet) -> new ExifRewriter().updateExifMetadataLossy(byteSource, os,
245                 outputSet);
246 
247         rewrite(rewriter, "lossy");
248     }
249 
250     @Test
251     public void testRewriteLossless() throws Exception {
252         final Rewriter rewriter = (byteSource, os, outputSet) -> new ExifRewriter().updateExifMetadataLossless(byteSource, os,
253                 outputSet);
254 
255         rewrite(rewriter, "lossless");
256     }
257 
258     private Map<Integer,TiffImageMetadata.Directory> makeDirectoryMap(final List<? extends ImageMetadataItem> directories) {
259         final Map<Integer,TiffImageMetadata.Directory> directoryMap = new HashMap<>();
260         for (final ImageMetadataItem element : directories) {
261             final TiffImageMetadata.Directory directory = (TiffImageMetadata.Directory) element;
262             directoryMap.put(directory.type, directory);
263         }
264         return directoryMap;
265     }
266 
267     private Map<Integer,TiffField> makeFieldMap(final List<? extends ImageMetadataItem> items) {
268         final Map<Integer,TiffField> fieldMap = new HashMap<>();
269         for (final ImageMetadataItem item2 : items) {
270             final TiffImageMetadata.TiffMetadataItem item = (TiffImageMetadata.TiffMetadataItem) item2;
271             final TiffField field = item.getTiffField();
272             if (!fieldMap.containsKey(field.getTag())) {
273                 fieldMap.put(field.getTag(), field);
274             }
275         }
276         return fieldMap;
277     }
278 
279     private void compare(final File imageFile, final TiffImageMetadata oldExifMetadata,
280             final TiffImageMetadata newExifMetadata) throws ImageReadException {
281         assertNotNull(oldExifMetadata);
282         assertNotNull(newExifMetadata);
283 
284         final List<? extends ImageMetadataItem> oldDirectories = oldExifMetadata.getDirectories();
285         final List<? extends ImageMetadataItem> newDirectories = newExifMetadata.getDirectories();
286 
287         assertEquals(oldDirectories.size(), newDirectories.size());
288 
289         final Map<Integer,TiffImageMetadata.Directory> oldDirectoryMap = makeDirectoryMap(oldDirectories);
290         final Map<Integer,TiffImageMetadata.Directory> newDirectoryMap = makeDirectoryMap(newDirectories);
291 
292         assertEquals(oldDirectories.size(), oldDirectoryMap.size());
293         final List<Integer> oldDirectoryTypes = new ArrayList<>(oldDirectoryMap.keySet());
294         oldDirectoryTypes.sort(null);
295         final List<Integer> newDirectoryTypes = new ArrayList<>(newDirectoryMap.keySet());
296         newDirectoryTypes.sort(null);
297         assertEquals(oldDirectoryTypes, newDirectoryTypes);
298 
299         for (final Integer dirType : oldDirectoryTypes) {
300 
301 
302             // Debug.debug("dirType", dirType);
303 
304             final TiffImageMetadata.Directory oldDirectory = oldDirectoryMap.get(dirType);
305             final TiffImageMetadata.Directory newDirectory = newDirectoryMap.get(dirType);
306             assertNotNull(oldDirectory);
307             assertNotNull(newDirectory);
308 
309             final List<? extends ImageMetadataItem> oldItems = oldDirectory.getItems();
310             final List<? extends ImageMetadataItem> newItems = newDirectory.getItems();
311 
312             final Map<Integer,TiffField> oldFieldMap = makeFieldMap(oldItems);
313             final Map<Integer,TiffField> newFieldMap = makeFieldMap(newItems);
314 
315             final Set<Integer> missingInNew = new HashSet<>(oldFieldMap.keySet());
316             missingInNew.removeAll(newFieldMap.keySet());
317 
318             final Set<Integer> missingInOld = new HashSet<>(newFieldMap.keySet());
319             missingInOld.removeAll(oldFieldMap.keySet());
320 
321             assertTrue(missingInNew.isEmpty());
322             assertTrue(missingInOld.isEmpty());
323 
324             assertEquals(oldItems.size(), oldFieldMap.size());
325             assertEquals(oldFieldMap.keySet(), newFieldMap.keySet());
326             assertEquals(oldFieldMap.keySet(), newFieldMap.keySet());
327 
328             final List<Integer> oldFieldTags = new ArrayList<>(oldFieldMap.keySet());
329             oldFieldTags.sort(null);
330             final List<Integer> newFieldTags = new ArrayList<>(newFieldMap.keySet());
331             newFieldTags.sort(null);
332             assertEquals(oldFieldTags, newFieldTags);
333 
334             for (final Integer fieldTag : oldFieldTags) {
335                 final TiffField oldField = oldFieldMap.get(fieldTag);
336                 final TiffField newField = newFieldMap.get(fieldTag);
337 
338                 // fieldTag.
339                 assertNotNull(oldField);
340                 assertNotNull(newField);
341 
342                 assertEquals(oldField.getTag(), newField.getTag());
343                 assertEquals(dirType.intValue(), newField.getDirectoryType());
344                 assertEquals(oldField.getDirectoryType(), newField.getDirectoryType());
345 
346                 if (oldField.getFieldType() == FieldType.ASCII) {
347                     // Imaging currently doesn't correctly rewrite
348                     // strings if any byte had the highest bit set,
349                     // so if the source had that, all bets are off.
350                     final byte[] rawBytes = oldField.getByteArrayValue();
351                     boolean hasInvalidByte = false;
352                     for (final byte rawByte : rawBytes) {
353                         if ((rawByte & 0x80) != 0) {
354                             hasInvalidByte = true;
355                             break;
356                         }
357                     }
358                     if (hasInvalidByte) {
359                         continue;
360                     }
361                 }
362 
363                 assertEquals(oldField.getCount(), newField.getCount());
364                 assertEquals(oldField.isLocalValue(), newField.isLocalValue());
365 
366                 if (oldField.getTag() == 0x202) {
367                     // ignore "jpg from raw length" value. may have off-by-one
368                     // bug in certain cameras.
369                     // i.e. Sony DCR-PC110
370                     continue;
371                 }
372 
373                 if (!oldField.getTagInfo().isOffset()) {
374                     if (oldField.getTagInfo().isText()) { /* do nothing */
375                     } else if (oldField.isLocalValue()) {
376                         if (oldField.getTag() == 0x116 || oldField.getTag() == 0x117) {
377                             assertEquals(oldField.getValue(), newField.getValue());
378                         } else {
379                             assertEquals(oldField.getBytesLength(), newField.getBytesLength());
380                             assertArrayEquals(oldField.getByteArrayValue(), newField.getByteArrayValue());
381                         }
382                     } else {
383                         assertArrayEquals(oldField.getByteArrayValue(), newField.getByteArrayValue());
384                     }
385                 }
386             }
387         }
388     }
389 
390 }