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.ImagingException;
33  import org.apache.commons.imaging.ImagingOverflowException;
34  import org.apache.commons.imaging.bytesource.ByteSource;
35  import org.apache.commons.imaging.common.BinaryFileParser;
36  import org.apache.commons.imaging.common.ByteConversions;
37  import org.apache.commons.imaging.formats.jpeg.JpegConstants;
38  import org.apache.commons.imaging.formats.jpeg.JpegUtils;
39  import org.apache.commons.imaging.formats.tiff.write.AbstractTiffImageWriter;
40  import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossless;
41  import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy;
42  import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
43  
44  /**
45   * Interface for Exif write/update/remove functionality for Jpeg/JFIF images.
46   *
47   * <p>
48   * See the source of the ExifMetadataUpdateExample class for example usage.
49   * </p>
50   *
51   * @see <a href=
52   *      "https://svn.apache.org/repos/asf/commons/proper/imaging/trunk/src/test/java/org/apache/commons/imaging/examples/WriteExifMetadataExample.java">
53   *      org.apache.commons.imaging.examples.WriteExifMetadataExample</a>
54   */
55  public class ExifRewriter extends BinaryFileParser {
56  
57      private abstract static class JFIFPiece {
58          protected abstract void write(OutputStream os) throws IOException;
59      }
60  
61      private static final class JFIFPieceImageData extends JFIFPiece {
62          public final byte[] markerBytes;
63          public final byte[] imageData;
64  
65          JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) {
66              this.markerBytes = markerBytes;
67              this.imageData = imageData;
68          }
69  
70          @Override
71          protected void write(final OutputStream os) throws IOException {
72              os.write(markerBytes);
73              os.write(imageData);
74          }
75      }
76  
77      private static final class JFIFPieces {
78          public final List<JFIFPiece> pieces;
79          public final List<JFIFPiece> exifPieces;
80  
81          JFIFPieces(final List<JFIFPiece> pieces, final List<JFIFPiece> exifPieces) {
82              this.pieces = pieces;
83              this.exifPieces = exifPieces;
84          }
85  
86      }
87  
88      private static class JFIFPieceSegment extends JFIFPiece {
89          public final int marker;
90          public final byte[] markerBytes;
91          public final byte[] markerLengthBytes;
92          public final byte[] segmentData;
93  
94          JFIFPieceSegment(final int marker, final byte[] markerBytes, final byte[] markerLengthBytes, final byte[] segmentData) {
95              this.marker = marker;
96              this.markerBytes = markerBytes;
97              this.markerLengthBytes = markerLengthBytes;
98              this.segmentData = segmentData;
99          }
100 
101         @Override
102         protected void write(final OutputStream os) throws IOException {
103             os.write(markerBytes);
104             os.write(markerLengthBytes);
105             os.write(segmentData);
106         }
107     }
108 
109     private static final class JFIFPieceSegmentExif extends JFIFPieceSegment {
110 
111         JFIFPieceSegmentExif(final int marker, final byte[] markerBytes, final byte[] markerLengthBytes, final byte[] segmentData) {
112             super(marker, markerBytes, markerLengthBytes, segmentData);
113         }
114     }
115 
116     /**
117      * Constructs a new instance. to guess whether a file contains an image based on its file extension.
118      */
119     public ExifRewriter() {
120         this(ByteOrder.BIG_ENDIAN);
121     }
122 
123     /**
124      * Constructs a new instance.
125      * <p>
126      *
127      * @param byteOrder byte order of EXIF segment.
128      */
129     public ExifRewriter(final ByteOrder byteOrder) {
130         super(byteOrder);
131     }
132 
133     private JFIFPieces analyzeJfif(final ByteSource byteSource) throws ImagingException, IOException {
134         final List<JFIFPiece> pieces = new ArrayList<>();
135         final List<JFIFPiece> exifPieces = new ArrayList<>();
136 
137         final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
138             // return false to exit before reading image data.
139             @Override
140             public boolean beginSos() {
141                 return true;
142             }
143 
144             // return false to exit traversal.
145             @Override
146             public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes,
147                     final byte[] segmentData) throws
148             // ImageWriteException,
149             ImagingException, IOException {
150                 if (marker != JpegConstants.JPEG_APP1_MARKER || !startsWith(segmentData, JpegConstants.EXIF_IDENTIFIER_CODE)) {
151                     pieces.add(new JFIFPieceSegment(marker, markerBytes, markerLengthBytes, segmentData));
152                 } else {
153                     final JFIFPiece piece = new JFIFPieceSegmentExif(marker, markerBytes, markerLengthBytes, segmentData);
154                     pieces.add(piece);
155                     exifPieces.add(piece);
156                 }
157                 return true;
158             }
159 
160             @Override
161             public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) {
162                 pieces.add(new JFIFPieceImageData(markerBytes, imageData));
163             }
164         };
165 
166         new JpegUtils().traverseJfif(byteSource, visitor);
167 
168         // GenericSegment exifSegment = exifSegmentArray[0];
169         // if (exifSegments.size() < 1)
170         // {
171         // // TODO: add support for adding, not just replacing.
172         // throw new ImageReadException("No APP1 EXIF segment found.");
173         // }
174 
175         return new JFIFPieces(pieces, exifPieces);
176     }
177 
178     /**
179      * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
180      *
181      * @param src Byte array containing JPEG image data.
182      * @param os  OutputStream to write the image to.
183      * @throws ImagingException if it fails to read the JFIF segments
184      * @throws IOException      if it fails to read the image data
185      * @throws ImagingException if it fails to write the updated data
186      */
187     public void removeExifMetadata(final byte[] src, final OutputStream os) throws ImagingException, IOException, ImagingException {
188         final ByteSource byteSource = ByteSource.array(src);
189         removeExifMetadata(byteSource, os);
190     }
191 
192     /**
193      * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
194      *
195      * @param byteSource ByteSource containing JPEG image data.
196      * @param os         OutputStream to write the image to.
197      * @throws ImagingException if it fails to read the JFIF segments
198      * @throws IOException      if it fails to read the image data
199      * @throws ImagingException if it fails to write the updated data
200      */
201     public void removeExifMetadata(final ByteSource byteSource, final OutputStream os) throws ImagingException, IOException, ImagingException {
202         final JFIFPieces jfifPieces = analyzeJfif(byteSource);
203         final List<JFIFPiece> pieces = jfifPieces.pieces;
204 
205         // Debug.debug("pieces", pieces);
206 
207         // pieces.removeAll(jfifPieces.exifSegments);
208 
209         // Debug.debug("pieces", pieces);
210 
211         writeSegmentsReplacingExif(os, pieces, null);
212     }
213 
214     /**
215      * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
216      * <p>
217      *
218      * @param src Image file.
219      * @param os  OutputStream to write the image to.
220      *
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         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 }