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