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