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}