View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.jpeg.xmp;
18  
19  import static org.apache.commons.imaging.common.BinaryFunctions.startsWith;
20  
21  import java.io.DataOutputStream;
22  import java.io.IOException;
23  import java.io.OutputStream;
24  import java.nio.ByteOrder;
25  import java.util.ArrayList;
26  import java.util.List;
27  
28  import org.apache.commons.imaging.ImageReadException;
29  import org.apache.commons.imaging.ImageWriteException;
30  import org.apache.commons.imaging.common.BinaryFileParser;
31  import org.apache.commons.imaging.common.ByteConversions;
32  import org.apache.commons.imaging.common.bytesource.ByteSource;
33  import org.apache.commons.imaging.formats.jpeg.JpegConstants;
34  import org.apache.commons.imaging.formats.jpeg.JpegUtils;
35  import org.apache.commons.imaging.formats.jpeg.iptc.IptcParser;
36  
37  /**
38   * Interface for Exif write/update/remove functionality for Jpeg/JFIF images.
39   */
40  public class JpegRewriter extends BinaryFileParser {
41      private static final ByteOrder JPEG_BYTE_ORDER = ByteOrder.BIG_ENDIAN;
42      private static final SegmentFilter EXIF_SEGMENT_FILTER = new SegmentFilter() {
43          @Override
44          public boolean filter(final JFIFPieceSegment segment) {
45              return segment.isExifSegment();
46          }
47      };
48      private static final SegmentFilter XMP_SEGMENT_FILTER = new SegmentFilter() {
49          @Override
50          public boolean filter(final JFIFPieceSegment segment) {
51              return segment.isXmpSegment();
52          }
53      };
54      private static final SegmentFilter PHOTOSHOP_APP13_SEGMENT_FILTER = new SegmentFilter() {
55          @Override
56          public boolean filter(final JFIFPieceSegment segment) {
57              return segment.isPhotoshopApp13Segment();
58          }
59      };
60  
61      /**
62       * Constructor. to guess whether a file contains an image based on its file
63       * extension.
64       */
65      public JpegRewriter() {
66          setByteOrder(JPEG_BYTE_ORDER);
67      }
68  
69      protected static class JFIFPieces {
70          public final List<JFIFPiece> pieces;
71          public final List<JFIFPiece> segmentPieces;
72  
73          public JFIFPieces(final List<JFIFPiece> pieces,
74                  final List<JFIFPiece> segmentPieces) {
75              this.pieces = pieces;
76              this.segmentPieces = segmentPieces;
77          }
78  
79      }
80  
81      protected abstract static class JFIFPiece {
82          protected abstract void write(OutputStream os) throws IOException;
83  
84          @Override
85          public String toString() {
86              return "[" + this.getClass().getName() + "]";
87          }
88      }
89  
90      protected static class JFIFPieceSegment extends JFIFPiece {
91          public final int marker;
92          private final byte[] markerBytes;
93          private final byte[] segmentLengthBytes;
94          private final byte[] segmentData;
95  
96          public JFIFPieceSegment(final int marker, final byte[] segmentData) {
97              this(marker,
98                      ByteConversions.toBytes((short) marker, JPEG_BYTE_ORDER),
99                      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)
186             throws ImageReadException, IOException
187     // , ImageWriteException
188     {
189         final List<JFIFPiece> pieces = new ArrayList<>();
190         final List<JFIFPiece> segmentPieces = new ArrayList<>();
191 
192         final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
193             // return false to exit before reading image data.
194             @Override
195             public boolean beginSOS() {
196                 return true;
197             }
198 
199             @Override
200             public void visitSOS(final int marker, final byte[] markerBytes, final byte[] imageData) {
201                 pieces.add(new JFIFPieceImageData(markerBytes, imageData));
202             }
203 
204             // return false to exit traversal.
205             @Override
206             public boolean visitSegment(final int marker, final byte[] markerBytes,
207                     final int segmentLength, final byte[] segmentLengthBytes,
208                     final byte[] segmentData) throws ImageReadException, IOException {
209                 final JFIFPiece piece = new JFIFPieceSegment(marker, markerBytes,
210                         segmentLengthBytes, segmentData);
211                 pieces.add(piece);
212                 segmentPieces.add(piece);
213 
214                 return true;
215             }
216         };
217 
218         new JpegUtils().traverseJFIF(byteSource, visitor);
219 
220         return new JFIFPieces(pieces, segmentPieces);
221     }
222 
223     private interface SegmentFilter {
224         boolean filter(JFIFPieceSegment segment);
225     }
226 
227     protected <T extends JFIFPiece> List<T> removeXmpSegments(final List<T> segments) {
228         return filterSegments(segments, XMP_SEGMENT_FILTER);
229     }
230 
231     protected <T extends JFIFPiece> List<T> removePhotoshopApp13Segments(
232             final List<T> segments) {
233         return filterSegments(segments, PHOTOSHOP_APP13_SEGMENT_FILTER);
234     }
235 
236     protected <T extends JFIFPiece> List<T> findPhotoshopApp13Segments(
237             final List<T> segments) {
238         return filterSegments(segments, PHOTOSHOP_APP13_SEGMENT_FILTER, true);
239     }
240 
241     protected <T extends JFIFPiece> List<T> removeExifSegments(final List<T> segments) {
242         return filterSegments(segments, EXIF_SEGMENT_FILTER);
243     }
244 
245     protected <T extends JFIFPiece> List<T> filterSegments(final List<T> segments,
246             final SegmentFilter filter) {
247         return filterSegments(segments, filter, false);
248     }
249 
250     protected <T extends JFIFPiece> List<T> filterSegments(final List<T> segments,
251             final SegmentFilter filter, final boolean reverse) {
252         final List<T> result = new ArrayList<>();
253 
254         for (final T piece : segments) {
255             if (piece instanceof JFIFPieceSegment) {
256                 if (filter.filter((JFIFPieceSegment) piece) ^ !reverse) {
257                     result.add(piece);
258                 }
259             } else if (!reverse) {
260                 result.add(piece);
261             }
262         }
263 
264         return result;
265     }
266 
267     protected <T extends JFIFPiece, U extends JFIFPiece> List<JFIFPiece> insertBeforeFirstAppSegments(
268             final List<T> segments, final List<U> newSegments) throws ImageWriteException {
269         int firstAppIndex = -1;
270         for (int i = 0; i < segments.size(); i++) {
271             final JFIFPiece piece = segments.get(i);
272             if (!(piece instanceof JFIFPieceSegment)) {
273                 continue;
274             }
275 
276             final JFIFPieceSegment segment = (JFIFPieceSegment) piece;
277             if (segment.isAppSegment()) {
278                 if (firstAppIndex == -1) {
279                     firstAppIndex = i;
280                 }
281             }
282         }
283 
284         final List<JFIFPiece> result = new ArrayList<JFIFPiece>(segments);
285         if (firstAppIndex == -1) {
286             throw new ImageWriteException("JPEG file has no APP segments.");
287         }
288         result.addAll(firstAppIndex, newSegments);
289         return result;
290     }
291 
292     protected <T extends JFIFPiece, U extends JFIFPiece> List<JFIFPiece> insertAfterLastAppSegments(
293             final List<T> segments, final List<U> newSegments) throws ImageWriteException {
294         int lastAppIndex = -1;
295         for (int i = 0; i < segments.size(); i++) {
296             final JFIFPiece piece = segments.get(i);
297             if (!(piece instanceof JFIFPieceSegment)) {
298                 continue;
299             }
300 
301             final JFIFPieceSegment segment = (JFIFPieceSegment) piece;
302             if (segment.isAppSegment()) {
303                 lastAppIndex = i;
304             }
305         }
306 
307         final List<JFIFPiece> result = new ArrayList<JFIFPiece>(segments);
308         if (lastAppIndex == -1) {
309             if (segments.size() < 1) {
310                 throw new ImageWriteException("JPEG file has no APP segments.");
311             }
312             result.addAll(1, newSegments);
313         } else {
314             result.addAll(lastAppIndex + 1, newSegments);
315         }
316 
317         return result;
318     }
319 
320     protected void writeSegments(final OutputStream outputStream,
321             final List<? extends JFIFPiece> segments) throws IOException {
322         try (DataOutputStream os = new DataOutputStream(outputStream)) {
323             JpegConstants.SOI.writeTo(os);
324 
325             for (final JFIFPiece piece : segments) {
326                 piece.write(os);
327             }
328         }
329     }
330 
331     // private void writeSegment(OutputStream os, JFIFPieceSegment piece)
332     // throws ImageWriteException, IOException
333     // {
334     // byte markerBytes[] = convertShortToByteArray(JPEG_APP1_MARKER,
335     // JPEG_BYTE_ORDER);
336     // if (piece.segmentData.length > 0xffff)
337     // throw new JpegSegmentOverflowException("Jpeg segment is too long: "
338     // + piece.segmentData.length);
339     // int segmentLength = piece.segmentData.length + 2;
340     // byte segmentLengthBytes[] = convertShortToByteArray(segmentLength,
341     // JPEG_BYTE_ORDER);
342     //
343     // os.write(markerBytes);
344     // os.write(segmentLengthBytes);
345     // os.write(piece.segmentData);
346     // }
347 
348     public static class JpegSegmentOverflowException extends ImageWriteException {
349         private static final long serialVersionUID = -1062145751550646846L;
350 
351         public JpegSegmentOverflowException(final String message) {
352             super(message);
353         }
354     }
355 
356 }