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