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