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