    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.imaging.formats.jpeg.exif;
    018
    019import static org.apache.commons.imaging.common.BinaryFunctions.remainingBytes;
    020import static org.apache.commons.imaging.common.BinaryFunctions.startsWith;
    021
    022import java.io.ByteArrayOutputStream;
    023import java.io.DataOutputStream;
    024import java.io.File;
    025import java.io.IOException;
    026import java.io.InputStream;
    027import java.io.OutputStream;
    028import java.nio.ByteOrder;
    029import java.util.ArrayList;
    030import java.util.List;
    031
    032import org.apache.commons.imaging.ImageReadException;
    033import org.apache.commons.imaging.ImageWriteException;
    034import org.apache.commons.imaging.common.BinaryFileParser;
    035import org.apache.commons.imaging.common.ByteConversions;
    036import org.apache.commons.imaging.common.bytesource.ByteSource;
    037import org.apache.commons.imaging.common.bytesource.ByteSourceArray;
    038import org.apache.commons.imaging.common.bytesource.ByteSourceFile;
    039import org.apache.commons.imaging.common.bytesource.ByteSourceInputStream;
    040import org.apache.commons.imaging.formats.jpeg.JpegConstants;
    041import org.apache.commons.imaging.formats.jpeg.JpegUtils;
    042import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterBase;
    043import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossless;
    044import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy;
    045import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
    046
    047/**
    048 * Interface for Exif write/update/remove functionality for Jpeg/JFIF images.
    049 *
    050 * <p>See the source of the ExifMetadataUpdateExample class for example usage.</p>
    051 *
    052 * @see <a
    053 *      href="https://svn.apache.org/repos/asf/commons/proper/imaging/trunk/src/test/java/org/apache/commons/imaging/examples/WriteExifMetadataExample.java">org.apache.commons.imaging.examples.WriteExifMetadataExample</a>
    054 */
    055public class ExifRewriter extends BinaryFileParser {
    056    /**
    057     * Constructor. to guess whether a file contains an image based on its file
    058     * extension.
    059     */
    060    public ExifRewriter() {
    061        this(ByteOrder.BIG_ENDIAN);
    062    }
    063
    064    /**
    065     * Constructor.
    066     * <p>
    067     *
    068     * @param byteOrder
    069     *            byte order of EXIF segment.
    070     */
    071    public ExifRewriter(final ByteOrder byteOrder) {
    072        setByteOrder(byteOrder);
    073    }
    074
    075    private static class JFIFPieces {
    076        public final List<JFIFPiece> pieces;
    077        public final List<JFIFPiece> exifPieces;
    078
    079        JFIFPieces(final List<JFIFPiece> pieces,
    080                final List<JFIFPiece> exifPieces) {
    081            this.pieces = pieces;
    082            this.exifPieces = exifPieces;
    083        }
    084
    085    }
    086
    087    private abstract static class JFIFPiece {
    088        protected abstract void write(OutputStream os) throws IOException;
    089    }
    090
    091    private static class JFIFPieceSegment extends JFIFPiece {
    092        public final int marker;
    093        public final byte[] markerBytes;
    094        public final byte[] markerLengthBytes;
    095        public final byte[] segmentData;
    096
    097        JFIFPieceSegment(final int marker, final byte[] markerBytes,
    098                final byte[] markerLengthBytes, final byte[] segmentData) {
    099            this.marker = marker;
    100            this.markerBytes = markerBytes;
    101            this.markerLengthBytes = markerLengthBytes;
    102            this.segmentData = segmentData;
    103        }
    104
    105        @Override
    106        protected void write(final OutputStream os) throws IOException {
    107            os.write(markerBytes);
    108            os.write(markerLengthBytes);
    109            os.write(segmentData);
    110        }
    111    }
    112
    113    private static class JFIFPieceSegmentExif extends JFIFPieceSegment {
    114
    115        JFIFPieceSegmentExif(final int marker, final byte[] markerBytes,
    116                final byte[] markerLengthBytes, final byte[] segmentData) {
    117            super(marker, markerBytes, markerLengthBytes, segmentData);
    118        }
    119    }
    120
    121    private static class JFIFPieceImageData extends JFIFPiece {
    122        public final byte[] markerBytes;
    123        public final byte[] imageData;
    124
    125        JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) {
    126            super();
    127            this.markerBytes = markerBytes;
    128            this.imageData = imageData;
    129        }
    130
    131        @Override
    132        protected void write(final OutputStream os) throws IOException {
    133            os.write(markerBytes);
    134            os.write(imageData);
    135        }
    136    }
    137
    138    private JFIFPieces analyzeJFIF(final ByteSource byteSource) throws ImageReadException, IOException {
    139        final List<JFIFPiece> pieces = new ArrayList<>();
    140        final List<JFIFPiece> exifPieces = new ArrayList<>();
    141
    142        final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
    143            // return false to exit before reading image data.
    144            @Override
    145            public boolean beginSOS() {
    146                return true;
    147            }
    148
    149            @Override
    150            public void visitSOS(final int marker, final byte[] markerBytes, final byte[] imageData) {
    151                pieces.add(new JFIFPieceImageData(markerBytes, imageData));
    152            }
    153
    154            // return false to exit traversal.
    155            @Override
    156            public boolean visitSegment(final int marker, final byte[] markerBytes,
    157                    final int markerLength, final byte[] markerLengthBytes,
    158                    final byte[] segmentData) throws
    159            // ImageWriteException,
    160                    ImageReadException, IOException {
    161                if (marker != JpegConstants.JPEG_APP1_MARKER) {
    162                    pieces.add(new JFIFPieceSegment(marker, markerBytes,
    163                            markerLengthBytes, segmentData));
    164                } else if (!startsWith(segmentData,
    165                        JpegConstants.EXIF_IDENTIFIER_CODE)) {
    166                    pieces.add(new JFIFPieceSegment(marker, markerBytes,
    167                            markerLengthBytes, segmentData));
    168                // } else if (exifSegmentArray[0] != null) {
    169                // // TODO: add support for multiple segments
    170                // throw new ImageReadException(
    171                // "More than one APP1 EXIF segment.");
    172                } else {
    173                    final JFIFPiece piece = new JFIFPieceSegmentExif(marker,
    174                            markerBytes, markerLengthBytes, segmentData);
    175                    pieces.add(piece);
    176                    exifPieces.add(piece);
    177                }
    178                return true;
    179            }
    180        };
    181
    182        new JpegUtils().traverseJFIF(byteSource, visitor);
    183
    184        // GenericSegment exifSegment = exifSegmentArray[0];
    185        // if (exifSegments.size() < 1)
    186        // {
    187        // // TODO: add support for adding, not just replacing.
    188        // throw new ImageReadException("No APP1 EXIF segment found.");
    189        // }
    190
    191        return new JFIFPieces(pieces, exifPieces);
    192    }
    193
    194    /**
    195     * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1
    196     * segment), and writes the result to a stream.
    197     * <p>
    198     *
    199     * @param src
    200     *            Image file.
    201     * @param os
    202     *            OutputStream to write the image to.
    203     *
    204     * @throws ImageReadException if it fails to read the JFIF segments
    205     * @throws IOException if it fails to read the image data
    206     * @throws ImageWriteException if it fails to write the updated data
    207     * @see java.io.File
    208     * @see java.io.OutputStream
    209     * @see java.io.File
    210     * @see java.io.OutputStream
    211     */
    212    public void removeExifMetadata(final File src, final OutputStream os)
    213            throws ImageReadException, IOException, ImageWriteException {
    214        final ByteSource byteSource = new ByteSourceFile(src);
    215        removeExifMetadata(byteSource, os);
    216    }
    217
    218    /**
    219     * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1
    220     * segment), and writes the result to a stream.
    221     *
    222     * @param src
    223     *            Byte array containing Jpeg image data.
    224     * @param os
    225     *            OutputStream to write the image to.
    226     * @throws ImageReadException if it fails to read the JFIF segments
    227     * @throws IOException if it fails to read the image data
    228     * @throws ImageWriteException if it fails to write the updated data
    229     */
    230    public void removeExifMetadata(final byte[] src, final OutputStream os)
    231            throws ImageReadException, IOException, ImageWriteException {
    232        final ByteSource byteSource = new ByteSourceArray(src);
    233        removeExifMetadata(byteSource, os);
    234    }
    235
    236    /**
    237     * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1
    238     * segment), and writes the result to a stream.
    239     *
    240     * @param src
    241     *            InputStream containing Jpeg image data.
    242     * @param os
    243     *            OutputStream to write the image to.
    244     * @throws ImageReadException if it fails to read the JFIF segments
    245     * @throws IOException if it fails to read the image data
    246     * @throws ImageWriteException if it fails to write the updated data
    247     */
    248    public void removeExifMetadata(final InputStream src, final OutputStream os)
    249            throws ImageReadException, IOException, ImageWriteException {
    250        final ByteSource byteSource = new ByteSourceInputStream(src, null);
    251        removeExifMetadata(byteSource, os);
    252    }
    253
    254    /**
    255     * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1
    256     * segment), and writes the result to a stream.
    257     *
    258     * @param byteSource
    259     *            ByteSource containing Jpeg image data.
    260     * @param os
    261     *            OutputStream to write the image to.
    262     * @throws ImageReadException if it fails to read the JFIF segments
    263     * @throws IOException if it fails to read the image data
    264     * @throws ImageWriteException if it fails to write the updated data
    265     */
    266    public void removeExifMetadata(final ByteSource byteSource, final OutputStream os)
    267            throws ImageReadException, IOException, ImageWriteException {
    268        final JFIFPieces jfifPieces = analyzeJFIF(byteSource);
    269        final List<JFIFPiece> pieces = jfifPieces.pieces;
    270
    271        // Debug.debug("pieces", pieces);
    272
    273        // pieces.removeAll(jfifPieces.exifSegments);
    274
    275        // Debug.debug("pieces", pieces);
    276
    277        writeSegmentsReplacingExif(os, pieces, null);
    278    }
    279
    280    /**
    281     * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a
    282     * stream.
    283     *
    284     * <p>Note that this uses the "Lossless" approach - in order to preserve data
    285     * embedded in the EXIF segment that it can't parse (such as Maker Notes),
    286     * this algorithm avoids overwriting any part of the original segment that
    287     * it couldn't parse. This can cause the EXIF segment to grow with each
    288     * update, which is a serious issue, since all EXIF data must fit in a
    289     * single APP1 segment of the Jpeg image.</p>
    290     *
    291     * @param src
    292     *            Image file.
    293     * @param os
    294     *            OutputStream to write the image to.
    295     * @param outputSet
    296     *            TiffOutputSet containing the EXIF data to write.
    297     * @throws ImageReadException if it fails to read the JFIF segments
    298     * @throws IOException if it fails to read the image data
    299     * @throws ImageWriteException if it fails to write the updated data
    300     */
    301    public void updateExifMetadataLossless(final File src, final OutputStream os,
    302            final TiffOutputSet outputSet) throws ImageReadException, IOException,
    303            ImageWriteException {
    304        final ByteSource byteSource = new ByteSourceFile(src);
    305        updateExifMetadataLossless(byteSource, os, outputSet);
    306    }
    307
    308    /**
    309     * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a
    310     * stream.
    311     *
    312     * <p>Note that this uses the "Lossless" approach - in order to preserve data
    313     * embedded in the EXIF segment that it can't parse (such as Maker Notes),
    314     * this algorithm avoids overwriting any part of the original segment that
    315     * it couldn't parse. This can cause the EXIF segment to grow with each
    316     * update, which is a serious issue, since all EXIF data must fit in a
    317     * single APP1 segment of the Jpeg image.</p>
    318     *
    319     * @param src
    320     *            Byte array containing Jpeg image data.
    321     * @param os
    322     *            OutputStream to write the image to.
    323     * @param outputSet
    324     *            TiffOutputSet containing the EXIF data to write.
    325     * @throws ImageReadException if it fails to read the JFIF segments
    326     * @throws IOException if it fails to read the image data
    327     * @throws ImageWriteException if it fails to write the updated data
    328     */
    329    public void updateExifMetadataLossless(final byte[] src, final OutputStream os,
    330            final TiffOutputSet outputSet) throws ImageReadException, IOException,
    331            ImageWriteException {
    332        final ByteSource byteSource = new ByteSourceArray(src);
    333        updateExifMetadataLossless(byteSource, os, outputSet);
    334    }
    335
    336    /**
    337     * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a
    338     * stream.
    339     *
    340     * <p>Note that this uses the "Lossless" approach - in order to preserve data
    341     * embedded in the EXIF segment that it can't parse (such as Maker Notes),
    342     * this algorithm avoids overwriting any part of the original segment that
    343     * it couldn't parse. This can cause the EXIF segment to grow with each
    344     * update, which is a serious issue, since all EXIF data must fit in a
    345     * single APP1 segment of the Jpeg image.</p>
    346     *
    347     * @param src
    348     *            InputStream containing Jpeg image data.
    349     * @param os
    350     *            OutputStream to write the image to.
    351     * @param outputSet
    352     *            TiffOutputSet containing the EXIF data to write.
    353     * @throws ImageReadException if it fails to read the JFIF segments
    354     * @throws IOException if it fails to read the image data
    355     * @throws ImageWriteException if it fails to write the updated data
    356     */
    357    public void updateExifMetadataLossless(final InputStream src, final OutputStream os,
    358            final TiffOutputSet outputSet) throws ImageReadException, IOException,
    359            ImageWriteException {
    360        final ByteSource byteSource = new ByteSourceInputStream(src, null);
    361        updateExifMetadataLossless(byteSource, os, outputSet);
    362    }
    363
    364    /**
    365     * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a
    366     * stream.
    367     *
    368     * <p>Note that this uses the "Lossless" approach - in order to preserve data
    369     * embedded in the EXIF segment that it can't parse (such as Maker Notes),
    370     * this algorithm avoids overwriting any part of the original segment that
    371     * it couldn't parse. This can cause the EXIF segment to grow with each
    372     * update, which is a serious issue, since all EXIF data must fit in a
    373     * single APP1 segment of the Jpeg image.</p>
    374     *
    375     * @param byteSource
    376     *            ByteSource containing Jpeg image data.
    377     * @param os
    378     *            OutputStream to write the image to.
    379     * @param outputSet
    380     *            TiffOutputSet containing the EXIF data to write.
    381     * @throws ImageReadException if it fails to read the JFIF segments
    382     * @throws IOException if it fails to read the image data
    383     * @throws ImageWriteException if it fails to write the updated data
    384     */
    385    public void updateExifMetadataLossless(final ByteSource byteSource,
    386            final OutputStream os, final TiffOutputSet outputSet)
    387            throws ImageReadException, IOException, ImageWriteException {
    388        // List outputDirectories = outputSet.getDirectories();
    389        final JFIFPieces jfifPieces = analyzeJFIF(byteSource);
    390        final List<JFIFPiece> pieces = jfifPieces.pieces;
    391
    392        TiffImageWriterBase writer;
    393        // Just use first APP1 segment for now.
    394        // Multiple APP1 segments are rare and poorly supported.
    395        if (!jfifPieces.exifPieces.isEmpty()) {
    396            JFIFPieceSegment exifPiece = null;
    397            exifPiece = (JFIFPieceSegment) jfifPieces.exifPieces.get(0);
    398
    399            byte[] exifBytes = exifPiece.segmentData;
    400            exifBytes = remainingBytes("trimmed exif bytes", exifBytes, 6);
    401
    402            writer = new TiffImageWriterLossless(outputSet.byteOrder, exifBytes);
    403
    404        } else {
    405            writer = new TiffImageWriterLossy(outputSet.byteOrder);
    406        }
    407
    408        final boolean includeEXIFPrefix = true;
    409        final byte[] newBytes = writeExifSegment(writer, outputSet, includeEXIFPrefix);
    410
    411        writeSegmentsReplacingExif(os, pieces, newBytes);
    412    }
    413
    414    /**
    415     * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a
    416     * stream.
    417     *
    418     * <p>Note that this uses the "Lossy" approach - the algorithm overwrites the
    419     * entire EXIF segment, ignoring the possibility that it may be discarding
    420     * data it couldn't parse (such as Maker Notes).</p>
    421     *
    422     * @param src
    423     *            Byte array containing Jpeg image data.
    424     * @param os
    425     *            OutputStream to write the image to.
    426     * @param outputSet
    427     *            TiffOutputSet containing the EXIF data to write.
    428     * @throws ImageReadException if it fails to read the JFIF segments
    429     * @throws IOException if it fails to read the image data
    430     * @throws ImageWriteException if it fails to write the updated data
    431     */
    432    public void updateExifMetadataLossy(final byte[] src, final OutputStream os,
    433            final TiffOutputSet outputSet) throws ImageReadException, IOException,
    434            ImageWriteException {
    435        final ByteSource byteSource = new ByteSourceArray(src);
    436        updateExifMetadataLossy(byteSource, os, outputSet);
    437    }
    438
    439    /**
    440     * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a
    441     * stream.
    442     *
    443     * <p>Note that this uses the "Lossy" approach - the algorithm overwrites the
    444     * entire EXIF segment, ignoring the possibility that it may be discarding
    445     * data it couldn't parse (such as Maker Notes).</p>
    446     *
    447     * @param src
    448     *            InputStream containing Jpeg image data.
    449     * @param os
    450     *            OutputStream to write the image to.
    451     * @param outputSet
    452     *            TiffOutputSet containing the EXIF data to write.
    453     * @throws ImageReadException if it fails to read the JFIF segments
    454     * @throws IOException if it fails to read the image data
    455     * @throws ImageWriteException if it fails to write the updated data
    456     */
    457    public void updateExifMetadataLossy(final InputStream src, final OutputStream os,
    458            final TiffOutputSet outputSet) throws ImageReadException, IOException,
    459            ImageWriteException {
    460        final ByteSource byteSource = new ByteSourceInputStream(src, null);
    461        updateExifMetadataLossy(byteSource, os, outputSet);
    462    }
    463
    464    /**
    465     * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a
    466     * stream.
    467     *
    468     * <p>Note that this uses the "Lossy" approach - the algorithm overwrites the
    469     * entire EXIF segment, ignoring the possibility that it may be discarding
    470     * data it couldn't parse (such as Maker Notes).</p>
    471     *
    472     * @param src
    473     *            Image file.
    474     * @param os
    475     *            OutputStream to write the image to.
    476     * @param outputSet
    477     *            TiffOutputSet containing the EXIF data to write.
    478     * @throws ImageReadException if it fails to read the JFIF segments
    479     * @throws IOException if it fails to read the image data
    480     * @throws ImageWriteException if it fails to write the updated data
    481     */
    482    public void updateExifMetadataLossy(final File src, final OutputStream os,
    483            final TiffOutputSet outputSet) throws ImageReadException, IOException,
    484            ImageWriteException {
    485        final ByteSource byteSource = new ByteSourceFile(src);
    486        updateExifMetadataLossy(byteSource, os, outputSet);
    487    }
    488
    489    /**
    490     * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a
    491     * stream.
    492     *
    493     * <p>Note that this uses the "Lossy" approach - the algorithm overwrites the
    494     * entire EXIF segment, ignoring the possibility that it may be discarding
    495     * data it couldn't parse (such as Maker Notes).</p>
    496     *
    497     * @param byteSource
    498     *            ByteSource containing Jpeg image data.
    499     * @param os
    500     *            OutputStream to write the image to.
    501     * @param outputSet
    502     *            TiffOutputSet containing the EXIF data to write.
    503     * @throws ImageReadException if it fails to read the JFIF segments
    504     * @throws IOException if it fails to read the image data
    505     * @throws ImageWriteException if it fails to write the updated data
    506     */
    507    public void updateExifMetadataLossy(final ByteSource byteSource, final OutputStream os,
    508            final TiffOutputSet outputSet) throws ImageReadException, IOException,
    509            ImageWriteException {
    510        final JFIFPieces jfifPieces = analyzeJFIF(byteSource);
    511        final List<JFIFPiece> pieces = jfifPieces.pieces;
    512
    513        final TiffImageWriterBase writer = new TiffImageWriterLossy(
    514                outputSet.byteOrder);
    515
    516        final boolean includeEXIFPrefix = true;
    517        final byte[] newBytes = writeExifSegment(writer, outputSet, includeEXIFPrefix);
    518
    519        writeSegmentsReplacingExif(os, pieces, newBytes);
    520    }
    521
    522    private void writeSegmentsReplacingExif(final OutputStream outputStream,
    523            final List<JFIFPiece> segments, final byte[] newBytes)
    524            throws ImageWriteException, IOException {
    525
    526        try (DataOutputStream os = new DataOutputStream(outputStream)) {
    527            JpegConstants.SOI.writeTo(os);
    528
    529            boolean hasExif = false;
    530
    531            for (final JFIFPiece piece : segments) {
    532                if (piece instanceof JFIFPieceSegmentExif) {
    533                    hasExif = true;
    534                    break;
    535                }
    536            }
    537
    538            if (!hasExif && newBytes != null) {
    539                final byte[] markerBytes = ByteConversions.toBytes((short) JpegConstants.JPEG_APP1_MARKER, getByteOrder());
    540                if (newBytes.length > 0xffff) {
    541                    throw new ExifOverflowException(
    542                            "APP1 Segment is too long: " + newBytes.length);
    543                }
    544                final int markerLength = newBytes.length + 2;
    545                final byte[] markerLengthBytes = ByteConversions.toBytes((short) markerLength, getByteOrder());
    546
    547                int index = 0;
    548                final JFIFPieceSegment firstSegment = (JFIFPieceSegment) segments.get(index);
    549                if (firstSegment.marker == JpegConstants.JFIF_MARKER) {
    550                    index = 1;
    551                }
    552                segments.add(index, new JFIFPieceSegmentExif(JpegConstants.JPEG_APP1_MARKER,
    553                        markerBytes, markerLengthBytes, newBytes));
    554            }
    555
    556            boolean APP1Written = false;
    557
    558            for (final JFIFPiece piece : segments) {
    559                if (piece instanceof JFIFPieceSegmentExif) {
    560                    // only replace first APP1 segment; skips others.
    561                    if (APP1Written) {
    562                        continue;
    563                    }
    564                    APP1Written = true;
    565
    566                    if (newBytes == null) {
    567                        continue;
    568                    }
    569
    570                    final byte[] markerBytes = ByteConversions.toBytes((short) JpegConstants.JPEG_APP1_MARKER, getByteOrder());
    571                    if (newBytes.length > 0xffff) {
    572                        throw new ExifOverflowException(
    573                                "APP1 Segment is too long: " + newBytes.length);
    574                    }
    575                    final int markerLength = newBytes.length + 2;
    576                    final byte[] markerLengthBytes = ByteConversions.toBytes((short) markerLength, getByteOrder());
    577
    578                    os.write(markerBytes);
    579                    os.write(markerLengthBytes);
    580                    os.write(newBytes);
    581                } else {
    582                    piece.write(os);
    583                }
    584            }
    585        }
    586    }
    587
    588    public static class ExifOverflowException extends ImageWriteException {
    589        private static final long serialVersionUID = 1401484357224931218L;
    590
    591        public ExifOverflowException(final String message) {
    592            super(message);
    593        }
    594    }
    595
    596    private byte[] writeExifSegment(final TiffImageWriterBase writer,
    597            final TiffOutputSet outputSet, final boolean includeEXIFPrefix)
    598            throws IOException, ImageWriteException {
    599        final ByteArrayOutputStream os = new ByteArrayOutputStream();
    600
    601        if (includeEXIFPrefix) {
    602            JpegConstants.EXIF_IDENTIFIER_CODE.writeTo(os);
    603            os.write(0);
    604            os.write(0);
    605        }
    606
    607        writer.write(os, outputSet);
    608
    609        return os.toByteArray();
    610    }
    611
    612}