    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.xmp;
    018
    019import static org.apache.commons.imaging.common.BinaryFunctions.startsWith;
    020
    021import java.io.DataOutputStream;
    022import java.io.IOException;
    023import java.io.OutputStream;
    024import java.nio.ByteOrder;
    025import java.util.ArrayList;
    026import java.util.List;
    027
    028import org.apache.commons.imaging.ImageReadException;
    029import org.apache.commons.imaging.ImageWriteException;
    030import org.apache.commons.imaging.common.BinaryFileParser;
    031import org.apache.commons.imaging.common.ByteConversions;
    032import org.apache.commons.imaging.common.bytesource.ByteSource;
    033import org.apache.commons.imaging.formats.jpeg.JpegConstants;
    034import org.apache.commons.imaging.formats.jpeg.JpegUtils;
    035import org.apache.commons.imaging.formats.jpeg.iptc.IptcParser;
    036
    037/**
    038 * Interface for Exif write/update/remove functionality for Jpeg/JFIF images.
    039 */
    040public class JpegRewriter extends BinaryFileParser {
    041    private static final ByteOrder JPEG_BYTE_ORDER = ByteOrder.BIG_ENDIAN;
    042    private static final SegmentFilter EXIF_SEGMENT_FILTER = JFIFPieceSegment::isExifSegment;
    043    private static final SegmentFilter XMP_SEGMENT_FILTER = JFIFPieceSegment::isXmpSegment;
    044    private static final SegmentFilter PHOTOSHOP_APP13_SEGMENT_FILTER = JFIFPieceSegment::isPhotoshopApp13Segment;
    045
    046    /**
    047     * Constructor. to guess whether a file contains an image based on its file
    048     * extension.
    049     */
    050    public JpegRewriter() {
    051        setByteOrder(JPEG_BYTE_ORDER);
    052    }
    053
    054    protected static class JFIFPieces {
    055        public final List<JFIFPiece> pieces;
    056        public final List<JFIFPiece> segmentPieces;
    057
    058        public JFIFPieces(final List<JFIFPiece> pieces,
    059                final List<JFIFPiece> segmentPieces) {
    060            this.pieces = pieces;
    061            this.segmentPieces = segmentPieces;
    062        }
    063
    064    }
    065
    066    protected abstract static class JFIFPiece {
    067        protected abstract void write(OutputStream os) throws IOException;
    068
    069        @Override
    070        public String toString() {
    071            return "[" + this.getClass().getName() + "]";
    072        }
    073    }
    074
    075    protected static class JFIFPieceSegment extends JFIFPiece {
    076        public final int marker;
    077        private final byte[] markerBytes;
    078        private final byte[] segmentLengthBytes;
    079        private final byte[] segmentData;
    080
    081        public JFIFPieceSegment(final int marker, final byte[] segmentData) {
    082            this(marker,
    083                    ByteConversions.toBytes((short) marker, JPEG_BYTE_ORDER),
    084                    ByteConversions.toBytes((short) (segmentData.length + 2), JPEG_BYTE_ORDER),
    085                    segmentData);
    086        }
    087
    088        JFIFPieceSegment(final int marker, final byte[] markerBytes,
    089                final byte[] segmentLengthBytes, final byte[] segmentData) {
    090            this.marker = marker;
    091            this.markerBytes = markerBytes;
    092            this.segmentLengthBytes = segmentLengthBytes;
    093            this.segmentData = segmentData.clone();
    094        }
    095
    096        @Override
    097        public String toString() {
    098            return "[" + this.getClass().getName() + " (0x"
    099                    + Integer.toHexString(marker) + ")]";
    100        }
    101
    102        @Override
    103        protected void write(final OutputStream os) throws IOException {
    104            os.write(markerBytes);
    105            os.write(segmentLengthBytes);
    106            os.write(segmentData);
    107        }
    108
    109        public boolean isApp1Segment() {
    110            return marker == JpegConstants.JPEG_APP1_MARKER;
    111        }
    112
    113        public boolean isAppSegment() {
    114            return marker >= JpegConstants.JPEG_APP0_MARKER && marker <= JpegConstants.JPEG_APP15_MARKER;
    115        }
    116
    117        public boolean isExifSegment() {
    118            if (marker != JpegConstants.JPEG_APP1_MARKER) {
    119                return false;
    120            }
    121            if (!startsWith(segmentData, JpegConstants.EXIF_IDENTIFIER_CODE)) {
    122                return false;
    123            }
    124            return true;
    125        }
    126
    127        public boolean isPhotoshopApp13Segment() {
    128            if (marker != JpegConstants.JPEG_APP13_MARKER) {
    129                return false;
    130            }
    131            if (!new IptcParser().isPhotoshopJpegSegment(segmentData)) {
    132                return false;
    133            }
    134            return true;
    135        }
    136
    137        public boolean isXmpSegment() {
    138            if (marker != JpegConstants.JPEG_APP1_MARKER) {
    139                return false;
    140            }
    141            if (!startsWith(segmentData, JpegConstants.XMP_IDENTIFIER)) {
    142                return false;
    143            }
    144            return true;
    145        }
    146
    147        public byte[] getSegmentData() {
    148            return segmentData.clone();
    149        }
    150
    151    }
    152
    153    static class JFIFPieceImageData extends JFIFPiece {
    154        private final byte[] markerBytes;
    155        private final byte[] imageData;
    156
    157        JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) {
    158            this.markerBytes = markerBytes;
    159            this.imageData = imageData;
    160        }
    161
    162        @Override
    163        protected void write(final OutputStream os) throws IOException {
    164            os.write(markerBytes);
    165            os.write(imageData);
    166        }
    167    }
    168
    169    protected JFIFPieces analyzeJFIF(final ByteSource byteSource) throws ImageReadException, IOException {
    170        final List<JFIFPiece> pieces = new ArrayList<>();
    171        final List<JFIFPiece> segmentPieces = new ArrayList<>();
    172
    173        final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
    174            // return false to exit before reading image data.
    175            @Override
    176            public boolean beginSOS() {
    177                return true;
    178            }
    179
    180            @Override
    181            public void visitSOS(final int marker, final byte[] markerBytes, final byte[] imageData) {
    182                pieces.add(new JFIFPieceImageData(markerBytes, imageData));
    183            }
    184
    185            // return false to exit traversal.
    186            @Override
    187            public boolean visitSegment(final int marker, final byte[] markerBytes,
    188                    final int segmentLength, final byte[] segmentLengthBytes,
    189                    final byte[] segmentData) throws ImageReadException, IOException {
    190                final JFIFPiece piece = new JFIFPieceSegment(marker, markerBytes,
    191                        segmentLengthBytes, segmentData);
    192                pieces.add(piece);
    193                segmentPieces.add(piece);
    194
    195                return true;
    196            }
    197        };
    198
    199        new JpegUtils().traverseJFIF(byteSource, visitor);
    200
    201        return new JFIFPieces(pieces, segmentPieces);
    202    }
    203
    204    private interface SegmentFilter {
    205        boolean filter(JFIFPieceSegment segment);
    206    }
    207
    208    protected <T extends JFIFPiece> List<T> removeXmpSegments(final List<T> segments) {
    209        return filterSegments(segments, XMP_SEGMENT_FILTER);
    210    }
    211
    212    protected <T extends JFIFPiece> List<T> removePhotoshopApp13Segments(
    213            final List<T> segments) {
    214        return filterSegments(segments, PHOTOSHOP_APP13_SEGMENT_FILTER);
    215    }
    216
    217    protected <T extends JFIFPiece> List<T> findPhotoshopApp13Segments(
    218            final List<T> segments) {
    219        return filterSegments(segments, PHOTOSHOP_APP13_SEGMENT_FILTER, true);
    220    }
    221
    222    protected <T extends JFIFPiece> List<T> removeExifSegments(final List<T> segments) {
    223        return filterSegments(segments, EXIF_SEGMENT_FILTER);
    224    }
    225
    226    protected <T extends JFIFPiece> List<T> filterSegments(final List<T> segments,
    227            final SegmentFilter filter) {
    228        return filterSegments(segments, filter, false);
    229    }
    230
    231    protected <T extends JFIFPiece> List<T> filterSegments(final List<T> segments,
    232            final SegmentFilter filter, final boolean reverse) {
    233        final List<T> result = new ArrayList<>();
    234
    235        for (final T piece : segments) {
    236            if (piece instanceof JFIFPieceSegment) {
    237                if (filter.filter((JFIFPieceSegment) piece) == reverse) {
    238                    result.add(piece);
    239                }
    240            } else if (!reverse) {
    241                result.add(piece);
    242            }
    243        }
    244
    245        return result;
    246    }
    247
    248    protected <T extends JFIFPiece, U extends JFIFPiece> List<JFIFPiece> insertBeforeFirstAppSegments(
    249            final List<T> segments, final List<U> newSegments) throws ImageWriteException {
    250        int firstAppIndex = -1;
    251        for (int i = 0; i < segments.size(); i++) {
    252            final JFIFPiece piece = segments.get(i);
    253            if (!(piece instanceof JFIFPieceSegment)) {
    254                continue;
    255            }
    256
    257            final JFIFPieceSegment segment = (JFIFPieceSegment) piece;
    258            if (segment.isAppSegment()) {
    259                if (firstAppIndex == -1) {
    260                    firstAppIndex = i;
    261                }
    262            }
    263        }
    264
    265        final List<JFIFPiece> result = new ArrayList<>(segments);
    266        if (firstAppIndex == -1) {
    267            throw new ImageWriteException("JPEG file has no APP segments.");
    268        }
    269        result.addAll(firstAppIndex, newSegments);
    270        return result;
    271    }
    272
    273    protected <T extends JFIFPiece, U extends JFIFPiece> List<JFIFPiece> insertAfterLastAppSegments(
    274            final List<T> segments, final List<U> newSegments) throws ImageWriteException {
    275        int lastAppIndex = -1;
    276        for (int i = 0; i < segments.size(); i++) {
    277            final JFIFPiece piece = segments.get(i);
    278            if (!(piece instanceof JFIFPieceSegment)) {
    279                continue;
    280            }
    281
    282            final JFIFPieceSegment segment = (JFIFPieceSegment) piece;
    283            if (segment.isAppSegment()) {
    284                lastAppIndex = i;
    285            }
    286        }
    287
    288        final List<JFIFPiece> result = new ArrayList<>(segments);
    289        if (lastAppIndex == -1) {
    290            if (segments.isEmpty()) {
    291                throw new ImageWriteException("JPEG file has no APP segments.");
    292            }
    293            result.addAll(1, newSegments);
    294        } else {
    295            result.addAll(lastAppIndex + 1, newSegments);
    296        }
    297
    298        return result;
    299    }
    300
    301    protected void writeSegments(final OutputStream outputStream,
    302            final List<? extends JFIFPiece> segments) throws IOException {
    303        try (DataOutputStream os = new DataOutputStream(outputStream)) {
    304            JpegConstants.SOI.writeTo(os);
    305
    306            for (final JFIFPiece piece : segments) {
    307                piece.write(os);
    308            }
    309        }
    310    }
    311
    312    // private void writeSegment(OutputStream os, JFIFPieceSegment piece)
    313    // throws ImageWriteException, IOException
    314    // {
    315    // byte markerBytes[] = convertShortToByteArray(JPEG_APP1_MARKER,
    316    // JPEG_BYTE_ORDER);
    317    // if (piece.segmentData.length > 0xffff)
    318    // throw new JpegSegmentOverflowException("Jpeg segment is too long: "
    319    // + piece.segmentData.length);
    320    // int segmentLength = piece.segmentData.length + 2;
    321    // byte segmentLengthBytes[] = convertShortToByteArray(segmentLength,
    322    // JPEG_BYTE_ORDER);
    323    //
    324    // os.write(markerBytes);
    325    // os.write(segmentLengthBytes);
    326    // os.write(piece.segmentData);
    327    // }
    328
    329    public static class JpegSegmentOverflowException extends ImageWriteException {
    330        private static final long serialVersionUID = -1062145751550646846L;
    331
    332        public JpegSegmentOverflowException(final String message) {
    333            super(message);
    334        }
    335    }
    336
    337}