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.ImagingException;
29  import org.apache.commons.imaging.bytesource.ByteSource;
30  import org.apache.commons.imaging.common.BinaryFileParser;
31  import org.apache.commons.imaging.common.ByteConversions;
32  import org.apache.commons.imaging.formats.jpeg.JpegConstants;
33  import org.apache.commons.imaging.formats.jpeg.JpegUtils;
34  import org.apache.commons.imaging.formats.jpeg.iptc.IptcParser;
35  
36  /**
37   * Interface for Exif write/update/remove functionality for Jpeg/JFIF images.
38   */
39  public class JpegRewriter extends BinaryFileParser {
40      protected abstract static class JFIFPiece {
41          @Override
42          public String toString() {
43              return "[" + this.getClass().getName() + "]";
44          }
45  
46          protected abstract void write(OutputStream os) throws IOException;
47      }
48  
49      static class JFIFPieceImageData extends JFIFPiece {
50          private final byte[] markerBytes;
51          private final byte[] imageData;
52  
53          JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) {
54              this.markerBytes = markerBytes;
55              this.imageData = imageData;
56          }
57  
58          @Override
59          protected void write(final OutputStream os) throws IOException {
60              os.write(markerBytes);
61              os.write(imageData);
62          }
63      }
64  
65      protected static class JFIFPieces {
66          public final List<JFIFPiece> pieces;
67          public final List<JFIFPiece> segmentPieces;
68  
69          public JFIFPieces(final List<JFIFPiece> pieces, final List<JFIFPiece> segmentPieces) {
70              this.pieces = pieces;
71              this.segmentPieces = segmentPieces;
72          }
73  
74      }
75  
76      protected static class JFIFPieceSegment extends JFIFPiece {
77          public final int marker;
78          private final byte[] markerBytes;
79          private final byte[] segmentLengthBytes;
80          private final byte[] segmentData;
81  
82          public JFIFPieceSegment(final int marker, final byte[] segmentData) {
83              this(marker, ByteConversions.toBytes((short) marker, JPEG_BYTE_ORDER), ByteConversions.toBytes((short) (segmentData.length + 2), JPEG_BYTE_ORDER),
84                      segmentData);
85          }
86  
87          JFIFPieceSegment(final int marker, final byte[] markerBytes, final byte[] segmentLengthBytes, final byte[] segmentData) {
88              this.marker = marker;
89              this.markerBytes = markerBytes;
90              this.segmentLengthBytes = segmentLengthBytes;
91              this.segmentData = segmentData.clone();
92          }
93  
94          public byte[] getSegmentData() {
95              return segmentData.clone();
96          }
97  
98          public boolean isApp1Segment() {
99              return marker == JpegConstants.JPEG_APP1_MARKER;
100         }
101 
102         public boolean isAppSegment() {
103             return marker >= JpegConstants.JPEG_APP0_MARKER && marker <= JpegConstants.JPEG_APP15_MARKER;
104         }
105 
106         public boolean isExifSegment() {
107             if (marker != JpegConstants.JPEG_APP1_MARKER) {
108                 return false;
109             }
110             if (!startsWith(segmentData, JpegConstants.EXIF_IDENTIFIER_CODE)) {
111                 return false;
112             }
113             return true;
114         }
115 
116         public boolean isPhotoshopApp13Segment() {
117             if (marker != JpegConstants.JPEG_APP13_MARKER) {
118                 return false;
119             }
120             if (!new IptcParser().isPhotoshopJpegSegment(segmentData)) {
121                 return false;
122             }
123             return true;
124         }
125 
126         public boolean isXmpSegment() {
127             if (marker != JpegConstants.JPEG_APP1_MARKER) {
128                 return false;
129             }
130             if (!startsWith(segmentData, JpegConstants.XMP_IDENTIFIER)) {
131                 return false;
132             }
133             return true;
134         }
135 
136         @Override
137         public String toString() {
138             return "[" + this.getClass().getName() + " (0x" + Integer.toHexString(marker) + ")]";
139         }
140 
141         @Override
142         protected void write(final OutputStream os) throws IOException {
143             os.write(markerBytes);
144             os.write(segmentLengthBytes);
145             os.write(segmentData);
146         }
147 
148     }
149 
150     private interface SegmentFilter {
151         boolean filter(JFIFPieceSegment segment);
152     }
153 
154     private static final ByteOrder JPEG_BYTE_ORDER = ByteOrder.BIG_ENDIAN;
155 
156     private static final SegmentFilter EXIF_SEGMENT_FILTER = JFIFPieceSegment::isExifSegment;
157 
158     private static final SegmentFilter XMP_SEGMENT_FILTER = JFIFPieceSegment::isXmpSegment;
159 
160     private static final SegmentFilter PHOTOSHOP_APP13_SEGMENT_FILTER = JFIFPieceSegment::isPhotoshopApp13Segment;
161 
162     /**
163      * Constructs a new instance. to guess whether a file contains an image based on its file extension.
164      */
165     public JpegRewriter() {
166         super(JPEG_BYTE_ORDER);
167     }
168 
169     protected JFIFPieces analyzeJfif(final ByteSource byteSource) throws ImagingException, 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             // return false to exit traversal.
181             @Override
182             public boolean visitSegment(final int marker, final byte[] markerBytes, final int segmentLength, final byte[] segmentLengthBytes,
183                     final byte[] segmentData) throws ImagingException, IOException {
184                 final JFIFPiece piece = new JFIFPieceSegment(marker, markerBytes, segmentLengthBytes, segmentData);
185                 pieces.add(piece);
186                 segmentPieces.add(piece);
187 
188                 return true;
189             }
190 
191             @Override
192             public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) {
193                 pieces.add(new JFIFPieceImageData(markerBytes, imageData));
194             }
195         };
196 
197         new JpegUtils().traverseJfif(byteSource, visitor);
198 
199         return new JFIFPieces(pieces, segmentPieces);
200     }
201 
202     protected <T extends JFIFPiece> List<T> filterSegments(final List<T> segments, final SegmentFilter filter) {
203         return filterSegments(segments, filter, false);
204     }
205 
206     protected <T extends JFIFPiece> List<T> filterSegments(final List<T> segments, final SegmentFilter filter, final boolean reverse) {
207         final List<T> result = new ArrayList<>();
208 
209         for (final T piece : segments) {
210             if (piece instanceof JFIFPieceSegment) {
211                 if (filter.filter((JFIFPieceSegment) piece) == reverse) {
212                     result.add(piece);
213                 }
214             } else if (!reverse) {
215                 result.add(piece);
216             }
217         }
218 
219         return result;
220     }
221 
222     protected <T extends JFIFPiece> List<T> findPhotoshopApp13Segments(final List<T> segments) {
223         return filterSegments(segments, PHOTOSHOP_APP13_SEGMENT_FILTER, true);
224     }
225 
226     protected <T extends JFIFPiece, U extends JFIFPiece> List<JFIFPiece> insertAfterLastAppSegments(final List<T> segments, final List<U> newSegments)
227             throws ImagingException {
228         int lastAppIndex = -1;
229         for (int i = 0; i < segments.size(); i++) {
230             final JFIFPiece piece = segments.get(i);
231             if (!(piece instanceof JFIFPieceSegment)) {
232                 continue;
233             }
234 
235             final JFIFPieceSegment segment = (JFIFPieceSegment) piece;
236             if (segment.isAppSegment()) {
237                 lastAppIndex = i;
238             }
239         }
240 
241         final List<JFIFPiece> result = new ArrayList<>(segments);
242         if (lastAppIndex == -1) {
243             if (segments.isEmpty()) {
244                 throw new ImagingException("JPEG file has no APP segments.");
245             }
246             result.addAll(1, newSegments);
247         } else {
248             result.addAll(lastAppIndex + 1, newSegments);
249         }
250 
251         return result;
252     }
253 
254     protected <T extends JFIFPiece, U extends JFIFPiece> List<JFIFPiece> insertBeforeFirstAppSegments(final List<T> segments, final List<U> newSegments)
255             throws ImagingException {
256         int firstAppIndex = -1;
257         for (int i = 0; i < segments.size(); i++) {
258             final JFIFPiece piece = segments.get(i);
259             if (!(piece instanceof JFIFPieceSegment)) {
260                 continue;
261             }
262 
263             final JFIFPieceSegment segment = (JFIFPieceSegment) piece;
264             if (segment.isAppSegment()) {
265                 if (firstAppIndex == -1) {
266                     firstAppIndex = i;
267                 }
268             }
269         }
270 
271         final List<JFIFPiece> result = new ArrayList<>(segments);
272         if (firstAppIndex == -1) {
273             throw new ImagingException("JPEG file has no APP segments.");
274         }
275         result.addAll(firstAppIndex, newSegments);
276         return result;
277     }
278 
279     protected <T extends JFIFPiece> List<T> removeExifSegments(final List<T> segments) {
280         return filterSegments(segments, EXIF_SEGMENT_FILTER);
281     }
282 
283     protected <T extends JFIFPiece> List<T> removePhotoshopApp13Segments(final List<T> segments) {
284         return filterSegments(segments, PHOTOSHOP_APP13_SEGMENT_FILTER);
285     }
286 
287     protected <T extends JFIFPiece> List<T> removeXmpSegments(final List<T> segments) {
288         return filterSegments(segments, XMP_SEGMENT_FILTER);
289     }
290 
291     // private void writeSegment(OutputStream os, JFIFPieceSegment piece)
292     // throws ImageWriteException, IOException
293     // {
294     // byte[] markerBytes = convertShortToByteArray(JPEG_APP1_MARKER,
295     // JPEG_BYTE_ORDER);
296     // if (piece.segmentData.length > 0xffff)
297     // throw new JpegSegmentOverflowException("JPEG segment is too long: "
298     // + piece.segmentData.length);
299     // int segmentLength = piece.segmentData.length + 2;
300     // byte[] segmentLengthBytes = convertShortToByteArray(segmentLength,
301     // JPEG_BYTE_ORDER);
302     //
303     // os.write(markerBytes);
304     // os.write(segmentLengthBytes);
305     // os.write(piece.segmentData);
306     // }
307 
308     protected void writeSegments(final OutputStream outputStream, final List<? extends JFIFPiece> segments) throws IOException {
309         try (DataOutputStream os = new DataOutputStream(outputStream)) {
310             JpegConstants.SOI.writeTo(os);
311 
312             for (final JFIFPiece piece : segments) {
313                 piece.write(os);
314             }
315         }
316     }
317 
318 }