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