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