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.exif; 018 019import static org.apache.commons.imaging.common.BinaryFunctions.remainingBytes; 020import static org.apache.commons.imaging.common.BinaryFunctions.startsWith; 021 022import java.io.ByteArrayOutputStream; 023import java.io.DataOutputStream; 024import java.io.File; 025import java.io.IOException; 026import java.io.InputStream; 027import java.io.OutputStream; 028import java.nio.ByteOrder; 029import java.util.ArrayList; 030import java.util.List; 031 032import org.apache.commons.imaging.ImageReadException; 033import org.apache.commons.imaging.ImageWriteException; 034import org.apache.commons.imaging.common.BinaryFileParser; 035import org.apache.commons.imaging.common.ByteConversions; 036import org.apache.commons.imaging.common.bytesource.ByteSource; 037import org.apache.commons.imaging.common.bytesource.ByteSourceArray; 038import org.apache.commons.imaging.common.bytesource.ByteSourceFile; 039import org.apache.commons.imaging.common.bytesource.ByteSourceInputStream; 040import org.apache.commons.imaging.formats.jpeg.JpegConstants; 041import org.apache.commons.imaging.formats.jpeg.JpegUtils; 042import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterBase; 043import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossless; 044import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy; 045import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet; 046 047/** 048 * Interface for Exif write/update/remove functionality for Jpeg/JFIF images. 049 * 050 * <p>See the source of the ExifMetadataUpdateExample class for example usage.</p> 051 * 052 * @see <a 053 * href="https://svn.apache.org/repos/asf/commons/proper/imaging/trunk/src/test/java/org/apache/commons/imaging/examples/WriteExifMetadataExample.java">org.apache.commons.imaging.examples.WriteExifMetadataExample</a> 054 */ 055public class ExifRewriter extends BinaryFileParser { 056 /** 057 * Constructor. to guess whether a file contains an image based on its file 058 * extension. 059 */ 060 public ExifRewriter() { 061 this(ByteOrder.BIG_ENDIAN); 062 } 063 064 /** 065 * Constructor. 066 * <p> 067 * 068 * @param byteOrder 069 * byte order of EXIF segment. 070 */ 071 public ExifRewriter(final ByteOrder byteOrder) { 072 setByteOrder(byteOrder); 073 } 074 075 private static class JFIFPieces { 076 public final List<JFIFPiece> pieces; 077 public final List<JFIFPiece> exifPieces; 078 079 JFIFPieces(final List<JFIFPiece> pieces, 080 final List<JFIFPiece> exifPieces) { 081 this.pieces = pieces; 082 this.exifPieces = exifPieces; 083 } 084 085 } 086 087 private abstract static class JFIFPiece { 088 protected abstract void write(OutputStream os) throws IOException; 089 } 090 091 private static class JFIFPieceSegment extends JFIFPiece { 092 public final int marker; 093 public final byte[] markerBytes; 094 public final byte[] markerLengthBytes; 095 public final byte[] segmentData; 096 097 JFIFPieceSegment(final int marker, final byte[] markerBytes, 098 final byte[] markerLengthBytes, final byte[] segmentData) { 099 this.marker = marker; 100 this.markerBytes = markerBytes; 101 this.markerLengthBytes = markerLengthBytes; 102 this.segmentData = segmentData; 103 } 104 105 @Override 106 protected void write(final OutputStream os) throws IOException { 107 os.write(markerBytes); 108 os.write(markerLengthBytes); 109 os.write(segmentData); 110 } 111 } 112 113 private static class JFIFPieceSegmentExif extends JFIFPieceSegment { 114 115 JFIFPieceSegmentExif(final int marker, final byte[] markerBytes, 116 final byte[] markerLengthBytes, final byte[] segmentData) { 117 super(marker, markerBytes, markerLengthBytes, segmentData); 118 } 119 } 120 121 private static class JFIFPieceImageData extends JFIFPiece { 122 public final byte[] markerBytes; 123 public final byte[] imageData; 124 125 JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) { 126 super(); 127 this.markerBytes = markerBytes; 128 this.imageData = imageData; 129 } 130 131 @Override 132 protected void write(final OutputStream os) throws IOException { 133 os.write(markerBytes); 134 os.write(imageData); 135 } 136 } 137 138 private JFIFPieces analyzeJFIF(final ByteSource byteSource) throws ImageReadException, IOException { 139 final List<JFIFPiece> pieces = new ArrayList<>(); 140 final List<JFIFPiece> exifPieces = new ArrayList<>(); 141 142 final JpegUtils.Visitor visitor = new JpegUtils.Visitor() { 143 // return false to exit before reading image data. 144 @Override 145 public boolean beginSOS() { 146 return true; 147 } 148 149 @Override 150 public void visitSOS(final int marker, final byte[] markerBytes, final byte[] imageData) { 151 pieces.add(new JFIFPieceImageData(markerBytes, imageData)); 152 } 153 154 // return false to exit traversal. 155 @Override 156 public boolean visitSegment(final int marker, final byte[] markerBytes, 157 final int markerLength, final byte[] markerLengthBytes, 158 final byte[] segmentData) throws 159 // ImageWriteException, 160 ImageReadException, IOException { 161 if (marker != JpegConstants.JPEG_APP1_MARKER) { 162 pieces.add(new JFIFPieceSegment(marker, markerBytes, 163 markerLengthBytes, segmentData)); 164 } else if (!startsWith(segmentData, 165 JpegConstants.EXIF_IDENTIFIER_CODE)) { 166 pieces.add(new JFIFPieceSegment(marker, markerBytes, 167 markerLengthBytes, segmentData)); 168 // } else if (exifSegmentArray[0] != null) { 169 // // TODO: add support for multiple segments 170 // throw new ImageReadException( 171 // "More than one APP1 EXIF segment."); 172 } else { 173 final JFIFPiece piece = new JFIFPieceSegmentExif(marker, 174 markerBytes, markerLengthBytes, segmentData); 175 pieces.add(piece); 176 exifPieces.add(piece); 177 } 178 return true; 179 } 180 }; 181 182 new JpegUtils().traverseJFIF(byteSource, visitor); 183 184 // GenericSegment exifSegment = exifSegmentArray[0]; 185 // if (exifSegments.size() < 1) 186 // { 187 // // TODO: add support for adding, not just replacing. 188 // throw new ImageReadException("No APP1 EXIF segment found."); 189 // } 190 191 return new JFIFPieces(pieces, exifPieces); 192 } 193 194 /** 195 * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1 196 * segment), and writes the result to a stream. 197 * <p> 198 * 199 * @param src 200 * Image file. 201 * @param os 202 * OutputStream to write the image to. 203 * 204 * @throws ImageReadException if it fails to read the JFIF segments 205 * @throws IOException if it fails to read the image data 206 * @throws ImageWriteException if it fails to write the updated data 207 * @see java.io.File 208 * @see java.io.OutputStream 209 * @see java.io.File 210 * @see java.io.OutputStream 211 */ 212 public void removeExifMetadata(final File src, final OutputStream os) 213 throws ImageReadException, IOException, ImageWriteException { 214 final ByteSource byteSource = new ByteSourceFile(src); 215 removeExifMetadata(byteSource, os); 216 } 217 218 /** 219 * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1 220 * segment), and writes the result to a stream. 221 * 222 * @param src 223 * Byte array containing Jpeg image data. 224 * @param os 225 * OutputStream to write the image to. 226 * @throws ImageReadException if it fails to read the JFIF segments 227 * @throws IOException if it fails to read the image data 228 * @throws ImageWriteException if it fails to write the updated data 229 */ 230 public void removeExifMetadata(final byte[] src, final OutputStream os) 231 throws ImageReadException, IOException, ImageWriteException { 232 final ByteSource byteSource = new ByteSourceArray(src); 233 removeExifMetadata(byteSource, os); 234 } 235 236 /** 237 * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1 238 * segment), and writes the result to a stream. 239 * 240 * @param src 241 * InputStream containing Jpeg image data. 242 * @param os 243 * OutputStream to write the image to. 244 * @throws ImageReadException if it fails to read the JFIF segments 245 * @throws IOException if it fails to read the image data 246 * @throws ImageWriteException if it fails to write the updated data 247 */ 248 public void removeExifMetadata(final InputStream src, final OutputStream os) 249 throws ImageReadException, IOException, ImageWriteException { 250 final ByteSource byteSource = new ByteSourceInputStream(src, null); 251 removeExifMetadata(byteSource, os); 252 } 253 254 /** 255 * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1 256 * segment), and writes the result to a stream. 257 * 258 * @param byteSource 259 * ByteSource containing Jpeg image data. 260 * @param os 261 * OutputStream to write the image to. 262 * @throws ImageReadException if it fails to read the JFIF segments 263 * @throws IOException if it fails to read the image data 264 * @throws ImageWriteException if it fails to write the updated data 265 */ 266 public void removeExifMetadata(final ByteSource byteSource, final OutputStream os) 267 throws ImageReadException, IOException, ImageWriteException { 268 final JFIFPieces jfifPieces = analyzeJFIF(byteSource); 269 final List<JFIFPiece> pieces = jfifPieces.pieces; 270 271 // Debug.debug("pieces", pieces); 272 273 // pieces.removeAll(jfifPieces.exifSegments); 274 275 // Debug.debug("pieces", pieces); 276 277 writeSegmentsReplacingExif(os, pieces, null); 278 } 279 280 /** 281 * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a 282 * stream. 283 * 284 * <p>Note that this uses the "Lossless" approach - in order to preserve data 285 * embedded in the EXIF segment that it can't parse (such as Maker Notes), 286 * this algorithm avoids overwriting any part of the original segment that 287 * it couldn't parse. This can cause the EXIF segment to grow with each 288 * update, which is a serious issue, since all EXIF data must fit in a 289 * single APP1 segment of the Jpeg image.</p> 290 * 291 * @param src 292 * Image file. 293 * @param os 294 * OutputStream to write the image to. 295 * @param outputSet 296 * TiffOutputSet containing the EXIF data to write. 297 * @throws ImageReadException if it fails to read the JFIF segments 298 * @throws IOException if it fails to read the image data 299 * @throws ImageWriteException if it fails to write the updated data 300 */ 301 public void updateExifMetadataLossless(final File src, final OutputStream os, 302 final TiffOutputSet outputSet) throws ImageReadException, IOException, 303 ImageWriteException { 304 final ByteSource byteSource = new ByteSourceFile(src); 305 updateExifMetadataLossless(byteSource, os, outputSet); 306 } 307 308 /** 309 * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a 310 * stream. 311 * 312 * <p>Note that this uses the "Lossless" approach - in order to preserve data 313 * embedded in the EXIF segment that it can't parse (such as Maker Notes), 314 * this algorithm avoids overwriting any part of the original segment that 315 * it couldn't parse. This can cause the EXIF segment to grow with each 316 * update, which is a serious issue, since all EXIF data must fit in a 317 * single APP1 segment of the Jpeg image.</p> 318 * 319 * @param src 320 * Byte array containing Jpeg image data. 321 * @param os 322 * OutputStream to write the image to. 323 * @param outputSet 324 * TiffOutputSet containing the EXIF data to write. 325 * @throws ImageReadException if it fails to read the JFIF segments 326 * @throws IOException if it fails to read the image data 327 * @throws ImageWriteException if it fails to write the updated data 328 */ 329 public void updateExifMetadataLossless(final byte[] src, final OutputStream os, 330 final TiffOutputSet outputSet) throws ImageReadException, IOException, 331 ImageWriteException { 332 final ByteSource byteSource = new ByteSourceArray(src); 333 updateExifMetadataLossless(byteSource, os, outputSet); 334 } 335 336 /** 337 * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a 338 * stream. 339 * 340 * <p>Note that this uses the "Lossless" approach - in order to preserve data 341 * embedded in the EXIF segment that it can't parse (such as Maker Notes), 342 * this algorithm avoids overwriting any part of the original segment that 343 * it couldn't parse. This can cause the EXIF segment to grow with each 344 * update, which is a serious issue, since all EXIF data must fit in a 345 * single APP1 segment of the Jpeg image.</p> 346 * 347 * @param src 348 * InputStream containing Jpeg image data. 349 * @param os 350 * OutputStream to write the image to. 351 * @param outputSet 352 * TiffOutputSet containing the EXIF data to write. 353 * @throws ImageReadException if it fails to read the JFIF segments 354 * @throws IOException if it fails to read the image data 355 * @throws ImageWriteException if it fails to write the updated data 356 */ 357 public void updateExifMetadataLossless(final InputStream src, final OutputStream os, 358 final TiffOutputSet outputSet) throws ImageReadException, IOException, 359 ImageWriteException { 360 final ByteSource byteSource = new ByteSourceInputStream(src, null); 361 updateExifMetadataLossless(byteSource, os, outputSet); 362 } 363 364 /** 365 * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a 366 * stream. 367 * 368 * <p>Note that this uses the "Lossless" approach - in order to preserve data 369 * embedded in the EXIF segment that it can't parse (such as Maker Notes), 370 * this algorithm avoids overwriting any part of the original segment that 371 * it couldn't parse. This can cause the EXIF segment to grow with each 372 * update, which is a serious issue, since all EXIF data must fit in a 373 * single APP1 segment of the Jpeg image.</p> 374 * 375 * @param byteSource 376 * ByteSource containing Jpeg image data. 377 * @param os 378 * OutputStream to write the image to. 379 * @param outputSet 380 * TiffOutputSet containing the EXIF data to write. 381 * @throws ImageReadException if it fails to read the JFIF segments 382 * @throws IOException if it fails to read the image data 383 * @throws ImageWriteException if it fails to write the updated data 384 */ 385 public void updateExifMetadataLossless(final ByteSource byteSource, 386 final OutputStream os, final TiffOutputSet outputSet) 387 throws ImageReadException, IOException, ImageWriteException { 388 // List outputDirectories = outputSet.getDirectories(); 389 final JFIFPieces jfifPieces = analyzeJFIF(byteSource); 390 final List<JFIFPiece> pieces = jfifPieces.pieces; 391 392 TiffImageWriterBase writer; 393 // Just use first APP1 segment for now. 394 // Multiple APP1 segments are rare and poorly supported. 395 if (!jfifPieces.exifPieces.isEmpty()) { 396 JFIFPieceSegment exifPiece = null; 397 exifPiece = (JFIFPieceSegment) jfifPieces.exifPieces.get(0); 398 399 byte[] exifBytes = exifPiece.segmentData; 400 exifBytes = remainingBytes("trimmed exif bytes", exifBytes, 6); 401 402 writer = new TiffImageWriterLossless(outputSet.byteOrder, exifBytes); 403 404 } else { 405 writer = new TiffImageWriterLossy(outputSet.byteOrder); 406 } 407 408 final boolean includeEXIFPrefix = true; 409 final byte[] newBytes = writeExifSegment(writer, outputSet, includeEXIFPrefix); 410 411 writeSegmentsReplacingExif(os, pieces, newBytes); 412 } 413 414 /** 415 * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a 416 * stream. 417 * 418 * <p>Note that this uses the "Lossy" approach - the algorithm overwrites the 419 * entire EXIF segment, ignoring the possibility that it may be discarding 420 * data it couldn't parse (such as Maker Notes).</p> 421 * 422 * @param src 423 * Byte array containing Jpeg image data. 424 * @param os 425 * OutputStream to write the image to. 426 * @param outputSet 427 * TiffOutputSet containing the EXIF data to write. 428 * @throws ImageReadException if it fails to read the JFIF segments 429 * @throws IOException if it fails to read the image data 430 * @throws ImageWriteException if it fails to write the updated data 431 */ 432 public void updateExifMetadataLossy(final byte[] src, final OutputStream os, 433 final TiffOutputSet outputSet) throws ImageReadException, IOException, 434 ImageWriteException { 435 final ByteSource byteSource = new ByteSourceArray(src); 436 updateExifMetadataLossy(byteSource, os, outputSet); 437 } 438 439 /** 440 * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a 441 * stream. 442 * 443 * <p>Note that this uses the "Lossy" approach - the algorithm overwrites the 444 * entire EXIF segment, ignoring the possibility that it may be discarding 445 * data it couldn't parse (such as Maker Notes).</p> 446 * 447 * @param src 448 * InputStream containing Jpeg image data. 449 * @param os 450 * OutputStream to write the image to. 451 * @param outputSet 452 * TiffOutputSet containing the EXIF data to write. 453 * @throws ImageReadException if it fails to read the JFIF segments 454 * @throws IOException if it fails to read the image data 455 * @throws ImageWriteException if it fails to write the updated data 456 */ 457 public void updateExifMetadataLossy(final InputStream src, final OutputStream os, 458 final TiffOutputSet outputSet) throws ImageReadException, IOException, 459 ImageWriteException { 460 final ByteSource byteSource = new ByteSourceInputStream(src, null); 461 updateExifMetadataLossy(byteSource, os, outputSet); 462 } 463 464 /** 465 * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a 466 * stream. 467 * 468 * <p>Note that this uses the "Lossy" approach - the algorithm overwrites the 469 * entire EXIF segment, ignoring the possibility that it may be discarding 470 * data it couldn't parse (such as Maker Notes).</p> 471 * 472 * @param src 473 * Image file. 474 * @param os 475 * OutputStream to write the image to. 476 * @param outputSet 477 * TiffOutputSet containing the EXIF data to write. 478 * @throws ImageReadException if it fails to read the JFIF segments 479 * @throws IOException if it fails to read the image data 480 * @throws ImageWriteException if it fails to write the updated data 481 */ 482 public void updateExifMetadataLossy(final File src, final OutputStream os, 483 final TiffOutputSet outputSet) throws ImageReadException, IOException, 484 ImageWriteException { 485 final ByteSource byteSource = new ByteSourceFile(src); 486 updateExifMetadataLossy(byteSource, os, outputSet); 487 } 488 489 /** 490 * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a 491 * stream. 492 * 493 * <p>Note that this uses the "Lossy" approach - the algorithm overwrites the 494 * entire EXIF segment, ignoring the possibility that it may be discarding 495 * data it couldn't parse (such as Maker Notes).</p> 496 * 497 * @param byteSource 498 * ByteSource containing Jpeg image data. 499 * @param os 500 * OutputStream to write the image to. 501 * @param outputSet 502 * TiffOutputSet containing the EXIF data to write. 503 * @throws ImageReadException if it fails to read the JFIF segments 504 * @throws IOException if it fails to read the image data 505 * @throws ImageWriteException if it fails to write the updated data 506 */ 507 public void updateExifMetadataLossy(final ByteSource byteSource, final OutputStream os, 508 final TiffOutputSet outputSet) throws ImageReadException, IOException, 509 ImageWriteException { 510 final JFIFPieces jfifPieces = analyzeJFIF(byteSource); 511 final List<JFIFPiece> pieces = jfifPieces.pieces; 512 513 final TiffImageWriterBase writer = new TiffImageWriterLossy( 514 outputSet.byteOrder); 515 516 final boolean includeEXIFPrefix = true; 517 final byte[] newBytes = writeExifSegment(writer, outputSet, includeEXIFPrefix); 518 519 writeSegmentsReplacingExif(os, pieces, newBytes); 520 } 521 522 private void writeSegmentsReplacingExif(final OutputStream outputStream, 523 final List<JFIFPiece> segments, final byte[] newBytes) 524 throws ImageWriteException, IOException { 525 526 try (DataOutputStream os = new DataOutputStream(outputStream)) { 527 JpegConstants.SOI.writeTo(os); 528 529 boolean hasExif = false; 530 531 for (final JFIFPiece piece : segments) { 532 if (piece instanceof JFIFPieceSegmentExif) { 533 hasExif = true; 534 break; 535 } 536 } 537 538 if (!hasExif && newBytes != null) { 539 final byte[] markerBytes = ByteConversions.toBytes((short) JpegConstants.JPEG_APP1_MARKER, getByteOrder()); 540 if (newBytes.length > 0xffff) { 541 throw new ExifOverflowException( 542 "APP1 Segment is too long: " + newBytes.length); 543 } 544 final int markerLength = newBytes.length + 2; 545 final byte[] markerLengthBytes = ByteConversions.toBytes((short) markerLength, getByteOrder()); 546 547 int index = 0; 548 final JFIFPieceSegment firstSegment = (JFIFPieceSegment) segments.get(index); 549 if (firstSegment.marker == JpegConstants.JFIF_MARKER) { 550 index = 1; 551 } 552 segments.add(index, new JFIFPieceSegmentExif(JpegConstants.JPEG_APP1_MARKER, 553 markerBytes, markerLengthBytes, newBytes)); 554 } 555 556 boolean APP1Written = false; 557 558 for (final JFIFPiece piece : segments) { 559 if (piece instanceof JFIFPieceSegmentExif) { 560 // only replace first APP1 segment; skips others. 561 if (APP1Written) { 562 continue; 563 } 564 APP1Written = true; 565 566 if (newBytes == null) { 567 continue; 568 } 569 570 final byte[] markerBytes = ByteConversions.toBytes((short) JpegConstants.JPEG_APP1_MARKER, getByteOrder()); 571 if (newBytes.length > 0xffff) { 572 throw new ExifOverflowException( 573 "APP1 Segment is too long: " + newBytes.length); 574 } 575 final int markerLength = newBytes.length + 2; 576 final byte[] markerLengthBytes = ByteConversions.toBytes((short) markerLength, getByteOrder()); 577 578 os.write(markerBytes); 579 os.write(markerLengthBytes); 580 os.write(newBytes); 581 } else { 582 piece.write(os); 583 } 584 } 585 } 586 } 587 588 public static class ExifOverflowException extends ImageWriteException { 589 private static final long serialVersionUID = 1401484357224931218L; 590 591 public ExifOverflowException(final String message) { 592 super(message); 593 } 594 } 595 596 private byte[] writeExifSegment(final TiffImageWriterBase writer, 597 final TiffOutputSet outputSet, final boolean includeEXIFPrefix) 598 throws IOException, ImageWriteException { 599 final ByteArrayOutputStream os = new ByteArrayOutputStream(); 600 601 if (includeEXIFPrefix) { 602 JpegConstants.EXIF_IDENTIFIER_CODE.writeTo(os); 603 os.write(0); 604 os.write(0); 605 } 606 607 writer.write(os, outputSet); 608 609 return os.toByteArray(); 610 } 611 612}