001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.imaging.formats.jpeg.exif;
018
019import static org.apache.commons.imaging.common.BinaryFunctions.remainingBytes;
020import static org.apache.commons.imaging.common.BinaryFunctions.startsWith;
021
022import java.io.ByteArrayOutputStream;
023import java.io.DataOutputStream;
024import java.io.File;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.OutputStream;
028import java.nio.ByteOrder;
029import java.util.ArrayList;
030import java.util.List;
031
032import org.apache.commons.imaging.ImageReadException;
033import org.apache.commons.imaging.ImageWriteException;
034import org.apache.commons.imaging.common.BinaryFileParser;
035import org.apache.commons.imaging.common.ByteConversions;
036import org.apache.commons.imaging.common.bytesource.ByteSource;
037import org.apache.commons.imaging.common.bytesource.ByteSourceArray;
038import org.apache.commons.imaging.common.bytesource.ByteSourceFile;
039import org.apache.commons.imaging.common.bytesource.ByteSourceInputStream;
040import org.apache.commons.imaging.formats.jpeg.JpegConstants;
041import org.apache.commons.imaging.formats.jpeg.JpegUtils;
042import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterBase;
043import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossless;
044import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy;
045import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
046
047/**
048 * Interface for Exif write/update/remove functionality for Jpeg/JFIF images.
049 *
050 * <p>See the source of the ExifMetadataUpdateExample class for example usage.</p>
051 *
052 * @see <a
053 *      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>
054 */
055public class ExifRewriter extends BinaryFileParser {
056    /**
057     * Constructor. to guess whether a file contains an image based on its file
058     * extension.
059     */
060    public ExifRewriter() {
061        this(ByteOrder.BIG_ENDIAN);
062    }
063
064    /**
065     * Constructor.
066     * <p>
067     *
068     * @param byteOrder
069     *            byte order of EXIF segment.
070     */
071    public ExifRewriter(final ByteOrder byteOrder) {
072        setByteOrder(byteOrder);
073    }
074
075    private static class JFIFPieces {
076        public final List<JFIFPiece> pieces;
077        public final List<JFIFPiece> exifPieces;
078
079        JFIFPieces(final List<JFIFPiece> pieces,
080                final List<JFIFPiece> exifPieces) {
081            this.pieces = pieces;
082            this.exifPieces = exifPieces;
083        }
084
085    }
086
087    private abstract static class JFIFPiece {
088        protected abstract void write(OutputStream os) throws IOException;
089    }
090
091    private static class JFIFPieceSegment extends JFIFPiece {
092        public final int marker;
093        public final byte[] markerBytes;
094        public final byte[] markerLengthBytes;
095        public final byte[] segmentData;
096
097        JFIFPieceSegment(final int marker, final byte[] markerBytes,
098                final byte[] markerLengthBytes, final byte[] segmentData) {
099            this.marker = marker;
100            this.markerBytes = markerBytes;
101            this.markerLengthBytes = markerLengthBytes;
102            this.segmentData = segmentData;
103        }
104
105        @Override
106        protected void write(final OutputStream os) throws IOException {
107            os.write(markerBytes);
108            os.write(markerLengthBytes);
109            os.write(segmentData);
110        }
111    }
112
113    private static class JFIFPieceSegmentExif extends JFIFPieceSegment {
114
115        JFIFPieceSegmentExif(final int marker, final byte[] markerBytes,
116                final byte[] markerLengthBytes, final byte[] segmentData) {
117            super(marker, markerBytes, markerLengthBytes, segmentData);
118        }
119    }
120
121    private static class JFIFPieceImageData extends JFIFPiece {
122        public final byte[] markerBytes;
123        public final byte[] imageData;
124
125        JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) {
126            this.markerBytes = markerBytes;
127            this.imageData = imageData;
128        }
129
130        @Override
131        protected void write(final OutputStream os) throws IOException {
132            os.write(markerBytes);
133            os.write(imageData);
134        }
135    }
136
137    private JFIFPieces analyzeJFIF(final ByteSource byteSource) throws ImageReadException, IOException {
138        final List<JFIFPiece> pieces = new ArrayList<>();
139        final List<JFIFPiece> exifPieces = new ArrayList<>();
140
141        final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
142            // return false to exit before reading image data.
143            @Override
144            public boolean beginSOS() {
145                return true;
146            }
147
148            @Override
149            public void visitSOS(final int marker, final byte[] markerBytes, final byte[] imageData) {
150                pieces.add(new JFIFPieceImageData(markerBytes, imageData));
151            }
152
153            // return false to exit traversal.
154            @Override
155            public boolean visitSegment(final int marker, final byte[] markerBytes,
156                    final int markerLength, final byte[] markerLengthBytes,
157                    final byte[] segmentData) throws
158            // ImageWriteException,
159                    ImageReadException, IOException {
160                if (marker != JpegConstants.JPEG_APP1_MARKER) {
161                    pieces.add(new JFIFPieceSegment(marker, markerBytes,
162                            markerLengthBytes, segmentData));
163                } else if (!startsWith(segmentData,
164                        JpegConstants.EXIF_IDENTIFIER_CODE)) {
165                    pieces.add(new JFIFPieceSegment(marker, markerBytes,
166                            markerLengthBytes, segmentData));
167                // } else if (exifSegmentArray[0] != null) {
168                // // TODO: add support for multiple segments
169                // throw new ImageReadException(
170                // "More than one APP1 EXIF segment.");
171                } else {
172                    final JFIFPiece piece = new JFIFPieceSegmentExif(marker,
173                            markerBytes, markerLengthBytes, segmentData);
174                    pieces.add(piece);
175                    exifPieces.add(piece);
176                }
177                return true;
178            }
179        };
180
181        new JpegUtils().traverseJFIF(byteSource, visitor);
182
183        // GenericSegment exifSegment = exifSegmentArray[0];
184        // if (exifSegments.size() < 1)
185        // {
186        // // TODO: add support for adding, not just replacing.
187        // throw new ImageReadException("No APP1 EXIF segment found.");
188        // }
189
190        return new JFIFPieces(pieces, exifPieces);
191    }
192
193    /**
194     * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1
195     * segment), and writes the result to a stream.
196     * <p>
197     *
198     * @param src
199     *            Image file.
200     * @param os
201     *            OutputStream to write the image to.
202     *
203     * @throws ImageReadException if it fails to read the JFIF segments
204     * @throws IOException if it fails to read the image data
205     * @throws ImageWriteException if it fails to write the updated data
206     * @see java.io.File
207     * @see java.io.OutputStream
208     * @see java.io.File
209     * @see java.io.OutputStream
210     */
211    public void removeExifMetadata(final File src, final OutputStream os)
212            throws ImageReadException, IOException, ImageWriteException {
213        final ByteSource byteSource = new ByteSourceFile(src);
214        removeExifMetadata(byteSource, os);
215    }
216
217    /**
218     * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1
219     * segment), and writes the result to a stream.
220     *
221     * @param src
222     *            Byte array containing Jpeg image data.
223     * @param os
224     *            OutputStream to write the image to.
225     * @throws ImageReadException if it fails to read the JFIF segments
226     * @throws IOException if it fails to read the image data
227     * @throws ImageWriteException if it fails to write the updated data
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     *
239     * @param src
240     *            InputStream containing Jpeg image data.
241     * @param os
242     *            OutputStream to write the image to.
243     * @throws ImageReadException if it fails to read the JFIF segments
244     * @throws IOException if it fails to read the image data
245     * @throws ImageWriteException if it fails to write the updated data
246     */
247    public void removeExifMetadata(final InputStream src, final OutputStream os)
248            throws ImageReadException, IOException, ImageWriteException {
249        final ByteSource byteSource = new ByteSourceInputStream(src, null);
250        removeExifMetadata(byteSource, os);
251    }
252
253    /**
254     * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1
255     * segment), and writes the result to a stream.
256     *
257     * @param byteSource
258     *            ByteSource containing Jpeg image data.
259     * @param os
260     *            OutputStream to write the image to.
261     * @throws ImageReadException if it fails to read the JFIF segments
262     * @throws IOException if it fails to read the image data
263     * @throws ImageWriteException if it fails to write the updated data
264     */
265    public void removeExifMetadata(final ByteSource byteSource, final OutputStream os)
266            throws ImageReadException, IOException, ImageWriteException {
267        final JFIFPieces jfifPieces = analyzeJFIF(byteSource);
268        final List<JFIFPiece> pieces = jfifPieces.pieces;
269
270        // Debug.debug("pieces", pieces);
271
272        // pieces.removeAll(jfifPieces.exifSegments);
273
274        // Debug.debug("pieces", pieces);
275
276        writeSegmentsReplacingExif(os, pieces, null);
277    }
278
279    /**
280     * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a
281     * stream.
282     *
283     * <p>Note that this uses the "Lossless" approach - in order to preserve data
284     * embedded in the EXIF segment that it can't parse (such as Maker Notes),
285     * this algorithm avoids overwriting any part of the original segment that
286     * it couldn't parse. This can cause the EXIF segment to grow with each
287     * update, which is a serious issue, since all EXIF data must fit in a
288     * single APP1 segment of the Jpeg image.</p>
289     *
290     * @param src
291     *            Image file.
292     * @param os
293     *            OutputStream to write the image to.
294     * @param outputSet
295     *            TiffOutputSet containing the EXIF data to write.
296     * @throws ImageReadException if it fails to read the JFIF segments
297     * @throws IOException if it fails to read the image data
298     * @throws ImageWriteException if it fails to write the updated data
299     */
300    public void updateExifMetadataLossless(final File src, final OutputStream os,
301            final TiffOutputSet outputSet) throws ImageReadException, IOException,
302            ImageWriteException {
303        final ByteSource byteSource = new ByteSourceFile(src);
304        updateExifMetadataLossless(byteSource, os, outputSet);
305    }
306
307    /**
308     * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a
309     * stream.
310     *
311     * <p>Note that this uses the "Lossless" approach - in order to preserve data
312     * embedded in the EXIF segment that it can't parse (such as Maker Notes),
313     * this algorithm avoids overwriting any part of the original segment that
314     * it couldn't parse. This can cause the EXIF segment to grow with each
315     * update, which is a serious issue, since all EXIF data must fit in a
316     * single APP1 segment of the Jpeg image.</p>
317     *
318     * @param src
319     *            Byte array containing Jpeg image data.
320     * @param os
321     *            OutputStream to write the image to.
322     * @param outputSet
323     *            TiffOutputSet containing the EXIF data to write.
324     * @throws ImageReadException if it fails to read the JFIF segments
325     * @throws IOException if it fails to read the image data
326     * @throws ImageWriteException if it fails to write the updated data
327     */
328    public void updateExifMetadataLossless(final byte[] src, final OutputStream os,
329            final TiffOutputSet outputSet) throws ImageReadException, IOException,
330            ImageWriteException {
331        final ByteSource byteSource = new ByteSourceArray(src);
332        updateExifMetadataLossless(byteSource, os, outputSet);
333    }
334
335    /**
336     * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a
337     * stream.
338     *
339     * <p>Note that this uses the "Lossless" approach - in order to preserve data
340     * embedded in the EXIF segment that it can't parse (such as Maker Notes),
341     * this algorithm avoids overwriting any part of the original segment that
342     * it couldn't parse. This can cause the EXIF segment to grow with each
343     * update, which is a serious issue, since all EXIF data must fit in a
344     * single APP1 segment of the Jpeg image.</p>
345     *
346     * @param src
347     *            InputStream containing Jpeg image data.
348     * @param os
349     *            OutputStream to write the image to.
350     * @param outputSet
351     *            TiffOutputSet containing the EXIF data to write.
352     * @throws ImageReadException if it fails to read the JFIF segments
353     * @throws IOException if it fails to read the image data
354     * @throws ImageWriteException if it fails to write the updated data
355     */
356    public void updateExifMetadataLossless(final InputStream src, final OutputStream os,
357            final TiffOutputSet outputSet) throws ImageReadException, IOException,
358            ImageWriteException {
359        final ByteSource byteSource = new ByteSourceInputStream(src, null);
360        updateExifMetadataLossless(byteSource, os, outputSet);
361    }
362
363    /**
364     * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a
365     * stream.
366     *
367     * <p>Note that this uses the "Lossless" approach - in order to preserve data
368     * embedded in the EXIF segment that it can't parse (such as Maker Notes),
369     * this algorithm avoids overwriting any part of the original segment that
370     * it couldn't parse. This can cause the EXIF segment to grow with each
371     * update, which is a serious issue, since all EXIF data must fit in a
372     * single APP1 segment of the Jpeg image.</p>
373     *
374     * @param byteSource
375     *            ByteSource containing Jpeg image data.
376     * @param os
377     *            OutputStream to write the image to.
378     * @param outputSet
379     *            TiffOutputSet containing the EXIF data to write.
380     * @throws ImageReadException if it fails to read the JFIF segments
381     * @throws IOException if it fails to read the image data
382     * @throws ImageWriteException if it fails to write the updated data
383     */
384    public void updateExifMetadataLossless(final ByteSource byteSource,
385            final OutputStream os, final TiffOutputSet outputSet)
386            throws ImageReadException, IOException, ImageWriteException {
387        // List outputDirectories = outputSet.getDirectories();
388        final JFIFPieces jfifPieces = analyzeJFIF(byteSource);
389        final List<JFIFPiece> pieces = jfifPieces.pieces;
390
391        TiffImageWriterBase writer;
392        // Just use first APP1 segment for now.
393        // Multiple APP1 segments are rare and poorly supported.
394        if (!jfifPieces.exifPieces.isEmpty()) {
395            final JFIFPieceSegment exifPiece = (JFIFPieceSegment) jfifPieces.exifPieces.get(0);
396
397            byte[] exifBytes = exifPiece.segmentData;
398            exifBytes = remainingBytes("trimmed exif bytes", exifBytes, 6);
399
400            writer = new TiffImageWriterLossless(outputSet.byteOrder, exifBytes);
401
402        } else {
403            writer = new TiffImageWriterLossy(outputSet.byteOrder);
404        }
405
406        final boolean includeEXIFPrefix = true;
407        final byte[] newBytes = writeExifSegment(writer, outputSet, includeEXIFPrefix);
408
409        writeSegmentsReplacingExif(os, pieces, newBytes);
410    }
411
412    /**
413     * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a
414     * stream.
415     *
416     * <p>Note that this uses the "Lossy" approach - the algorithm overwrites the
417     * entire EXIF segment, ignoring the possibility that it may be discarding
418     * data it couldn't parse (such as Maker Notes).</p>
419     *
420     * @param src
421     *            Byte array containing Jpeg image data.
422     * @param os
423     *            OutputStream to write the image to.
424     * @param outputSet
425     *            TiffOutputSet containing the EXIF data to write.
426     * @throws ImageReadException if it fails to read the JFIF segments
427     * @throws IOException if it fails to read the image data
428     * @throws ImageWriteException if it fails to write the updated data
429     */
430    public void updateExifMetadataLossy(final byte[] src, final OutputStream os,
431            final TiffOutputSet outputSet) throws ImageReadException, IOException,
432            ImageWriteException {
433        final ByteSource byteSource = new ByteSourceArray(src);
434        updateExifMetadataLossy(byteSource, os, outputSet);
435    }
436
437    /**
438     * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a
439     * stream.
440     *
441     * <p>Note that this uses the "Lossy" approach - the algorithm overwrites the
442     * entire EXIF segment, ignoring the possibility that it may be discarding
443     * data it couldn't parse (such as Maker Notes).</p>
444     *
445     * @param src
446     *            InputStream containing Jpeg image data.
447     * @param os
448     *            OutputStream to write the image to.
449     * @param outputSet
450     *            TiffOutputSet containing the EXIF data to write.
451     * @throws ImageReadException if it fails to read the JFIF segments
452     * @throws IOException if it fails to read the image data
453     * @throws ImageWriteException if it fails to write the updated data
454     */
455    public void updateExifMetadataLossy(final InputStream src, final OutputStream os,
456            final TiffOutputSet outputSet) throws ImageReadException, IOException,
457            ImageWriteException {
458        final ByteSource byteSource = new ByteSourceInputStream(src, null);
459        updateExifMetadataLossy(byteSource, os, outputSet);
460    }
461
462    /**
463     * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a
464     * stream.
465     *
466     * <p>Note that this uses the "Lossy" approach - the algorithm overwrites the
467     * entire EXIF segment, ignoring the possibility that it may be discarding
468     * data it couldn't parse (such as Maker Notes).</p>
469     *
470     * @param src
471     *            Image file.
472     * @param os
473     *            OutputStream to write the image to.
474     * @param outputSet
475     *            TiffOutputSet containing the EXIF data to write.
476     * @throws ImageReadException if it fails to read the JFIF segments
477     * @throws IOException if it fails to read the image data
478     * @throws ImageWriteException if it fails to write the updated data
479     */
480    public void updateExifMetadataLossy(final File src, final OutputStream os,
481            final TiffOutputSet outputSet) throws ImageReadException, IOException,
482            ImageWriteException {
483        final ByteSource byteSource = new ByteSourceFile(src);
484        updateExifMetadataLossy(byteSource, os, outputSet);
485    }
486
487    /**
488     * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a
489     * stream.
490     *
491     * <p>Note that this uses the "Lossy" approach - the algorithm overwrites the
492     * entire EXIF segment, ignoring the possibility that it may be discarding
493     * data it couldn't parse (such as Maker Notes).</p>
494     *
495     * @param byteSource
496     *            ByteSource containing Jpeg image data.
497     * @param os
498     *            OutputStream to write the image to.
499     * @param outputSet
500     *            TiffOutputSet containing the EXIF data to write.
501     * @throws ImageReadException if it fails to read the JFIF segments
502     * @throws IOException if it fails to read the image data
503     * @throws ImageWriteException if it fails to write the updated data
504     */
505    public void updateExifMetadataLossy(final ByteSource byteSource, final OutputStream os,
506            final TiffOutputSet outputSet) throws ImageReadException, IOException,
507            ImageWriteException {
508        final JFIFPieces jfifPieces = analyzeJFIF(byteSource);
509        final List<JFIFPiece> pieces = jfifPieces.pieces;
510
511        final TiffImageWriterBase writer = new TiffImageWriterLossy(
512                outputSet.byteOrder);
513
514        final boolean includeEXIFPrefix = true;
515        final byte[] newBytes = writeExifSegment(writer, outputSet, includeEXIFPrefix);
516
517        writeSegmentsReplacingExif(os, pieces, newBytes);
518    }
519
520    private void writeSegmentsReplacingExif(final OutputStream outputStream,
521            final List<JFIFPiece> segments, final byte[] newBytes)
522            throws ImageWriteException, IOException {
523
524        try (DataOutputStream os = new DataOutputStream(outputStream)) {
525            JpegConstants.SOI.writeTo(os);
526
527            boolean hasExif = false;
528
529            for (final JFIFPiece piece : segments) {
530                if (piece instanceof JFIFPieceSegmentExif) {
531                    hasExif = true;
532                    break;
533                }
534            }
535
536            if (!hasExif && newBytes != null) {
537                final byte[] markerBytes = ByteConversions.toBytes((short) JpegConstants.JPEG_APP1_MARKER, getByteOrder());
538                if (newBytes.length > 0xffff) {
539                    throw new ExifOverflowException(
540                            "APP1 Segment is too long: " + newBytes.length);
541                }
542                final int markerLength = newBytes.length + 2;
543                final byte[] markerLengthBytes = ByteConversions.toBytes((short) markerLength, getByteOrder());
544
545                int index = 0;
546                final JFIFPieceSegment firstSegment = (JFIFPieceSegment) segments.get(index);
547                if (firstSegment.marker == JpegConstants.JFIF_MARKER) {
548                    index = 1;
549                }
550                segments.add(index, new JFIFPieceSegmentExif(JpegConstants.JPEG_APP1_MARKER,
551                        markerBytes, markerLengthBytes, newBytes));
552            }
553
554            boolean APP1Written = false;
555
556            for (final JFIFPiece piece : segments) {
557                if (piece instanceof JFIFPieceSegmentExif) {
558                    // only replace first APP1 segment; skips others.
559                    if (APP1Written) {
560                        continue;
561                    }
562                    APP1Written = true;
563
564                    if (newBytes == null) {
565                        continue;
566                    }
567
568                    final byte[] markerBytes = ByteConversions.toBytes((short) JpegConstants.JPEG_APP1_MARKER, getByteOrder());
569                    if (newBytes.length > 0xffff) {
570                        throw new ExifOverflowException(
571                                "APP1 Segment is too long: " + newBytes.length);
572                    }
573                    final int markerLength = newBytes.length + 2;
574                    final byte[] markerLengthBytes = ByteConversions.toBytes((short) markerLength, getByteOrder());
575
576                    os.write(markerBytes);
577                    os.write(markerLengthBytes);
578                    os.write(newBytes);
579                } else {
580                    piece.write(os);
581                }
582            }
583        }
584    }
585
586    public static class ExifOverflowException extends ImageWriteException {
587        private static final long serialVersionUID = 1401484357224931218L;
588
589        public ExifOverflowException(final String message) {
590            super(message);
591        }
592    }
593
594    private byte[] writeExifSegment(final TiffImageWriterBase writer,
595            final TiffOutputSet outputSet, final boolean includeEXIFPrefix)
596            throws IOException, ImageWriteException {
597        final ByteArrayOutputStream os = new ByteArrayOutputStream();
598
599        if (includeEXIFPrefix) {
600            JpegConstants.EXIF_IDENTIFIER_CODE.writeTo(os);
601            os.write(0);
602            os.write(0);
603        }
604
605        writer.write(os, outputSet);
606
607        return os.toByteArray();
608    }
609
610}