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