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;
020
021import java.io.ByteArrayOutputStream;
022import java.io.DataOutputStream;
023import java.io.File;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.OutputStream;
027import java.nio.ByteOrder;
028import java.util.ArrayList;
029import java.util.List;
030
031import org.apache.commons.imaging.ImagingException;
032import org.apache.commons.imaging.ImagingOverflowException;
033import org.apache.commons.imaging.bytesource.ByteSource;
034import org.apache.commons.imaging.common.BinaryFileParser;
035import org.apache.commons.imaging.common.ByteConversions;
036import org.apache.commons.imaging.formats.jpeg.JpegConstants;
037import org.apache.commons.imaging.formats.jpeg.JpegUtils;
038import org.apache.commons.imaging.formats.tiff.write.AbstractTiffImageWriter;
039import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossless;
040import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy;
041import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
042
043/**
044 * Interface for Exif write/update/remove functionality for Jpeg/JFIF images.
045 *
046 * <p>
047 * See the source of the ExifMetadataUpdateExample class for example usage.
048 * </p>
049 *
050 * @see <a href=
051 *      "https://svn.apache.org/repos/asf/commons/proper/imaging/trunk/src/test/java/org/apache/commons/imaging/examples/WriteExifMetadataExample.java">
052 *      org.apache.commons.imaging.examples.WriteExifMetadataExample</a>
053 */
054public class ExifRewriter extends BinaryFileParser {
055
056    private abstract static class JFIFPiece {
057        protected abstract void write(OutputStream os) throws IOException;
058    }
059
060    private static final class JFIFPieceImageData extends JFIFPiece {
061        public final byte[] markerBytes;
062        public final byte[] imageData;
063
064        JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) {
065            this.markerBytes = markerBytes;
066            this.imageData = imageData;
067        }
068
069        @Override
070        protected void write(final OutputStream os) throws IOException {
071            os.write(markerBytes);
072            os.write(imageData);
073        }
074    }
075
076    private static final class JFIFPieces {
077        public final List<JFIFPiece> pieces;
078        public final List<JFIFPiece> exifPieces;
079
080        JFIFPieces(final List<JFIFPiece> pieces, final List<JFIFPiece> exifPieces) {
081            this.pieces = pieces;
082            this.exifPieces = exifPieces;
083        }
084
085    }
086
087    private static class JFIFPieceSegment extends JFIFPiece {
088        public final int marker;
089        public final byte[] markerBytes;
090        public final byte[] markerLengthBytes;
091        public final byte[] segmentData;
092
093        JFIFPieceSegment(final int marker, final byte[] markerBytes, final byte[] markerLengthBytes, final byte[] segmentData) {
094            this.marker = marker;
095            this.markerBytes = markerBytes;
096            this.markerLengthBytes = markerLengthBytes;
097            this.segmentData = segmentData;
098        }
099
100        @Override
101        protected void write(final OutputStream os) throws IOException {
102            os.write(markerBytes);
103            os.write(markerLengthBytes);
104            os.write(segmentData);
105        }
106    }
107
108    private static final class JFIFPieceSegmentExif extends JFIFPieceSegment {
109
110        JFIFPieceSegmentExif(final int marker, final byte[] markerBytes, final byte[] markerLengthBytes, final byte[] segmentData) {
111            super(marker, markerBytes, markerLengthBytes, segmentData);
112        }
113    }
114
115    /**
116     * Constructs a new instance with the default, big-endian, byte order.
117     * <p>
118     * Whether a file contains an image based on its file extension.
119     * </p>
120     */
121    public ExifRewriter() {
122        this(ByteOrder.BIG_ENDIAN);
123    }
124
125    /**
126     * Constructs a new instance.
127     *
128     * @param byteOrder byte order of EXIF segment.
129     */
130    public ExifRewriter(final ByteOrder byteOrder) {
131        super(byteOrder);
132    }
133
134    private JFIFPieces analyzeJfif(final ByteSource byteSource) throws ImagingException, IOException {
135        final List<JFIFPiece> pieces = new ArrayList<>();
136        final List<JFIFPiece> exifPieces = new ArrayList<>();
137
138        final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
139            // return false to exit before reading image data.
140            @Override
141            public boolean beginSos() {
142                return true;
143            }
144
145            // return false to exit traversal.
146            @Override
147            public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes,
148                    final byte[] segmentData) throws
149            // ImageWriteException,
150            ImagingException, IOException {
151                if (marker != JpegConstants.JPEG_APP1_MARKER || !JpegConstants.EXIF_IDENTIFIER_CODE.isStartOf(segmentData)) {
152                    pieces.add(new JFIFPieceSegment(marker, markerBytes, markerLengthBytes, segmentData));
153                } else {
154                    final JFIFPiece piece = new JFIFPieceSegmentExif(marker, markerBytes, markerLengthBytes, segmentData);
155                    pieces.add(piece);
156                    exifPieces.add(piece);
157                }
158                return true;
159            }
160
161            @Override
162            public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) {
163                pieces.add(new JFIFPieceImageData(markerBytes, imageData));
164            }
165        };
166
167        new JpegUtils().traverseJfif(byteSource, visitor);
168
169        // GenericSegment exifSegment = exifSegmentArray[0];
170        // if (exifSegments.size() < 1)
171        // {
172        // // TODO: add support for adding, not just replacing.
173        // throw new ImageReadException("No APP1 EXIF segment found.");
174        // }
175
176        return new JFIFPieces(pieces, exifPieces);
177    }
178
179    /**
180     * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
181     *
182     * @param src Byte array containing JPEG image data.
183     * @param os  OutputStream to write the image to.
184     * @throws ImagingException if it fails to read the JFIF segments
185     * @throws IOException      if it fails to read the image data
186     * @throws ImagingException if it fails to write the updated data
187     */
188    public void removeExifMetadata(final byte[] src, final OutputStream os) throws ImagingException, IOException, ImagingException {
189        final ByteSource byteSource = ByteSource.array(src);
190        removeExifMetadata(byteSource, os);
191    }
192
193    /**
194     * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
195     *
196     * @param byteSource ByteSource containing JPEG image data.
197     * @param os         OutputStream to write the image to.
198     * @throws ImagingException if it fails to read the JFIF segments
199     * @throws IOException      if it fails to read the image data
200     * @throws ImagingException if it fails to write the updated data
201     */
202    public void removeExifMetadata(final ByteSource byteSource, final OutputStream os) throws ImagingException, IOException, ImagingException {
203        final JFIFPieces jfifPieces = analyzeJfif(byteSource);
204        final List<JFIFPiece> pieces = jfifPieces.pieces;
205
206        // Debug.debug("pieces", pieces);
207
208        // pieces.removeAll(jfifPieces.exifSegments);
209
210        // Debug.debug("pieces", pieces);
211
212        writeSegmentsReplacingExif(os, pieces, null);
213    }
214
215    /**
216     * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
217     * <p>
218     *
219     * @param src Image file.
220     * @param os  OutputStream to write the image to.
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        final 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}