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.exif;
18
19 import static org.apache.commons.imaging.common.BinaryFunctions.remainingBytes;
20
21 import java.io.ByteArrayOutputStream;
22 import java.io.DataOutputStream;
23 import java.io.File;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.OutputStream;
27 import java.nio.ByteOrder;
28 import java.util.ArrayList;
29 import java.util.List;
30
31 import org.apache.commons.imaging.ImagingException;
32 import org.apache.commons.imaging.ImagingOverflowException;
33 import org.apache.commons.imaging.bytesource.ByteSource;
34 import org.apache.commons.imaging.common.BinaryFileParser;
35 import org.apache.commons.imaging.common.ByteConversions;
36 import org.apache.commons.imaging.formats.jpeg.JpegConstants;
37 import org.apache.commons.imaging.formats.jpeg.JpegUtils;
38 import org.apache.commons.imaging.formats.tiff.write.AbstractTiffImageWriter;
39 import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossless;
40 import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy;
41 import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
42
43 /**
44 * Interface for Exif write/update/remove functionality for Jpeg/JFIF images.
45 *
46 * <p>
47 * See the source of the ExifMetadataUpdateExample class for example usage.
48 * </p>
49 *
50 * @see <a href=
51 * "https://svn.apache.org/repos/asf/commons/proper/imaging/trunk/src/test/java/org/apache/commons/imaging/examples/WriteExifMetadataExample.java">
52 * org.apache.commons.imaging.examples.WriteExifMetadataExample</a>
53 */
54 public class ExifRewriter extends BinaryFileParser {
55
56 private abstract static class JFIFPiece {
57 protected abstract void write(OutputStream os) throws IOException;
58 }
59
60 private static final class JFIFPieceImageData extends JFIFPiece {
61 public final byte[] markerBytes;
62 public final byte[] imageData;
63
64 JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) {
65 this.markerBytes = markerBytes;
66 this.imageData = imageData;
67 }
68
69 @Override
70 protected void write(final OutputStream os) throws IOException {
71 os.write(markerBytes);
72 os.write(imageData);
73 }
74 }
75
76 private static final class JFIFPieces {
77 public final List<JFIFPiece> pieces;
78 public final List<JFIFPiece> exifPieces;
79
80 JFIFPieces(final List<JFIFPiece> pieces, final List<JFIFPiece> exifPieces) {
81 this.pieces = pieces;
82 this.exifPieces = exifPieces;
83 }
84
85 }
86
87 private static class JFIFPieceSegment extends JFIFPiece {
88 public final int marker;
89 public final byte[] markerBytes;
90 public final byte[] markerLengthBytes;
91 public final byte[] segmentData;
92
93 JFIFPieceSegment(final int marker, final byte[] markerBytes, final byte[] markerLengthBytes, final byte[] segmentData) {
94 this.marker = marker;
95 this.markerBytes = markerBytes;
96 this.markerLengthBytes = markerLengthBytes;
97 this.segmentData = segmentData;
98 }
99
100 @Override
101 protected void write(final OutputStream os) throws IOException {
102 os.write(markerBytes);
103 os.write(markerLengthBytes);
104 os.write(segmentData);
105 }
106 }
107
108 private static final class JFIFPieceSegmentExif extends JFIFPieceSegment {
109
110 JFIFPieceSegmentExif(final int marker, final byte[] markerBytes, final byte[] markerLengthBytes, final byte[] segmentData) {
111 super(marker, markerBytes, markerLengthBytes, segmentData);
112 }
113 }
114
115 /**
116 * Constructs a new instance with the default, big-endian, byte order.
117 * <p>
118 * Whether a file contains an image based on its file extension.
119 * </p>
120 */
121 public ExifRewriter() {
122 this(ByteOrder.BIG_ENDIAN);
123 }
124
125 /**
126 * Constructs a new instance.
127 *
128 * @param byteOrder byte order of EXIF segment.
129 */
130 public ExifRewriter(final ByteOrder byteOrder) {
131 super(byteOrder);
132 }
133
134 private JFIFPieces analyzeJfif(final ByteSource byteSource) throws ImagingException, IOException {
135 final List<JFIFPiece> pieces = new ArrayList<>();
136 final List<JFIFPiece> exifPieces = new ArrayList<>();
137
138 final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
139 // return false to exit before reading image data.
140 @Override
141 public boolean beginSos() {
142 return true;
143 }
144
145 // return false to exit traversal.
146 @Override
147 public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes,
148 final byte[] segmentData) throws
149 // ImageWriteException,
150 ImagingException, IOException {
151 if (marker != JpegConstants.JPEG_APP1_MARKER || !JpegConstants.EXIF_IDENTIFIER_CODE.isStartOf(segmentData)) {
152 pieces.add(new JFIFPieceSegment(marker, markerBytes, markerLengthBytes, segmentData));
153 } else {
154 final JFIFPiece piece = new JFIFPieceSegmentExif(marker, markerBytes, markerLengthBytes, segmentData);
155 pieces.add(piece);
156 exifPieces.add(piece);
157 }
158 return true;
159 }
160
161 @Override
162 public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) {
163 pieces.add(new JFIFPieceImageData(markerBytes, imageData));
164 }
165 };
166
167 new JpegUtils().traverseJfif(byteSource, visitor);
168
169 // GenericSegment exifSegment = exifSegmentArray[0];
170 // if (exifSegments.size() < 1)
171 // {
172 // // TODO: add support for adding, not just replacing.
173 // throw new ImageReadException("No APP1 EXIF segment found.");
174 // }
175
176 return new JFIFPieces(pieces, exifPieces);
177 }
178
179 /**
180 * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
181 *
182 * @param src Byte array containing JPEG image data.
183 * @param os OutputStream to write the image to.
184 * @throws ImagingException if it fails to read the JFIF segments
185 * @throws IOException if it fails to read the image data
186 * @throws ImagingException if it fails to write the updated data
187 */
188 public void removeExifMetadata(final byte[] src, final OutputStream os) throws ImagingException, IOException, ImagingException {
189 final ByteSource byteSource = ByteSource.array(src);
190 removeExifMetadata(byteSource, os);
191 }
192
193 /**
194 * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
195 *
196 * @param byteSource ByteSource containing JPEG image data.
197 * @param os OutputStream to write the image to.
198 * @throws ImagingException if it fails to read the JFIF segments
199 * @throws IOException if it fails to read the image data
200 * @throws ImagingException if it fails to write the updated data
201 */
202 public void removeExifMetadata(final ByteSource byteSource, final OutputStream os) throws ImagingException, IOException, ImagingException {
203 final JFIFPieces jfifPieces = analyzeJfif(byteSource);
204 final List<JFIFPiece> pieces = jfifPieces.pieces;
205
206 // Debug.debug("pieces", pieces);
207
208 // pieces.removeAll(jfifPieces.exifSegments);
209
210 // Debug.debug("pieces", pieces);
211
212 writeSegmentsReplacingExif(os, pieces, null);
213 }
214
215 /**
216 * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
217 * <p>
218 *
219 * @param src Image file.
220 * @param os OutputStream to write the image to.
221 * @throws ImagingException if it fails to read the JFIF segments
222 * @throws IOException if it fails to read the image data
223 * @throws ImagingException if it fails to write the updated data
224 * @see java.io.File
225 * @see java.io.OutputStream
226 * @see java.io.File
227 * @see java.io.OutputStream
228 */
229 public void removeExifMetadata(final File src, final OutputStream os) throws ImagingException, IOException, ImagingException {
230 final ByteSource byteSource = ByteSource.file(src);
231 removeExifMetadata(byteSource, os);
232 }
233
234 /**
235 * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream.
236 *
237 * @param src InputStream containing JPEG image data.
238 * @param os OutputStream to write the image to.
239 * @throws ImagingException if it fails to read the JFIF segments
240 * @throws IOException if it fails to read the image data
241 * @throws ImagingException if it fails to write the updated data
242 */
243 public void removeExifMetadata(final InputStream src, final OutputStream os) throws ImagingException, IOException, ImagingException {
244 final ByteSource byteSource = ByteSource.inputStream(src, null);
245 removeExifMetadata(byteSource, os);
246 }
247
248 /**
249 * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
250 *
251 * <p>
252 * Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF segment that it can't parse (such as Maker Notes), this
253 * algorithm avoids overwriting any part of the original segment that it couldn't parse. This can cause the EXIF segment to grow with each update, which is
254 * a serious issue, since all EXIF data must fit in a single APP1 segment of the JPEG image.
255 * </p>
256 *
257 * @param src Byte array containing JPEG image data.
258 * @param os OutputStream to write the image to.
259 * @param outputSet TiffOutputSet containing the EXIF data to write.
260 * @throws ImagingException if it fails to read the JFIF segments
261 * @throws IOException if it fails to read the image data
262 * @throws ImagingException if it fails to write the updated data
263 */
264 public void updateExifMetadataLossless(final byte[] src, final OutputStream os, final TiffOutputSet outputSet)
265 throws ImagingException, IOException, ImagingException {
266 final ByteSource byteSource = ByteSource.array(src);
267 updateExifMetadataLossless(byteSource, os, outputSet);
268 }
269
270 /**
271 * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
272 *
273 * <p>
274 * Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF segment that it can't parse (such as Maker Notes), this
275 * algorithm avoids overwriting any part of the original segment that it couldn't parse. This can cause the EXIF segment to grow with each update, which is
276 * a serious issue, since all EXIF data must fit in a single APP1 segment of the JPEG image.
277 * </p>
278 *
279 * @param byteSource ByteSource containing JPEG image data.
280 * @param os OutputStream to write the image to.
281 * @param outputSet TiffOutputSet containing the EXIF data to write.
282 * @throws ImagingException if it fails to read the JFIF segments
283 * @throws IOException if it fails to read the image data
284 * @throws ImagingException if it fails to write the updated data
285 */
286 public void updateExifMetadataLossless(final ByteSource byteSource, final OutputStream os, final TiffOutputSet outputSet)
287 throws ImagingException, IOException, ImagingException {
288 // List outputDirectories = outputSet.getDirectories();
289 final JFIFPieces jfifPieces = analyzeJfif(byteSource);
290 final List<JFIFPiece> pieces = jfifPieces.pieces;
291
292 final AbstractTiffImageWriter writer;
293 // Just use first APP1 segment for now.
294 // Multiple APP1 segments are rare and poorly supported.
295 if (!jfifPieces.exifPieces.isEmpty()) {
296 final JFIFPieceSegment exifPiece = (JFIFPieceSegment) jfifPieces.exifPieces.get(0);
297
298 byte[] exifBytes = exifPiece.segmentData;
299 exifBytes = remainingBytes("trimmed exif bytes", exifBytes, 6);
300
301 writer = new TiffImageWriterLossless(outputSet.byteOrder, exifBytes);
302
303 } else {
304 writer = new TiffImageWriterLossy(outputSet.byteOrder);
305 }
306
307 final boolean includeEXIFPrefix = true;
308 final byte[] newBytes = writeExifSegment(writer, outputSet, includeEXIFPrefix);
309
310 writeSegmentsReplacingExif(os, pieces, newBytes);
311 }
312
313 /**
314 * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
315 *
316 * <p>
317 * Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF segment that it can't parse (such as Maker Notes), this
318 * algorithm avoids overwriting any part of the original segment that it couldn't parse. This can cause the EXIF segment to grow with each update, which is
319 * a serious issue, since all EXIF data must fit in a single APP1 segment of the JPEG image.
320 * </p>
321 *
322 * @param src Image file.
323 * @param os OutputStream to write the image to.
324 * @param outputSet TiffOutputSet containing the EXIF data to write.
325 * @throws ImagingException if it fails to read the JFIF segments
326 * @throws IOException if it fails to read the image data
327 * @throws ImagingException if it fails to write the updated data
328 */
329 public void updateExifMetadataLossless(final File src, final OutputStream os, final TiffOutputSet outputSet)
330 throws ImagingException, IOException, ImagingException {
331 final ByteSource byteSource = ByteSource.file(src);
332 updateExifMetadataLossless(byteSource, os, outputSet);
333 }
334
335 /**
336 * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
337 *
338 * <p>
339 * Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF segment that it can't parse (such as Maker Notes), this
340 * algorithm avoids overwriting any part of the original segment that it couldn't parse. This can cause the EXIF segment to grow with each update, which is
341 * a serious issue, since all EXIF data must fit in a single APP1 segment of the JPEG image.
342 * </p>
343 *
344 * @param src InputStream containing JPEG image data.
345 * @param os OutputStream to write the image to.
346 * @param outputSet TiffOutputSet containing the EXIF data to write.
347 * @throws ImagingException if it fails to read the JFIF segments
348 * @throws IOException if it fails to read the image data
349 * @throws ImagingException if it fails to write the updated data
350 */
351 public void updateExifMetadataLossless(final InputStream src, final OutputStream os, final TiffOutputSet outputSet)
352 throws ImagingException, IOException, ImagingException {
353 final ByteSource byteSource = ByteSource.inputStream(src, null);
354 updateExifMetadataLossless(byteSource, os, outputSet);
355 }
356
357 /**
358 * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
359 *
360 * <p>
361 * Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, ignoring the possibility that it may be discarding data it
362 * couldn't parse (such as Maker Notes).
363 * </p>
364 *
365 * @param src Byte array containing JPEG image data.
366 * @param os OutputStream to write the image to.
367 * @param outputSet TiffOutputSet containing the EXIF data to write.
368 * @throws ImagingException if it fails to read the JFIF segments
369 * @throws IOException if it fails to read the image data
370 * @throws ImagingException if it fails to write the updated data
371 */
372 public void updateExifMetadataLossy(final byte[] src, final OutputStream os, final TiffOutputSet outputSet)
373 throws ImagingException, IOException, ImagingException {
374 final ByteSource byteSource = ByteSource.array(src);
375 updateExifMetadataLossy(byteSource, os, outputSet);
376 }
377
378 /**
379 * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
380 *
381 * <p>
382 * Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, ignoring the possibility that it may be discarding data it
383 * couldn't parse (such as Maker Notes).
384 * </p>
385 *
386 * @param byteSource ByteSource containing JPEG image data.
387 * @param os OutputStream to write the image to.
388 * @param outputSet TiffOutputSet containing the EXIF data to write.
389 * @throws ImagingException if it fails to read the JFIF segments
390 * @throws IOException if it fails to read the image data
391 * @throws ImagingException if it fails to write the updated data
392 */
393 public void updateExifMetadataLossy(final ByteSource byteSource, final OutputStream os, final TiffOutputSet outputSet)
394 throws ImagingException, IOException, ImagingException {
395 final JFIFPieces jfifPieces = analyzeJfif(byteSource);
396 final List<JFIFPiece> pieces = jfifPieces.pieces;
397
398 final AbstractTiffImageWriter writer = new TiffImageWriterLossy(outputSet.byteOrder);
399
400 final boolean includeEXIFPrefix = true;
401 final byte[] newBytes = writeExifSegment(writer, outputSet, includeEXIFPrefix);
402
403 writeSegmentsReplacingExif(os, pieces, newBytes);
404 }
405
406 /**
407 * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
408 *
409 * <p>
410 * Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, ignoring the possibility that it may be discarding data it
411 * couldn't parse (such as Maker Notes).
412 * </p>
413 *
414 * @param src Image file.
415 * @param os OutputStream to write the image to.
416 * @param outputSet TiffOutputSet containing the EXIF data to write.
417 * @throws ImagingException if it fails to read the JFIF segments
418 * @throws IOException if it fails to read the image data
419 * @throws ImagingException if it fails to write the updated data
420 */
421 public void updateExifMetadataLossy(final File src, final OutputStream os, final TiffOutputSet outputSet)
422 throws ImagingException, IOException, ImagingException {
423 final ByteSource byteSource = ByteSource.file(src);
424 updateExifMetadataLossy(byteSource, os, outputSet);
425 }
426
427 /**
428 * Reads a JPEG image, replaces the EXIF metadata and writes the result to a stream.
429 *
430 * <p>
431 * Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, ignoring the possibility that it may be discarding data it
432 * couldn't parse (such as Maker Notes).
433 * </p>
434 *
435 * @param src InputStream containing JPEG image data.
436 * @param os OutputStream to write the image to.
437 * @param outputSet TiffOutputSet containing the EXIF data to write.
438 * @throws ImagingException if it fails to read the JFIF segments
439 * @throws IOException if it fails to read the image data
440 * @throws ImagingException if it fails to write the updated data
441 */
442 public void updateExifMetadataLossy(final InputStream src, final OutputStream os, final TiffOutputSet outputSet)
443 throws ImagingException, IOException, ImagingException {
444 final ByteSource byteSource = ByteSource.inputStream(src, null);
445 updateExifMetadataLossy(byteSource, os, outputSet);
446 }
447
448 private byte[] writeExifSegment(final AbstractTiffImageWriter writer, final TiffOutputSet outputSet, final boolean includeEXIFPrefix)
449 throws IOException, ImagingException {
450 final ByteArrayOutputStream os = new ByteArrayOutputStream();
451
452 if (includeEXIFPrefix) {
453 JpegConstants.EXIF_IDENTIFIER_CODE.writeTo(os);
454 os.write(0);
455 os.write(0);
456 }
457
458 writer.write(os, outputSet);
459
460 return os.toByteArray();
461 }
462
463 private void writeSegmentsReplacingExif(final OutputStream outputStream, final List<JFIFPiece> segments, final byte[] newBytes)
464 throws ImagingException, IOException {
465
466 try (DataOutputStream os = new DataOutputStream(outputStream)) {
467 JpegConstants.SOI.writeTo(os);
468
469 boolean hasExif = false;
470
471 for (final JFIFPiece piece : segments) {
472 if (piece instanceof JFIFPieceSegmentExif) {
473 hasExif = true;
474 break;
475 }
476 }
477
478 if (!hasExif && newBytes != null) {
479 final byte[] markerBytes = ByteConversions.toBytes((short) JpegConstants.JPEG_APP1_MARKER, getByteOrder());
480 if (newBytes.length > 0xffff) {
481 throw new ImagingOverflowException("APP1 Segment is too long: " + newBytes.length);
482 }
483 final int markerLength = newBytes.length + 2;
484 final byte[] markerLengthBytes = ByteConversions.toBytes((short) markerLength, getByteOrder());
485
486 int index = 0;
487 final JFIFPieceSegment firstSegment = (JFIFPieceSegment) segments.get(index);
488 if (firstSegment.marker == JpegConstants.JFIF_MARKER) {
489 index = 1;
490 }
491 segments.add(index, new JFIFPieceSegmentExif(JpegConstants.JPEG_APP1_MARKER, markerBytes, markerLengthBytes, newBytes));
492 }
493
494 boolean APP1Written = false;
495
496 for (final JFIFPiece piece : segments) {
497 if (piece instanceof JFIFPieceSegmentExif) {
498 // only replace first APP1 segment; skips others.
499 if (APP1Written) {
500 continue;
501 }
502 APP1Written = true;
503
504 if (newBytes == null) {
505 continue;
506 }
507
508 final byte[] markerBytes = ByteConversions.toBytes((short) JpegConstants.JPEG_APP1_MARKER, getByteOrder());
509 if (newBytes.length > 0xffff) {
510 throw new ImagingOverflowException("APP1 Segment is too long: " + newBytes.length);
511 }
512 final int markerLength = newBytes.length + 2;
513 final byte[] markerLengthBytes = ByteConversions.toBytes((short) markerLength, getByteOrder());
514
515 os.write(markerBytes);
516 os.write(markerLengthBytes);
517 os.write(newBytes);
518 } else {
519 piece.write(os);
520 }
521 }
522 }
523 }
524
525 }