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.ImagingException;
033import org.apache.commons.imaging.ImagingOverflowException;
034import org.apache.commons.imaging.bytesource.ByteSource;
035import org.apache.commons.imaging.common.BinaryFileParser;
036import org.apache.commons.imaging.common.ByteConversions;
037import org.apache.commons.imaging.formats.jpeg.JpegConstants;
038import org.apache.commons.imaging.formats.jpeg.JpegUtils;
039import org.apache.commons.imaging.formats.tiff.write.AbstractTiffImageWriter;
040import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossless;
041import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy;
042import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
043
044/**
045 * Interface for Exif write/update/remove functionality for Jpeg/JFIF images.
046 *
047 * <p>
048 * See the source of the ExifMetadataUpdateExample class for example usage.
049 * </p>
050 *
051 * @see <a href=
052 *      "https://svn.apache.org/repos/asf/commons/proper/imaging/trunk/src/test/java/org/apache/commons/imaging/examples/WriteExifMetadataExample.java">
053 *      org.apache.commons.imaging.examples.WriteExifMetadataExample</a>
054 */
055public class ExifRewriter extends BinaryFileParser {
056
057    private abstract static class JFIFPiece {
058        protected abstract void write(OutputStream os) throws IOException;
059    }
060
061    private static final class JFIFPieceImageData extends JFIFPiece {
062        public final byte[] markerBytes;
063        public final byte[] imageData;
064
065        JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) {
066            this.markerBytes = markerBytes;
067            this.imageData = imageData;
068        }
069
070        @Override
071        protected void write(final OutputStream os) throws IOException {
072            os.write(markerBytes);
073            os.write(imageData);
074        }
075    }
076
077    private static final class JFIFPieces {
078        public final List<JFIFPiece> pieces;
079        public final List<JFIFPiece> exifPieces;
080
081        JFIFPieces(final List<JFIFPiece> pieces, final List<JFIFPiece> exifPieces) {
082            this.pieces = pieces;
083            this.exifPieces = exifPieces;
084        }
085
086    }
087
088    private static class JFIFPieceSegment extends JFIFPiece {
089        public final int marker;
090        public final byte[] markerBytes;
091        public final byte[] markerLengthBytes;
092        public final byte[] segmentData;
093
094        JFIFPieceSegment(final int marker, final byte[] markerBytes, final byte[] markerLengthBytes, final byte[] segmentData) {
095            this.marker = marker;
096            this.markerBytes = markerBytes;
097            this.markerLengthBytes = markerLengthBytes;
098            this.segmentData = segmentData;
099        }
100
101        @Override
102        protected void write(final OutputStream os) throws IOException {
103            os.write(markerBytes);
104            os.write(markerLengthBytes);
105            os.write(segmentData);
106        }
107    }
108
109    private static final class JFIFPieceSegmentExif extends JFIFPieceSegment {
110
111        JFIFPieceSegmentExif(final int marker, final byte[] markerBytes, final byte[] markerLengthBytes, final byte[] segmentData) {
112            super(marker, markerBytes, markerLengthBytes, segmentData);
113        }
114    }
115
116    /**
117     * Constructs a new instance. to guess whether a file contains an image based on its file extension.
118     */
119    public ExifRewriter() {
120        this(ByteOrder.BIG_ENDIAN);
121    }
122
123    /**
124     * Constructs a new instance.
125     * <p>
126     *
127     * @param byteOrder byte order of EXIF segment.
128     */
129    public ExifRewriter(final ByteOrder byteOrder) {
130        super(byteOrder);
131    }
132
133    private JFIFPieces analyzeJfif(final ByteSource byteSource) throws ImagingException, IOException {
134        final List<JFIFPiece> pieces = new ArrayList<>();
135        final List<JFIFPiece> exifPieces = new ArrayList<>();
136
137        final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
138            // return false to exit before reading image data.
139            @Override
140            public boolean beginSos() {
141                return true;
142            }
143
144            // return false to exit traversal.
145            @Override
146            public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes,
147                    final byte[] segmentData) throws
148            // ImageWriteException,
149            ImagingException, IOException {
150                if (marker != JpegConstants.JPEG_APP1_MARKER || !startsWith(segmentData, JpegConstants.EXIF_IDENTIFIER_CODE)) {
151                    pieces.add(new JFIFPieceSegment(marker, markerBytes, markerLengthBytes, segmentData));
152                } else {
153                    final JFIFPiece piece = new JFIFPieceSegmentExif(marker, markerBytes, markerLengthBytes, segmentData);
154                    pieces.add(piece);
155                    exifPieces.add(piece);
156                }
157                return true;
158            }
159
160            @Override
161            public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) {
162                pieces.add(new JFIFPieceImageData(markerBytes, imageData));
163            }
164        };
165
166        new JpegUtils().traverseJfif(byteSource, visitor);
167
168        // GenericSegment exifSegment = exifSegmentArray[0];
169        // if (exifSegments.size() < 1)
170        // {
171        // // TODO: add support for adding, not just replacing.
172        // throw new ImageReadException("No APP1 EXIF segment found.");
173        // }
174
175        return new JFIFPieces(pieces, exifPieces);
176    }
177
178    /**
179     * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
180     *
181     * @param src Byte array containing JPEG image data.
182     * @param os  OutputStream to write the image to.
183     * @throws ImagingException if it fails to read the JFIF segments
184     * @throws IOException      if it fails to read the image data
185     * @throws ImagingException if it fails to write the updated data
186     */
187    public void removeExifMetadata(final byte[] src, final OutputStream os) throws ImagingException, IOException, ImagingException {
188        final ByteSource byteSource = ByteSource.array(src);
189        removeExifMetadata(byteSource, os);
190    }
191
192    /**
193     * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
194     *
195     * @param byteSource ByteSource containing JPEG image data.
196     * @param os         OutputStream to write the image to.
197     * @throws ImagingException if it fails to read the JFIF segments
198     * @throws IOException      if it fails to read the image data
199     * @throws ImagingException if it fails to write the updated data
200     */
201    public void removeExifMetadata(final ByteSource byteSource, final OutputStream os) throws ImagingException, IOException, ImagingException {
202        final JFIFPieces jfifPieces = analyzeJfif(byteSource);
203        final List<JFIFPiece> pieces = jfifPieces.pieces;
204
205        // Debug.debug("pieces", pieces);
206
207        // pieces.removeAll(jfifPieces.exifSegments);
208
209        // Debug.debug("pieces", pieces);
210
211        writeSegmentsReplacingExif(os, pieces, null);
212    }
213
214    /**
215     * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
216     * <p>
217     *
218     * @param src Image file.
219     * @param os  OutputStream to write the image to.
220     *
221     * @throws ImagingException if it fails to read the JFIF segments
222     * @throws IOException      if it fails to read the image data
223     * @throws ImagingException if it fails to write the updated data
224     * @see java.io.File
225     * @see java.io.OutputStream
226     * @see java.io.File
227     * @see java.io.OutputStream
228     */
229    public void removeExifMetadata(final File src, final OutputStream os) throws ImagingException, IOException, ImagingException {
230        final ByteSource byteSource = ByteSource.file(src);
231        removeExifMetadata(byteSource, os);
232    }
233
234    /**
235     * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
236     *
237     * @param src InputStream containing JPEG image data.
238     * @param os  OutputStream to write the image to.
239     * @throws ImagingException if it fails to read the JFIF segments
240     * @throws IOException      if it fails to read the image data
241     * @throws ImagingException if it fails to write the updated data
242     */
243    public void removeExifMetadata(final InputStream src, final OutputStream os) throws ImagingException, IOException, ImagingException {
244        final ByteSource byteSource = ByteSource.inputStream(src, null);
245        removeExifMetadata(byteSource, os);
246    }
247
248    /**
249     * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
250     *
251     * <p>
252     * Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF segment that it can't parse (such as Maker Notes), this
253     * algorithm avoids overwriting any part of the original segment that it couldn't parse. This can cause the EXIF segment to grow with each update, which is
254     * a serious issue, since all EXIF data must fit in a single APP1 segment of the JPEG image.
255     * </p>
256     *
257     * @param src       Byte array containing JPEG image data.
258     * @param os        OutputStream to write the image to.
259     * @param outputSet TiffOutputSet containing the EXIF data to write.
260     * @throws ImagingException if it fails to read the JFIF segments
261     * @throws IOException      if it fails to read the image data
262     * @throws ImagingException if it fails to write the updated data
263     */
264    public void updateExifMetadataLossless(final byte[] src, final OutputStream os, final TiffOutputSet outputSet)
265            throws ImagingException, IOException, ImagingException {
266        final ByteSource byteSource = ByteSource.array(src);
267        updateExifMetadataLossless(byteSource, os, outputSet);
268    }
269
270    /**
271     * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
272     *
273     * <p>
274     * Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF segment that it can't parse (such as Maker Notes), this
275     * algorithm avoids overwriting any part of the original segment that it couldn't parse. This can cause the EXIF segment to grow with each update, which is
276     * a serious issue, since all EXIF data must fit in a single APP1 segment of the JPEG image.
277     * </p>
278     *
279     * @param byteSource ByteSource containing JPEG image data.
280     * @param os         OutputStream to write the image to.
281     * @param outputSet  TiffOutputSet containing the EXIF data to write.
282     * @throws ImagingException if it fails to read the JFIF segments
283     * @throws IOException      if it fails to read the image data
284     * @throws ImagingException if it fails to write the updated data
285     */
286    public void updateExifMetadataLossless(final ByteSource byteSource, final OutputStream os, final TiffOutputSet outputSet)
287            throws ImagingException, IOException, ImagingException {
288        // List outputDirectories = outputSet.getDirectories();
289        final JFIFPieces jfifPieces = analyzeJfif(byteSource);
290        final List<JFIFPiece> pieces = jfifPieces.pieces;
291
292        AbstractTiffImageWriter writer;
293        // Just use first APP1 segment for now.
294        // Multiple APP1 segments are rare and poorly supported.
295        if (!jfifPieces.exifPieces.isEmpty()) {
296            final JFIFPieceSegment exifPiece = (JFIFPieceSegment) jfifPieces.exifPieces.get(0);
297
298            byte[] exifBytes = exifPiece.segmentData;
299            exifBytes = remainingBytes("trimmed exif bytes", exifBytes, 6);
300
301            writer = new TiffImageWriterLossless(outputSet.byteOrder, exifBytes);
302
303        } else {
304            writer = new TiffImageWriterLossy(outputSet.byteOrder);
305        }
306
307        final boolean includeEXIFPrefix = true;
308        final byte[] newBytes = writeExifSegment(writer, outputSet, includeEXIFPrefix);
309
310        writeSegmentsReplacingExif(os, pieces, newBytes);
311    }
312
313    /**
314     * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
315     *
316     * <p>
317     * Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF segment that it can't parse (such as Maker Notes), this
318     * algorithm avoids overwriting any part of the original segment that it couldn't parse. This can cause the EXIF segment to grow with each update, which is
319     * a serious issue, since all EXIF data must fit in a single APP1 segment of the JPEG image.
320     * </p>
321     *
322     * @param src       Image file.
323     * @param os        OutputStream to write the image to.
324     * @param outputSet TiffOutputSet containing the EXIF data to write.
325     * @throws ImagingException if it fails to read the JFIF segments
326     * @throws IOException      if it fails to read the image data
327     * @throws ImagingException if it fails to write the updated data
328     */
329    public void updateExifMetadataLossless(final File src, final OutputStream os, final TiffOutputSet outputSet)
330            throws ImagingException, IOException, ImagingException {
331        final ByteSource byteSource = ByteSource.file(src);
332        updateExifMetadataLossless(byteSource, os, outputSet);
333    }
334
335    /**
336     * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
337     *
338     * <p>
339     * Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF segment that it can't parse (such as Maker Notes), this
340     * algorithm avoids overwriting any part of the original segment that it couldn't parse. This can cause the EXIF segment to grow with each update, which is
341     * a serious issue, since all EXIF data must fit in a single APP1 segment of the JPEG image.
342     * </p>
343     *
344     * @param src       InputStream containing JPEG image data.
345     * @param os        OutputStream to write the image to.
346     * @param outputSet TiffOutputSet containing the EXIF data to write.
347     * @throws ImagingException if it fails to read the JFIF segments
348     * @throws IOException      if it fails to read the image data
349     * @throws ImagingException if it fails to write the updated data
350     */
351    public void updateExifMetadataLossless(final InputStream src, final OutputStream os, final TiffOutputSet outputSet)
352            throws ImagingException, IOException, ImagingException {
353        final ByteSource byteSource = ByteSource.inputStream(src, null);
354        updateExifMetadataLossless(byteSource, os, outputSet);
355    }
356
357    /**
358     * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
359     *
360     * <p>
361     * Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, ignoring the possibility that it may be discarding data it
362     * couldn't parse (such as Maker Notes).
363     * </p>
364     *
365     * @param src       Byte array containing JPEG image data.
366     * @param os        OutputStream to write the image to.
367     * @param outputSet TiffOutputSet containing the EXIF data to write.
368     * @throws ImagingException if it fails to read the JFIF segments
369     * @throws IOException      if it fails to read the image data
370     * @throws ImagingException if it fails to write the updated data
371     */
372    public void updateExifMetadataLossy(final byte[] src, final OutputStream os, final TiffOutputSet outputSet)
373            throws ImagingException, IOException, ImagingException {
374        final ByteSource byteSource = ByteSource.array(src);
375        updateExifMetadataLossy(byteSource, os, outputSet);
376    }
377
378    /**
379     * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
380     *
381     * <p>
382     * Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, ignoring the possibility that it may be discarding data it
383     * couldn't parse (such as Maker Notes).
384     * </p>
385     *
386     * @param byteSource ByteSource containing JPEG image data.
387     * @param os         OutputStream to write the image to.
388     * @param outputSet  TiffOutputSet containing the EXIF data to write.
389     * @throws ImagingException if it fails to read the JFIF segments
390     * @throws IOException      if it fails to read the image data
391     * @throws ImagingException if it fails to write the updated data
392     */
393    public void updateExifMetadataLossy(final ByteSource byteSource, final OutputStream os, final TiffOutputSet outputSet)
394            throws ImagingException, IOException, ImagingException {
395        final JFIFPieces jfifPieces = analyzeJfif(byteSource);
396        final List<JFIFPiece> pieces = jfifPieces.pieces;
397
398        final AbstractTiffImageWriter writer = new TiffImageWriterLossy(outputSet.byteOrder);
399
400        final boolean includeEXIFPrefix = true;
401        final byte[] newBytes = writeExifSegment(writer, outputSet, includeEXIFPrefix);
402
403        writeSegmentsReplacingExif(os, pieces, newBytes);
404    }
405
406    /**
407     * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
408     *
409     * <p>
410     * Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, ignoring the possibility that it may be discarding data it
411     * couldn't parse (such as Maker Notes).
412     * </p>
413     *
414     * @param src       Image file.
415     * @param os        OutputStream to write the image to.
416     * @param outputSet TiffOutputSet containing the EXIF data to write.
417     * @throws ImagingException if it fails to read the JFIF segments
418     * @throws IOException      if it fails to read the image data
419     * @throws ImagingException if it fails to write the updated data
420     */
421    public void updateExifMetadataLossy(final File src, final OutputStream os, final TiffOutputSet outputSet)
422            throws ImagingException, IOException, ImagingException {
423        final ByteSource byteSource = ByteSource.file(src);
424        updateExifMetadataLossy(byteSource, os, outputSet);
425    }
426
427    /**
428     * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
429     *
430     * <p>
431     * Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, ignoring the possibility that it may be discarding data it
432     * couldn't parse (such as Maker Notes).
433     * </p>
434     *
435     * @param src       InputStream containing JPEG image data.
436     * @param os        OutputStream to write the image to.
437     * @param outputSet TiffOutputSet containing the EXIF data to write.
438     * @throws ImagingException if it fails to read the JFIF segments
439     * @throws IOException      if it fails to read the image data
440     * @throws ImagingException if it fails to write the updated data
441     */
442    public void updateExifMetadataLossy(final InputStream src, final OutputStream os, final TiffOutputSet outputSet)
443            throws ImagingException, IOException, ImagingException {
444        final ByteSource byteSource = ByteSource.inputStream(src, null);
445        updateExifMetadataLossy(byteSource, os, outputSet);
446    }
447
448    private byte[] writeExifSegment(final AbstractTiffImageWriter writer, final TiffOutputSet outputSet, final boolean includeEXIFPrefix)
449            throws IOException, ImagingException {
450        final ByteArrayOutputStream os = new ByteArrayOutputStream();
451
452        if (includeEXIFPrefix) {
453            JpegConstants.EXIF_IDENTIFIER_CODE.writeTo(os);
454            os.write(0);
455            os.write(0);
456        }
457
458        writer.write(os, outputSet);
459
460        return os.toByteArray();
461    }
462
463    private void writeSegmentsReplacingExif(final OutputStream outputStream, final List<JFIFPiece> segments, final byte[] newBytes)
464            throws ImagingException, IOException {
465
466        try (DataOutputStream os = new DataOutputStream(outputStream)) {
467            JpegConstants.SOI.writeTo(os);
468
469            boolean hasExif = false;
470
471            for (final JFIFPiece piece : segments) {
472                if (piece instanceof JFIFPieceSegmentExif) {
473                    hasExif = true;
474                    break;
475                }
476            }
477
478            if (!hasExif && newBytes != null) {
479                final byte[] markerBytes = ByteConversions.toBytes((short) JpegConstants.JPEG_APP1_MARKER, getByteOrder());
480                if (newBytes.length > 0xffff) {
481                    throw new ImagingOverflowException("APP1 Segment is too long: " + newBytes.length);
482                }
483                final int markerLength = newBytes.length + 2;
484                final byte[] markerLengthBytes = ByteConversions.toBytes((short) markerLength, getByteOrder());
485
486                int index = 0;
487                final JFIFPieceSegment firstSegment = (JFIFPieceSegment) segments.get(index);
488                if (firstSegment.marker == JpegConstants.JFIF_MARKER) {
489                    index = 1;
490                }
491                segments.add(index, new JFIFPieceSegmentExif(JpegConstants.JPEG_APP1_MARKER, markerBytes, markerLengthBytes, newBytes));
492            }
493
494            boolean APP1Written = false;
495
496            for (final JFIFPiece piece : segments) {
497                if (piece instanceof JFIFPieceSegmentExif) {
498                    // only replace first APP1 segment; skips others.
499                    if (APP1Written) {
500                        continue;
501                    }
502                    APP1Written = true;
503
504                    if (newBytes == null) {
505                        continue;
506                    }
507
508                    final byte[] markerBytes = ByteConversions.toBytes((short) JpegConstants.JPEG_APP1_MARKER, getByteOrder());
509                    if (newBytes.length > 0xffff) {
510                        throw new ImagingOverflowException("APP1 Segment is too long: " + newBytes.length);
511                    }
512                    final int markerLength = newBytes.length + 2;
513                    final byte[] markerLengthBytes = ByteConversions.toBytes((short) markerLength, getByteOrder());
514
515                    os.write(markerBytes);
516                    os.write(markerLengthBytes);
517                    os.write(newBytes);
518                } else {
519                    piece.write(os);
520                }
521            }
522        }
523    }
524
525}