    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}