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; 020 021import java.io.ByteArrayOutputStream; 022import java.io.DataOutputStream; 023import java.io.File; 024import java.io.IOException; 025import java.io.InputStream; 026import java.io.OutputStream; 027import java.nio.ByteOrder; 028import java.util.ArrayList; 029import java.util.List; 030 031import org.apache.commons.imaging.ImagingException; 032import org.apache.commons.imaging.ImagingOverflowException; 033import org.apache.commons.imaging.bytesource.ByteSource; 034import org.apache.commons.imaging.common.BinaryFileParser; 035import org.apache.commons.imaging.common.ByteConversions; 036import org.apache.commons.imaging.formats.jpeg.JpegConstants; 037import org.apache.commons.imaging.formats.jpeg.JpegUtils; 038import org.apache.commons.imaging.formats.tiff.write.AbstractTiffImageWriter; 039import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossless; 040import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy; 041import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet; 042 043/** 044 * Interface for Exif write/update/remove functionality for Jpeg/JFIF images. 045 * 046 * <p> 047 * See the source of the ExifMetadataUpdateExample class for example usage. 048 * </p> 049 * 050 * @see <a href= 051 * "https://svn.apache.org/repos/asf/commons/proper/imaging/trunk/src/test/java/org/apache/commons/imaging/examples/WriteExifMetadataExample.java"> 052 * org.apache.commons.imaging.examples.WriteExifMetadataExample</a> 053 */ 054public class ExifRewriter extends BinaryFileParser { 055 056 private abstract static class JFIFPiece { 057 protected abstract void write(OutputStream os) throws IOException; 058 } 059 060 private static final class JFIFPieceImageData extends JFIFPiece { 061 public final byte[] markerBytes; 062 public final byte[] imageData; 063 064 JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) { 065 this.markerBytes = markerBytes; 066 this.imageData = imageData; 067 } 068 069 @Override 070 protected void write(final OutputStream os) throws IOException { 071 os.write(markerBytes); 072 os.write(imageData); 073 } 074 } 075 076 private static final class JFIFPieces { 077 public final List<JFIFPiece> pieces; 078 public final List<JFIFPiece> exifPieces; 079 080 JFIFPieces(final List<JFIFPiece> pieces, final List<JFIFPiece> exifPieces) { 081 this.pieces = pieces; 082 this.exifPieces = exifPieces; 083 } 084 085 } 086 087 private static class JFIFPieceSegment extends JFIFPiece { 088 public final int marker; 089 public final byte[] markerBytes; 090 public final byte[] markerLengthBytes; 091 public final byte[] segmentData; 092 093 JFIFPieceSegment(final int marker, final byte[] markerBytes, final byte[] markerLengthBytes, final byte[] segmentData) { 094 this.marker = marker; 095 this.markerBytes = markerBytes; 096 this.markerLengthBytes = markerLengthBytes; 097 this.segmentData = segmentData; 098 } 099 100 @Override 101 protected void write(final OutputStream os) throws IOException { 102 os.write(markerBytes); 103 os.write(markerLengthBytes); 104 os.write(segmentData); 105 } 106 } 107 108 private static final class JFIFPieceSegmentExif extends JFIFPieceSegment { 109 110 JFIFPieceSegmentExif(final int marker, final byte[] markerBytes, final byte[] markerLengthBytes, final byte[] segmentData) { 111 super(marker, markerBytes, markerLengthBytes, segmentData); 112 } 113 } 114 115 /** 116 * Constructs a new instance with the default, big-endian, byte order. 117 * <p> 118 * Whether a file contains an image based on its file extension. 119 * </p> 120 */ 121 public ExifRewriter() { 122 this(ByteOrder.BIG_ENDIAN); 123 } 124 125 /** 126 * Constructs a new instance. 127 * 128 * @param byteOrder byte order of EXIF segment. 129 */ 130 public ExifRewriter(final ByteOrder byteOrder) { 131 super(byteOrder); 132 } 133 134 private JFIFPieces analyzeJfif(final ByteSource byteSource) throws ImagingException, IOException { 135 final List<JFIFPiece> pieces = new ArrayList<>(); 136 final List<JFIFPiece> exifPieces = new ArrayList<>(); 137 138 final JpegUtils.Visitor visitor = new JpegUtils.Visitor() { 139 // return false to exit before reading image data. 140 @Override 141 public boolean beginSos() { 142 return true; 143 } 144 145 // return false to exit traversal. 146 @Override 147 public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes, 148 final byte[] segmentData) throws 149 // ImageWriteException, 150 ImagingException, IOException { 151 if (marker != JpegConstants.JPEG_APP1_MARKER || !JpegConstants.EXIF_IDENTIFIER_CODE.isStartOf(segmentData)) { 152 pieces.add(new JFIFPieceSegment(marker, markerBytes, markerLengthBytes, segmentData)); 153 } else { 154 final JFIFPiece piece = new JFIFPieceSegmentExif(marker, markerBytes, markerLengthBytes, segmentData); 155 pieces.add(piece); 156 exifPieces.add(piece); 157 } 158 return true; 159 } 160 161 @Override 162 public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) { 163 pieces.add(new JFIFPieceImageData(markerBytes, imageData)); 164 } 165 }; 166 167 new JpegUtils().traverseJfif(byteSource, visitor); 168 169 // GenericSegment exifSegment = exifSegmentArray[0]; 170 // if (exifSegments.size() < 1) 171 // { 172 // // TODO: add support for adding, not just replacing. 173 // throw new ImageReadException("No APP1 EXIF segment found."); 174 // } 175 176 return new JFIFPieces(pieces, exifPieces); 177 } 178 179 /** 180 * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream. 181 * 182 * @param src Byte array containing JPEG image data. 183 * @param os OutputStream to write the image to. 184 * @throws ImagingException if it fails to read the JFIF segments 185 * @throws IOException if it fails to read the image data 186 * @throws ImagingException if it fails to write the updated data 187 */ 188 public void removeExifMetadata(final byte[] src, final OutputStream os) throws ImagingException, IOException, ImagingException { 189 final ByteSource byteSource = ByteSource.array(src); 190 removeExifMetadata(byteSource, os); 191 } 192 193 /** 194 * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream. 195 * 196 * @param byteSource ByteSource containing JPEG image data. 197 * @param os OutputStream to write the image to. 198 * @throws ImagingException if it fails to read the JFIF segments 199 * @throws IOException if it fails to read the image data 200 * @throws ImagingException if it fails to write the updated data 201 */ 202 public void removeExifMetadata(final ByteSource byteSource, final OutputStream os) throws ImagingException, IOException, ImagingException { 203 final JFIFPieces jfifPieces = analyzeJfif(byteSource); 204 final List<JFIFPiece> pieces = jfifPieces.pieces; 205 206 // Debug.debug("pieces", pieces); 207 208 // pieces.removeAll(jfifPieces.exifSegments); 209 210 // Debug.debug("pieces", pieces); 211 212 writeSegmentsReplacingExif(os, pieces, null); 213 } 214 215 /** 216 * Reads a JPEG image, removes all EXIF metadata (by removing the APP1 segment), and writes the result to a stream. 217 * <p> 218 * 219 * @param src Image file. 220 * @param os OutputStream to write the image to. 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 final 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}