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.apache.commons.imaging.common.BinaryFunctions.remainingBytes;
20  
21  import java.io.ByteArrayOutputStream;
22  import java.io.DataOutputStream;
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.OutputStream;
27  import java.nio.ByteOrder;
28  import java.util.ArrayList;
29  import java.util.List;
30  
31  import org.apache.commons.imaging.ImagingException;
32  import org.apache.commons.imaging.ImagingOverflowException;
33  import org.apache.commons.imaging.bytesource.ByteSource;
34  import org.apache.commons.imaging.common.BinaryFileParser;
35  import org.apache.commons.imaging.common.ByteConversions;
36  import org.apache.commons.imaging.formats.jpeg.JpegConstants;
37  import org.apache.commons.imaging.formats.jpeg.JpegUtils;
38  import org.apache.commons.imaging.formats.tiff.write.AbstractTiffImageWriter;
39  import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossless;
40  import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy;
41  import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
42  
43  /**
44   * Interface for Exif write/update/remove functionality for Jpeg/JFIF images.
45   *
46   * <p>
47   * See the source of the ExifMetadataUpdateExample class for example usage.
48   * </p>
49   *
50   * @see <a href=
51   *      "https://svn.apache.org/repos/asf/commons/proper/imaging/trunk/src/test/java/org/apache/commons/imaging/examples/WriteExifMetadataExample.java">
52   *      org.apache.commons.imaging.examples.WriteExifMetadataExample</a>
53   */
54  public class ExifRewriter extends BinaryFileParser {
55  
56      private abstract static class JFIFPiece {
57          protected abstract void write(OutputStream os) throws IOException;
58      }
59  
60      private static final class JFIFPieceImageData extends JFIFPiece {
61          public final byte[] markerBytes;
62          public final byte[] imageData;
63  
64          JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) {
65              this.markerBytes = markerBytes;
66              this.imageData = imageData;
67          }
68  
69          @Override
70          protected void write(final OutputStream os) throws IOException {
71              os.write(markerBytes);
72              os.write(imageData);
73          }
74      }
75  
76      private static final class JFIFPieces {
77          public final List<JFIFPiece> pieces;
78          public final List<JFIFPiece> exifPieces;
79  
80          JFIFPieces(final List<JFIFPiece> pieces, final List<JFIFPiece> exifPieces) {
81              this.pieces = pieces;
82              this.exifPieces = exifPieces;
83          }
84  
85      }
86  
87      private static class JFIFPieceSegment extends JFIFPiece {
88          public final int marker;
89          public final byte[] markerBytes;
90          public final byte[] markerLengthBytes;
91          public final byte[] segmentData;
92  
93          JFIFPieceSegment(final int marker, final byte[] markerBytes, final byte[] markerLengthBytes, final byte[] segmentData) {
94              this.marker = marker;
95              this.markerBytes = markerBytes;
96              this.markerLengthBytes = markerLengthBytes;
97              this.segmentData = segmentData;
98          }
99  
100         @Override
101         protected void write(final OutputStream os) throws IOException {
102             os.write(markerBytes);
103             os.write(markerLengthBytes);
104             os.write(segmentData);
105         }
106     }
107 
108     private static final class JFIFPieceSegmentExif extends JFIFPieceSegment {
109 
110         JFIFPieceSegmentExif(final int marker, final byte[] markerBytes, final byte[] markerLengthBytes, final byte[] segmentData) {
111             super(marker, markerBytes, markerLengthBytes, segmentData);
112         }
113     }
114 
115     /**
116      * Constructs a new instance with the default, big-endian, byte order.
117      * <p>
118      * Whether a file contains an image based on its file extension.
119      * </p>
120      */
121     public ExifRewriter() {
122         this(ByteOrder.BIG_ENDIAN);
123     }
124 
125     /**
126      * Constructs a new instance.
127      *
128      * @param byteOrder byte order of EXIF segment.
129      */
130     public ExifRewriter(final ByteOrder byteOrder) {
131         super(byteOrder);
132     }
133 
134     private JFIFPieces analyzeJfif(final ByteSource byteSource) throws ImagingException, IOException {
135         final List<JFIFPiece> pieces = new ArrayList<>();
136         final List<JFIFPiece> exifPieces = new ArrayList<>();
137 
138         final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
139             // return false to exit before reading image data.
140             @Override
141             public boolean beginSos() {
142                 return true;
143             }
144 
145             // return false to exit traversal.
146             @Override
147             public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes,
148                     final byte[] segmentData) throws
149             // ImageWriteException,
150             ImagingException, IOException {
151                 if (marker != JpegConstants.JPEG_APP1_MARKER || !JpegConstants.EXIF_IDENTIFIER_CODE.isStartOf(segmentData)) {
152                     pieces.add(new JFIFPieceSegment(marker, markerBytes, markerLengthBytes, segmentData));
153                 } else {
154                     final JFIFPiece piece = new JFIFPieceSegmentExif(marker, markerBytes, markerLengthBytes, segmentData);
155                     pieces.add(piece);
156                     exifPieces.add(piece);
157                 }
158                 return true;
159             }
160 
161             @Override
162             public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) {
163                 pieces.add(new JFIFPieceImageData(markerBytes, imageData));
164             }
165         };
166 
167         new JpegUtils().traverseJfif(byteSource, visitor);
168 
169         // GenericSegment exifSegment = exifSegmentArray[0];
170         // if (exifSegments.size() < 1)
171         // {
172         // // TODO: add support for adding, not just replacing.
173         // throw new ImageReadException("No APP1 EXIF segment found.");
174         // }
175 
176         return new JFIFPieces(pieces, exifPieces);
177     }
178 
179     /**
180      * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
181      *
182      * @param src Byte array containing JPEG image data.
183      * @param os  OutputStream to write the image to.
184      * @throws ImagingException if it fails to read the JFIF segments
185      * @throws IOException      if it fails to read the image data
186      * @throws ImagingException if it fails to write the updated data
187      */
188     public void removeExifMetadata(final byte[] src, final OutputStream os) throws ImagingException, IOException, ImagingException {
189         final ByteSource byteSource = ByteSource.array(src);
190         removeExifMetadata(byteSource, os);
191     }
192 
193     /**
194      * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
195      *
196      * @param byteSource ByteSource containing JPEG image data.
197      * @param os         OutputStream to write the image to.
198      * @throws ImagingException if it fails to read the JFIF segments
199      * @throws IOException      if it fails to read the image data
200      * @throws ImagingException if it fails to write the updated data
201      */
202     public void removeExifMetadata(final ByteSource byteSource, final OutputStream os) throws ImagingException, IOException, ImagingException {
203         final JFIFPieces jfifPieces = analyzeJfif(byteSource);
204         final List<JFIFPiece> pieces = jfifPieces.pieces;
205 
206         // Debug.debug("pieces", pieces);
207 
208         // pieces.removeAll(jfifPieces.exifSegments);
209 
210         // Debug.debug("pieces", pieces);
211 
212         writeSegmentsReplacingExif(os, pieces, null);
213     }
214 
215     /**
216      * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
217      * <p>
218      *
219      * @param src Image file.
220      * @param os  OutputStream to write the image to.
221      * @throws ImagingException if it fails to read the JFIF segments
222      * @throws IOException      if it fails to read the image data
223      * @throws ImagingException if it fails to write the updated data
224      * @see java.io.File
225      * @see java.io.OutputStream
226      * @see java.io.File
227      * @see java.io.OutputStream
228      */
229     public void removeExifMetadata(final File src, final OutputStream os) throws ImagingException, IOException, ImagingException {
230         final ByteSource byteSource = ByteSource.file(src);
231         removeExifMetadata(byteSource, os);
232     }
233 
234     /**
235      * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
236      *
237      * @param src InputStream containing JPEG image data.
238      * @param os  OutputStream to write the image to.
239      * @throws ImagingException if it fails to read the JFIF segments
240      * @throws IOException      if it fails to read the image data
241      * @throws ImagingException if it fails to write the updated data
242      */
243     public void removeExifMetadata(final InputStream src, final OutputStream os) throws ImagingException, IOException, ImagingException {
244         final ByteSource byteSource = ByteSource.inputStream(src, null);
245         removeExifMetadata(byteSource, os);
246     }
247 
248     /**
249      * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
250      *
251      * <p>
252      * Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF segment that it can't parse (such as Maker Notes), this
253      * algorithm avoids overwriting any part of the original segment that it couldn't parse. This can cause the EXIF segment to grow with each update, which is
254      * a serious issue, since all EXIF data must fit in a single APP1 segment of the JPEG image.
255      * </p>
256      *
257      * @param src       Byte array containing JPEG image data.
258      * @param os        OutputStream to write the image to.
259      * @param outputSet TiffOutputSet containing the EXIF data to write.
260      * @throws ImagingException if it fails to read the JFIF segments
261      * @throws IOException      if it fails to read the image data
262      * @throws ImagingException if it fails to write the updated data
263      */
264     public void updateExifMetadataLossless(final byte[] src, final OutputStream os, final TiffOutputSet outputSet)
265             throws ImagingException, IOException, ImagingException {
266         final ByteSource byteSource = ByteSource.array(src);
267         updateExifMetadataLossless(byteSource, os, outputSet);
268     }
269 
270     /**
271      * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
272      *
273      * <p>
274      * Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF segment that it can't parse (such as Maker Notes), this
275      * algorithm avoids overwriting any part of the original segment that it couldn't parse. This can cause the EXIF segment to grow with each update, which is
276      * a serious issue, since all EXIF data must fit in a single APP1 segment of the JPEG image.
277      * </p>
278      *
279      * @param byteSource ByteSource containing JPEG image data.
280      * @param os         OutputStream to write the image to.
281      * @param outputSet  TiffOutputSet containing the EXIF data to write.
282      * @throws ImagingException if it fails to read the JFIF segments
283      * @throws IOException      if it fails to read the image data
284      * @throws ImagingException if it fails to write the updated data
285      */
286     public void updateExifMetadataLossless(final ByteSource byteSource, final OutputStream os, final TiffOutputSet outputSet)
287             throws ImagingException, IOException, ImagingException {
288         // List outputDirectories = outputSet.getDirectories();
289         final JFIFPieces jfifPieces = analyzeJfif(byteSource);
290         final List<JFIFPiece> pieces = jfifPieces.pieces;
291 
292         final AbstractTiffImageWriter writer;
293         // Just use first APP1 segment for now.
294         // Multiple APP1 segments are rare and poorly supported.
295         if (!jfifPieces.exifPieces.isEmpty()) {
296             final JFIFPieceSegment exifPiece = (JFIFPieceSegment) jfifPieces.exifPieces.get(0);
297 
298             byte[] exifBytes = exifPiece.segmentData;
299             exifBytes = remainingBytes("trimmed exif bytes", exifBytes, 6);
300 
301             writer = new TiffImageWriterLossless(outputSet.byteOrder, exifBytes);
302 
303         } else {
304             writer = new TiffImageWriterLossy(outputSet.byteOrder);
305         }
306 
307         final boolean includeEXIFPrefix = true;
308         final byte[] newBytes = writeExifSegment(writer, outputSet, includeEXIFPrefix);
309 
310         writeSegmentsReplacingExif(os, pieces, newBytes);
311     }
312 
313     /**
314      * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
315      *
316      * <p>
317      * Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF segment that it can't parse (such as Maker Notes), this
318      * algorithm avoids overwriting any part of the original segment that it couldn't parse. This can cause the EXIF segment to grow with each update, which is
319      * a serious issue, since all EXIF data must fit in a single APP1 segment of the JPEG image.
320      * </p>
321      *
322      * @param src       Image file.
323      * @param os        OutputStream to write the image to.
324      * @param outputSet TiffOutputSet containing the EXIF data to write.
325      * @throws ImagingException if it fails to read the JFIF segments
326      * @throws IOException      if it fails to read the image data
327      * @throws ImagingException if it fails to write the updated data
328      */
329     public void updateExifMetadataLossless(final File src, final OutputStream os, final TiffOutputSet outputSet)
330             throws ImagingException, IOException, ImagingException {
331         final ByteSource byteSource = ByteSource.file(src);
332         updateExifMetadataLossless(byteSource, os, outputSet);
333     }
334 
335     /**
336      * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
337      *
338      * <p>
339      * Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF segment that it can't parse (such as Maker Notes), this
340      * algorithm avoids overwriting any part of the original segment that it couldn't parse. This can cause the EXIF segment to grow with each update, which is
341      * a serious issue, since all EXIF data must fit in a single APP1 segment of the JPEG image.
342      * </p>
343      *
344      * @param src       InputStream containing JPEG image data.
345      * @param os        OutputStream to write the image to.
346      * @param outputSet TiffOutputSet containing the EXIF data to write.
347      * @throws ImagingException if it fails to read the JFIF segments
348      * @throws IOException      if it fails to read the image data
349      * @throws ImagingException if it fails to write the updated data
350      */
351     public void updateExifMetadataLossless(final InputStream src, final OutputStream os, final TiffOutputSet outputSet)
352             throws ImagingException, IOException, ImagingException {
353         final ByteSource byteSource = ByteSource.inputStream(src, null);
354         updateExifMetadataLossless(byteSource, os, outputSet);
355     }
356 
357     /**
358      * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
359      *
360      * <p>
361      * Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, ignoring the possibility that it may be discarding data it
362      * couldn't parse (such as Maker Notes).
363      * </p>
364      *
365      * @param src       Byte array containing JPEG image data.
366      * @param os        OutputStream to write the image to.
367      * @param outputSet TiffOutputSet containing the EXIF data to write.
368      * @throws ImagingException if it fails to read the JFIF segments
369      * @throws IOException      if it fails to read the image data
370      * @throws ImagingException if it fails to write the updated data
371      */
372     public void updateExifMetadataLossy(final byte[] src, final OutputStream os, final TiffOutputSet outputSet)
373             throws ImagingException, IOException, ImagingException {
374         final ByteSource byteSource = ByteSource.array(src);
375         updateExifMetadataLossy(byteSource, os, outputSet);
376     }
377 
378     /**
379      * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
380      *
381      * <p>
382      * Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, ignoring the possibility that it may be discarding data it
383      * couldn't parse (such as Maker Notes).
384      * </p>
385      *
386      * @param byteSource ByteSource containing JPEG image data.
387      * @param os         OutputStream to write the image to.
388      * @param outputSet  TiffOutputSet containing the EXIF data to write.
389      * @throws ImagingException if it fails to read the JFIF segments
390      * @throws IOException      if it fails to read the image data
391      * @throws ImagingException if it fails to write the updated data
392      */
393     public void updateExifMetadataLossy(final ByteSource byteSource, final OutputStream os, final TiffOutputSet outputSet)
394             throws ImagingException, IOException, ImagingException {
395         final JFIFPieces jfifPieces = analyzeJfif(byteSource);
396         final List<JFIFPiece> pieces = jfifPieces.pieces;
397 
398         final AbstractTiffImageWriter writer = new TiffImageWriterLossy(outputSet.byteOrder);
399 
400         final boolean includeEXIFPrefix = true;
401         final byte[] newBytes = writeExifSegment(writer, outputSet, includeEXIFPrefix);
402 
403         writeSegmentsReplacingExif(os, pieces, newBytes);
404     }
405 
406     /**
407      * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
408      *
409      * <p>
410      * Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, ignoring the possibility that it may be discarding data it
411      * couldn't parse (such as Maker Notes).
412      * </p>
413      *
414      * @param src       Image file.
415      * @param os        OutputStream to write the image to.
416      * @param outputSet TiffOutputSet containing the EXIF data to write.
417      * @throws ImagingException if it fails to read the JFIF segments
418      * @throws IOException      if it fails to read the image data
419      * @throws ImagingException if it fails to write the updated data
420      */
421     public void updateExifMetadataLossy(final File src, final OutputStream os, final TiffOutputSet outputSet)
422             throws ImagingException, IOException, ImagingException {
423         final ByteSource byteSource = ByteSource.file(src);
424         updateExifMetadataLossy(byteSource, os, outputSet);
425     }
426 
427     /**
428      * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
429      *
430      * <p>
431      * Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, ignoring the possibility that it may be discarding data it
432      * couldn't parse (such as Maker Notes).
433      * </p>
434      *
435      * @param src       InputStream containing JPEG image data.
436      * @param os        OutputStream to write the image to.
437      * @param outputSet TiffOutputSet containing the EXIF data to write.
438      * @throws ImagingException if it fails to read the JFIF segments
439      * @throws IOException      if it fails to read the image data
440      * @throws ImagingException if it fails to write the updated data
441      */
442     public void updateExifMetadataLossy(final InputStream src, final OutputStream os, final TiffOutputSet outputSet)
443             throws ImagingException, IOException, ImagingException {
444         final ByteSource byteSource = ByteSource.inputStream(src, null);
445         updateExifMetadataLossy(byteSource, os, outputSet);
446     }
447 
448     private byte[] writeExifSegment(final AbstractTiffImageWriter writer, final TiffOutputSet outputSet, final boolean includeEXIFPrefix)
449             throws IOException, ImagingException {
450         final ByteArrayOutputStream os = new ByteArrayOutputStream();
451 
452         if (includeEXIFPrefix) {
453             JpegConstants.EXIF_IDENTIFIER_CODE.writeTo(os);
454             os.write(0);
455             os.write(0);
456         }
457 
458         writer.write(os, outputSet);
459 
460         return os.toByteArray();
461     }
462 
463     private void writeSegmentsReplacingExif(final OutputStream outputStream, final List<JFIFPiece> segments, final byte[] newBytes)
464             throws ImagingException, IOException {
465 
466         try (DataOutputStream os = new DataOutputStream(outputStream)) {
467             JpegConstants.SOI.writeTo(os);
468 
469             boolean hasExif = false;
470 
471             for (final JFIFPiece piece : segments) {
472                 if (piece instanceof JFIFPieceSegmentExif) {
473                     hasExif = true;
474                     break;
475                 }
476             }
477 
478             if (!hasExif && newBytes != null) {
479                 final byte[] markerBytes = ByteConversions.toBytes((short) JpegConstants.JPEG_APP1_MARKER, getByteOrder());
480                 if (newBytes.length > 0xffff) {
481                     throw new ImagingOverflowException("APP1 Segment is too long: " + newBytes.length);
482                 }
483                 final int markerLength = newBytes.length + 2;
484                 final byte[] markerLengthBytes = ByteConversions.toBytes((short) markerLength, getByteOrder());
485 
486                 int index = 0;
487                 final JFIFPieceSegment firstSegment = (JFIFPieceSegment) segments.get(index);
488                 if (firstSegment.marker == JpegConstants.JFIF_MARKER) {
489                     index = 1;
490                 }
491                 segments.add(index, new JFIFPieceSegmentExif(JpegConstants.JPEG_APP1_MARKER, markerBytes, markerLengthBytes, newBytes));
492             }
493 
494             boolean APP1Written = false;
495 
496             for (final JFIFPiece piece : segments) {
497                 if (piece instanceof JFIFPieceSegmentExif) {
498                     // only replace first APP1 segment; skips others.
499                     if (APP1Written) {
500                         continue;
501                     }
502                     APP1Written = true;
503 
504                     if (newBytes == null) {
505                         continue;
506                     }
507 
508                     final byte[] markerBytes = ByteConversions.toBytes((short) JpegConstants.JPEG_APP1_MARKER, getByteOrder());
509                     if (newBytes.length > 0xffff) {
510                         throw new ImagingOverflowException("APP1 Segment is too long: " + newBytes.length);
511                     }
512                     final int markerLength = newBytes.length + 2;
513                     final byte[] markerLengthBytes = ByteConversions.toBytes((short) markerLength, getByteOrder());
514 
515                     os.write(markerBytes);
516                     os.write(markerLengthBytes);
517                     os.write(newBytes);
518                 } else {
519                     piece.write(os);
520                 }
521             }
522         }
523     }
524 
525 }