1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.imaging.formats.jpeg.exif;
18
19 import static org.apache.commons.imaging.common.BinaryFunctions.remainingBytes;
20 import static org.apache.commons.imaging.common.BinaryFunctions.startsWith;
21
22 import java.io.ByteArrayOutputStream;
23 import java.io.DataOutputStream;
24 import java.io.File;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.OutputStream;
28 import java.nio.ByteOrder;
29 import java.util.ArrayList;
30 import java.util.List;
31
32 import org.apache.commons.imaging.ImagingException;
33 import org.apache.commons.imaging.ImagingOverflowException;
34 import org.apache.commons.imaging.bytesource.ByteSource;
35 import org.apache.commons.imaging.common.BinaryFileParser;
36 import org.apache.commons.imaging.common.ByteConversions;
37 import org.apache.commons.imaging.formats.jpeg.JpegConstants;
38 import org.apache.commons.imaging.formats.jpeg.JpegUtils;
39 import org.apache.commons.imaging.formats.tiff.write.AbstractTiffImageWriter;
40 import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossless;
41 import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy;
42 import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
43
44
45
46
47
48
49
50
51
52
53
54
55 public class ExifRewriter extends BinaryFileParser {
56
57 private abstract static class JFIFPiece {
58 protected abstract void write(OutputStream os) throws IOException;
59 }
60
61 private static final class JFIFPieceImageData extends JFIFPiece {
62 public final byte[] markerBytes;
63 public final byte[] imageData;
64
65 JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) {
66 this.markerBytes = markerBytes;
67 this.imageData = imageData;
68 }
69
70 @Override
71 protected void write(final OutputStream os) throws IOException {
72 os.write(markerBytes);
73 os.write(imageData);
74 }
75 }
76
77 private static final class JFIFPieces {
78 public final List<JFIFPiece> pieces;
79 public final List<JFIFPiece> exifPieces;
80
81 JFIFPieces(final List<JFIFPiece> pieces, final List<JFIFPiece> exifPieces) {
82 this.pieces = pieces;
83 this.exifPieces = exifPieces;
84 }
85
86 }
87
88 private static class JFIFPieceSegment extends JFIFPiece {
89 public final int marker;
90 public final byte[] markerBytes;
91 public final byte[] markerLengthBytes;
92 public final byte[] segmentData;
93
94 JFIFPieceSegment(final int marker, final byte[] markerBytes, final byte[] markerLengthBytes, final byte[] segmentData) {
95 this.marker = marker;
96 this.markerBytes = markerBytes;
97 this.markerLengthBytes = markerLengthBytes;
98 this.segmentData = segmentData;
99 }
100
101 @Override
102 protected void write(final OutputStream os) throws IOException {
103 os.write(markerBytes);
104 os.write(markerLengthBytes);
105 os.write(segmentData);
106 }
107 }
108
109 private static final class JFIFPieceSegmentExif extends JFIFPieceSegment {
110
111 JFIFPieceSegmentExif(final int marker, final byte[] markerBytes, final byte[] markerLengthBytes, final byte[] segmentData) {
112 super(marker, markerBytes, markerLengthBytes, segmentData);
113 }
114 }
115
116
117
118
119 public ExifRewriter() {
120 this(ByteOrder.BIG_ENDIAN);
121 }
122
123
124
125
126
127
128
129 public ExifRewriter(final ByteOrder byteOrder) {
130 super(byteOrder);
131 }
132
133 private JFIFPieces analyzeJfif(final ByteSource byteSource) throws ImagingException, IOException {
134 final List<JFIFPiece> pieces = new ArrayList<>();
135 final List<JFIFPiece> exifPieces = new ArrayList<>();
136
137 final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
138
139 @Override
140 public boolean beginSos() {
141 return true;
142 }
143
144
145 @Override
146 public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes,
147 final byte[] segmentData) throws
148
149 ImagingException, IOException {
150 if (marker != JpegConstants.JPEG_APP1_MARKER || !startsWith(segmentData, JpegConstants.EXIF_IDENTIFIER_CODE)) {
151 pieces.add(new JFIFPieceSegment(marker, markerBytes, markerLengthBytes, segmentData));
152 } else {
153 final JFIFPiece piece = new JFIFPieceSegmentExif(marker, markerBytes, markerLengthBytes, segmentData);
154 pieces.add(piece);
155 exifPieces.add(piece);
156 }
157 return true;
158 }
159
160 @Override
161 public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) {
162 pieces.add(new JFIFPieceImageData(markerBytes, imageData));
163 }
164 };
165
166 new JpegUtils().traverseJfif(byteSource, visitor);
167
168
169
170
171
172
173
174
175 return new JFIFPieces(pieces, exifPieces);
176 }
177
178
179
180
181
182
183
184
185
186
187 public void removeExifMetadata(final byte[] src, final OutputStream os) throws ImagingException, IOException, ImagingException {
188 final ByteSource byteSource = ByteSource.array(src);
189 removeExifMetadata(byteSource, os);
190 }
191
192
193
194
195
196
197
198
199
200
201 public void removeExifMetadata(final ByteSource byteSource, final OutputStream os) throws ImagingException, IOException, ImagingException {
202 final JFIFPieces jfifPieces = analyzeJfif(byteSource);
203 final List<JFIFPiece> pieces = jfifPieces.pieces;
204
205
206
207
208
209
210
211 writeSegmentsReplacingExif(os, pieces, null);
212 }
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
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
236
237
238
239
240
241
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
250
251
252
253
254
255
256
257
258
259
260
261
262
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
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286 public void updateExifMetadataLossless(final ByteSource byteSource, final OutputStream os, final TiffOutputSet outputSet)
287 throws ImagingException, IOException, ImagingException {
288
289 final JFIFPieces jfifPieces = analyzeJfif(byteSource);
290 final List<JFIFPiece> pieces = jfifPieces.pieces;
291
292 AbstractTiffImageWriter writer;
293
294
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
315
316
317
318
319
320
321
322
323
324
325
326
327
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
337
338
339
340
341
342
343
344
345
346
347
348
349
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
359
360
361
362
363
364
365
366
367
368
369
370
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
380
381
382
383
384
385
386
387
388
389
390
391
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
408
409
410
411
412
413
414
415
416
417
418
419
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
429
430
431
432
433
434
435
436
437
438
439
440
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
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 }