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.ImagingException; 033import org.apache.commons.imaging.ImagingOverflowException; 034import org.apache.commons.imaging.bytesource.ByteSource; 035import org.apache.commons.imaging.common.BinaryFileParser; 036import org.apache.commons.imaging.common.ByteConversions; 037import org.apache.commons.imaging.formats.jpeg.JpegConstants; 038import org.apache.commons.imaging.formats.jpeg.JpegUtils; 039import org.apache.commons.imaging.formats.tiff.write.AbstractTiffImageWriter; 040import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossless; 041import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy; 042import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet; 043 044/** 045 * Interface for Exif write/update/remove functionality for Jpeg/JFIF images. 046 * 047 * <p> 048 * See the source of the ExifMetadataUpdateExample class for example usage. 049 * </p> 050 * 051 * @see <a href= 052 * "https://svn.apache.org/repos/asf/commons/proper/imaging/trunk/src/test/java/org/apache/commons/imaging/examples/WriteExifMetadataExample.java"> 053 * org.apache.commons.imaging.examples.WriteExifMetadataExample</a> 054 */ 055public class ExifRewriter extends BinaryFileParser { 056 057 private abstract static class JFIFPiece { 058 protected abstract void write(OutputStream os) throws IOException; 059 } 060 061 private static final class JFIFPieceImageData extends JFIFPiece { 062 public final byte[] markerBytes; 063 public final byte[] imageData; 064 065 JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) { 066 this.markerBytes = markerBytes; 067 this.imageData = imageData; 068 } 069 070 @Override 071 protected void write(final OutputStream os) throws IOException { 072 os.write(markerBytes); 073 os.write(imageData); 074 } 075 } 076 077 private static final class JFIFPieces { 078 public final List<JFIFPiece> pieces; 079 public final List<JFIFPiece> exifPieces; 080 081 JFIFPieces(final List<JFIFPiece> pieces, final List<JFIFPiece> exifPieces) { 082 this.pieces = pieces; 083 this.exifPieces = exifPieces; 084 } 085 086 } 087 088 private static class JFIFPieceSegment extends JFIFPiece { 089 public final int marker; 090 public final byte[] markerBytes; 091 public final byte[] markerLengthBytes; 092 public final byte[] segmentData; 093 094 JFIFPieceSegment(final int marker, final byte[] markerBytes, final byte[] markerLengthBytes, final byte[] segmentData) { 095 this.marker = marker; 096 this.markerBytes = markerBytes; 097 this.markerLengthBytes = markerLengthBytes; 098 this.segmentData = segmentData; 099 } 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 * Constructs a new instance. to guess whether a file contains an image based on its file extension. 118 */ 119 public ExifRewriter() { 120 this(ByteOrder.BIG_ENDIAN); 121 } 122 123 /** 124 * Constructs a new instance. 125 * <p> 126 * 127 * @param byteOrder byte order of EXIF segment. 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 // return false to exit before reading image data. 139 @Override 140 public boolean beginSos() { 141 return true; 142 } 143 144 // return false to exit traversal. 145 @Override 146 public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes, 147 final byte[] segmentData) throws 148 // ImageWriteException, 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 // GenericSegment exifSegment = exifSegmentArray[0]; 169 // if (exifSegments.size() < 1) 170 // { 171 // // TODO: add support for adding, not just replacing. 172 // throw new ImageReadException("No APP1 EXIF segment found."); 173 // } 174 175 return new JFIFPieces(pieces, exifPieces); 176 } 177 178 /** 179 * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream. 180 * 181 * @param src Byte array containing JPEG image data. 182 * @param os OutputStream to write the image to. 183 * @throws ImagingException if it fails to read the JFIF segments 184 * @throws IOException if it fails to read the image data 185 * @throws ImagingException if it fails to write the updated data 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 * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream. 194 * 195 * @param byteSource ByteSource containing JPEG image data. 196 * @param os OutputStream to write the image to. 197 * @throws ImagingException if it fails to read the JFIF segments 198 * @throws IOException if it fails to read the image data 199 * @throws ImagingException if it fails to write the updated data 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 // Debug.debug("pieces", pieces); 206 207 // pieces.removeAll(jfifPieces.exifSegments); 208 209 // Debug.debug("pieces", pieces); 210 211 writeSegmentsReplacingExif(os, pieces, null); 212 } 213 214 /** 215 * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream. 216 * <p> 217 * 218 * @param src Image file. 219 * @param os OutputStream to write the image to. 220 * 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 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}