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