From 8e5d971a6fe98777b511065772a99ac9bdcac9f5 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 21 Jan 2025 14:31:33 +0100 Subject: [PATCH] ExifInterface upgrade --- .../media/ExifInterfaceFork.java | 303 ++++++++++++------ 1 file changed, 213 insertions(+), 90 deletions(-) diff --git a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java index 07bab0f4c..d3e422bb0 100644 --- a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java +++ b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java @@ -26,6 +26,7 @@ import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith; import static java.lang.annotation.ElementType.TYPE_USE; import static java.nio.ByteOrder.BIG_ENDIAN; import static java.nio.ByteOrder.LITTLE_ENDIAN; +import static java.nio.charset.StandardCharsets.UTF_8; import android.annotation.SuppressLint; import android.content.res.AssetManager; @@ -54,6 +55,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInput; import java.io.DataInputStream; +import java.io.DataOutputStream; import java.io.EOFException; import java.io.File; import java.io.FileDescriptor; @@ -89,7 +91,7 @@ import java.util.regex.Pattern; import java.util.zip.CRC32; /* - * Forked from 'androidx.exifinterface:exifinterface:1.4.0-alpha01' on 2024/11/17 + * Forked from 'androidx.exifinterface:exifinterface:1.4.0-beta01' on 2025/01/21 * Named differently to let ExifInterface be loaded as subdependency. * cf https://github.com/androidx/androidx/tree/androidx-main/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media */ @@ -190,6 +192,7 @@ public class ExifInterfaceFork { *
  • Default = None
  • * *

    + * * @see #DATA_UNCOMPRESSED * @see #DATA_JPEG */ @@ -205,6 +208,7 @@ public class ExifInterfaceFork { *

  • Default = None
  • * *

    + * * @see #PHOTOMETRIC_INTERPRETATION_RGB * @see #PHOTOMETRIC_INTERPRETATION_YCBCR */ @@ -219,6 +223,7 @@ public class ExifInterfaceFork { *

  • Default = {@link #ORIENTATION_NORMAL}
  • * *

    + * * @see #ORIENTATION_UNDEFINED * @see #ORIENTATION_NORMAL * @see #ORIENTATION_FLIP_HORIZONTAL @@ -254,6 +259,7 @@ public class ExifInterfaceFork { *

  • Count = 1
  • * *

    + * * @see #FORMAT_CHUNKY * @see #FORMAT_PLANAR */ @@ -294,6 +300,7 @@ public class ExifInterfaceFork { *

  • Default = {@link #Y_CB_CR_POSITIONING_CENTERED}
  • * *

    + * * @see #Y_CB_CR_POSITIONING_CENTERED * @see #Y_CB_CR_POSITIONING_CO_SITED */ @@ -309,6 +316,7 @@ public class ExifInterfaceFork { *

  • Default = 72
  • * *

    + * * @see #TAG_Y_RESOLUTION * @see #TAG_RESOLUTION_UNIT */ @@ -324,6 +332,7 @@ public class ExifInterfaceFork { *

  • Default = 72
  • * *

    + * * @see #TAG_X_RESOLUTION * @see #TAG_RESOLUTION_UNIT */ @@ -340,6 +349,7 @@ public class ExifInterfaceFork { *

  • Default = {@link #RESOLUTION_UNIT_INCHES}
  • * *

    + * * @see #RESOLUTION_UNIT_INCHES * @see #RESOLUTION_UNIT_CENTIMETERS * @see #TAG_X_RESOLUTION @@ -365,6 +375,7 @@ public class ExifInterfaceFork { *

    StripsPerImage = floor(({@link #TAG_IMAGE_LENGTH} + {@link #TAG_ROWS_PER_STRIP} - 1) * / {@link #TAG_ROWS_PER_STRIP})

    *

    + * * @see #TAG_ROWS_PER_STRIP * @see #TAG_STRIP_BYTE_COUNTS */ @@ -381,6 +392,7 @@ public class ExifInterfaceFork { *

  • Default = None
  • * *

    + * * @see #TAG_STRIP_OFFSETS * @see #TAG_STRIP_BYTE_COUNTS */ @@ -656,6 +668,7 @@ public class ExifInterfaceFork { *

  • Count = 1
  • * *

    + * * @see #COLOR_SPACE_S_RGB * @see #COLOR_SPACE_UNCALIBRATED */ @@ -962,6 +975,7 @@ public class ExifInterfaceFork { *

  • Default = {@link #EXPOSURE_PROGRAM_NOT_DEFINED}
  • * *

    + * * @see #EXPOSURE_PROGRAM_NOT_DEFINED * @see #EXPOSURE_PROGRAM_MANUAL * @see #EXPOSURE_PROGRAM_NORMAL @@ -1031,6 +1045,7 @@ public class ExifInterfaceFork { *

  • Default = None
  • * *

    + * * @see #SENSITIVITY_TYPE_UNKNOWN * @see #SENSITIVITY_TYPE_SOS * @see #SENSITIVITY_TYPE_REI @@ -1197,6 +1212,7 @@ public class ExifInterfaceFork { *

  • Default = {@link #METERING_MODE_UNKNOWN}
  • * *

    + * * @see #METERING_MODE_UNKNOWN * @see #METERING_MODE_AVERAGE * @see #METERING_MODE_CENTER_WEIGHT_AVERAGE @@ -1217,6 +1233,7 @@ public class ExifInterfaceFork { *

  • Default = {@link #LIGHT_SOURCE_UNKNOWN}
  • * *

    + * * @see #LIGHT_SOURCE_UNKNOWN * @see #LIGHT_SOURCE_DAYLIGHT * @see #LIGHT_SOURCE_FLUORESCENT @@ -1253,6 +1270,7 @@ public class ExifInterfaceFork { *

  • Count = 1
  • * *

    + * * @see #FLAG_FLASH_FIRED * @see #FLAG_FLASH_RETURN_LIGHT_NOT_DETECTED * @see #FLAG_FLASH_RETURN_LIGHT_DETECTED @@ -1365,6 +1383,7 @@ public class ExifInterfaceFork { *

  • Default = {@link #RESOLUTION_UNIT_INCHES}
  • * *

    + * * @see #TAG_RESOLUTION_UNIT * @see #RESOLUTION_UNIT_INCHES * @see #RESOLUTION_UNIT_CENTIMETERS @@ -1407,6 +1426,7 @@ public class ExifInterfaceFork { *

  • Default = None
  • * *

    + * * @see #SENSOR_TYPE_NOT_DEFINED * @see #SENSOR_TYPE_ONE_CHIP * @see #SENSOR_TYPE_TWO_CHIP @@ -1427,6 +1447,7 @@ public class ExifInterfaceFork { *

  • Default = {@link #FILE_SOURCE_DSC}
  • * *

    + * * @see #FILE_SOURCE_OTHER * @see #FILE_SOURCE_TRANSPARENT_SCANNER * @see #FILE_SOURCE_REFLEX_SCANNER @@ -1444,6 +1465,7 @@ public class ExifInterfaceFork { *

  • Default = 1
  • * *

    + * * @see #SCENE_TYPE_DIRECTLY_PHOTOGRAPHED */ public static final String TAG_SCENE_TYPE = "SceneType"; @@ -1457,6 +1479,7 @@ public class ExifInterfaceFork { *

  • Default = None
  • * *

    + * * @see #TAG_SENSING_METHOD * @see #SENSOR_TYPE_ONE_CHIP */ @@ -1473,6 +1496,7 @@ public class ExifInterfaceFork { *

  • Default = {@link #RENDERED_PROCESS_NORMAL}
  • * *

    + * * @see #RENDERED_PROCESS_NORMAL * @see #RENDERED_PROCESS_CUSTOM */ @@ -1489,6 +1513,7 @@ public class ExifInterfaceFork { *

  • Default = None
  • * *

    + * * @see #EXPOSURE_MODE_AUTO * @see #EXPOSURE_MODE_MANUAL * @see #EXPOSURE_MODE_AUTO_BRACKET @@ -1504,6 +1529,7 @@ public class ExifInterfaceFork { *

  • Default = None
  • * *

    + * * @see #WHITEBALANCE_AUTO * @see #WHITEBALANCE_MANUAL */ @@ -1553,6 +1579,7 @@ public class ExifInterfaceFork { *

  • Default = 0
  • * *

    + * * @see #SCENE_CAPTURE_TYPE_STANDARD * @see #SCENE_CAPTURE_TYPE_LANDSCAPE * @see #SCENE_CAPTURE_TYPE_PORTRAIT @@ -1569,6 +1596,7 @@ public class ExifInterfaceFork { *

  • Default = None
  • * *

    + * * @see #GAIN_CONTROL_NONE * @see #GAIN_CONTROL_LOW_GAIN_UP * @see #GAIN_CONTROL_HIGH_GAIN_UP @@ -1587,6 +1615,7 @@ public class ExifInterfaceFork { *

  • Default = {@link #CONTRAST_NORMAL}
  • * *

    + * * @see #CONTRAST_NORMAL * @see #CONTRAST_SOFT * @see #CONTRAST_HARD @@ -1603,6 +1632,7 @@ public class ExifInterfaceFork { *

  • Default = {@link #SATURATION_NORMAL}
  • * *

    + * * @see #SATURATION_NORMAL * @see #SATURATION_LOW * @see #SATURATION_HIGH @@ -1619,6 +1649,7 @@ public class ExifInterfaceFork { *

  • Default = {@link #SHARPNESS_NORMAL}
  • * *

    + * * @see #SHARPNESS_NORMAL * @see #SHARPNESS_SOFT * @see #SHARPNESS_HARD @@ -1646,6 +1677,7 @@ public class ExifInterfaceFork { *

  • Default = None
  • * *

    + * * @see #SUBJECT_DISTANCE_RANGE_UNKNOWN * @see #SUBJECT_DISTANCE_RANGE_MACRO * @see #SUBJECT_DISTANCE_RANGE_CLOSE_VIEW @@ -1675,6 +1707,7 @@ public class ExifInterfaceFork { *

  • Default = None
  • * *

    + * * @deprecated Use {@link #TAG_CAMERA_OWNER_NAME} instead. */ @Deprecated @@ -1780,6 +1813,7 @@ public class ExifInterfaceFork { *

  • Default = None
  • * *

    + * * @see #LATITUDE_NORTH * @see #LATITUDE_SOUTH */ @@ -1809,6 +1843,7 @@ public class ExifInterfaceFork { *

  • Default = None
  • * *

    + * * @see #LONGITUDE_EAST * @see #LONGITUDE_WEST */ @@ -1841,6 +1876,7 @@ public class ExifInterfaceFork { *

  • Default = 0
  • * *

    + * * @see #ALTITUDE_ABOVE_SEA_LEVEL * @see #ALTITUDE_BELOW_SEA_LEVEL */ @@ -1899,6 +1935,7 @@ public class ExifInterfaceFork { *

  • Default = None
  • * *

    + * * @see #GPS_MEASUREMENT_IN_PROGRESS * @see #GPS_MEASUREMENT_INTERRUPTED */ @@ -1915,6 +1952,7 @@ public class ExifInterfaceFork { *

  • Default = None
  • * *

    + * * @see #GPS_MEASUREMENT_2D * @see #GPS_MEASUREMENT_3D */ @@ -1941,6 +1979,7 @@ public class ExifInterfaceFork { *

  • Default = {@link #GPS_SPEED_KILOMETERS_PER_HOUR}
  • * *

    + * * @see #GPS_SPEED_KILOMETERS_PER_HOUR * @see #GPS_SPEED_MILES_PER_HOUR * @see #GPS_SPEED_KNOTS @@ -1968,6 +2007,7 @@ public class ExifInterfaceFork { *

  • Default = {@link #GPS_DIRECTION_TRUE}
  • * *

    + * * @see #GPS_DIRECTION_TRUE * @see #GPS_DIRECTION_MAGNETIC */ @@ -1994,6 +2034,7 @@ public class ExifInterfaceFork { *

  • Default = {@link #GPS_DIRECTION_TRUE}
  • * *

    + * * @see #GPS_DIRECTION_TRUE * @see #GPS_DIRECTION_MAGNETIC */ @@ -2032,6 +2073,7 @@ public class ExifInterfaceFork { *

  • Default = None
  • * *

    + * * @see #LATITUDE_NORTH * @see #LATITUDE_SOUTH */ @@ -2061,6 +2103,7 @@ public class ExifInterfaceFork { *

  • Default = None
  • * *

    + * * @see #LONGITUDE_EAST * @see #LONGITUDE_WEST */ @@ -2090,6 +2133,7 @@ public class ExifInterfaceFork { *

  • Default = {@link #GPS_DIRECTION_TRUE}
  • * *

    + * * @see #GPS_DIRECTION_TRUE * @see #GPS_DIRECTION_MAGNETIC */ @@ -2116,6 +2160,7 @@ public class ExifInterfaceFork { *

  • Default = {@link #GPS_DISTANCE_KILOMETERS}
  • * *

    + * * @see #GPS_DISTANCE_KILOMETERS * @see #GPS_DISTANCE_MILES * @see #GPS_DISTANCE_NAUTICAL_MILES @@ -2177,6 +2222,7 @@ public class ExifInterfaceFork { *

  • Default = None
  • * *

    + * * @see #GPS_MEASUREMENT_NO_DIFFERENTIAL * @see #GPS_MEASUREMENT_DIFFERENTIAL_CORRECTED */ @@ -3132,11 +3178,18 @@ public class ExifInterfaceFork { // See "Extensions to the PNG 1.2 Specification, Version 1.5.0", // 3.7. eXIf Exchangeable Image File (Exif) Profile private static final int PNG_CHUNK_TYPE_EXIF = 'e' << 24 | 'X' << 16 | 'I' << 8 | 'f'; + // See "XMP Specification Part 3: Storage in Files" section 1.1.5 + private static final int PNG_CHUNK_TYPE_ITXT = 'i' << 24 | 'T' << 16 | 'X' << 8 | 't'; private static final int PNG_CHUNK_TYPE_IHDR = 'I' << 24 | 'H' << 16 | 'D' << 8 | 'R'; private static final int PNG_CHUNK_TYPE_IEND = 'I' << 24 | 'E' << 16 | 'N' << 8 | 'D'; - private static final int PNG_CHUNK_TYPE_BYTE_LENGTH = 4; private static final int PNG_CHUNK_CRC_BYTE_LENGTH = 4; + /** + * The keyword and 5 null bytes defined by XMP spec part 3 table 9 (section 1.1.5). + */ + @VisibleForTesting + static final byte[] PNG_ITXT_XMP_KEYWORD = "XML:com.adobe.xmp\0\0\0\0\0".getBytes(UTF_8); + // See https://developers.google.com/speed/webp/docs/riff_container, Section "WebP File Header" private static final byte[] WEBP_SIGNATURE_1 = new byte[]{'R', 'I', 'F', 'F'}; private static final byte[] WEBP_SIGNATURE_2 = new byte[]{'W', 'E', 'B', 'P'}; @@ -4069,20 +4122,33 @@ public class ExifInterfaceFork { // Used to indicate offset from the start of the original input stream to EXIF data private int mOffsetToExifData; private int mOrfMakerNoteOffset; + + /** + * The position of the thumbnail within the Exif data (from {@link #mOffsetToExifData}). + */ private int mOrfThumbnailOffset; + private int mOrfThumbnailLength; private boolean mModified; /** * XMP data can occur as either part of the TIFF/Exif data (tag number 700), or as a separate - * section of the file (e.g. a separate APP1 segment in JPEG). XMP read from within the - * TIFF/Exif data is stored in {@link #mAttributes}, while XMP read from a separate section is - * here. If both are present, the disambiguation rules vary per file format, see - * {@link #getXmpHandlingForImageType(int)}. + * section of the file (e.g. a separate APP1 segment in JPEG, or an iTXt chunk in PNG). XMP read + * from within the TIFF/Exif data is stored in {@link #mAttributes}, while XMP read from a + * separate section is here. If both are present, the disambiguation rules vary per file format, + * see {@link #getXmpHandlingForImageType(int)}. */ @Nullable private ExifAttribute mXmpFromSeparateMarker; + /** + * True if the file on disk contains XMP in a separate section. + * + *

    This means the file the instance was loaded with, or the file created by the last call to + * {@link #saveAttributes()}. + */ + private boolean mFileOnDiskContainsSeparateXmpMarker; + // Pattern to check non zero timestamp private static final Pattern NON_ZERO_TIME_PATTERN = Pattern.compile(".*[1-9].*"); // Pattern to check gps timestamp @@ -4300,6 +4366,7 @@ public class ExifInterfaceFork { return XMP_HANDLING_PREFER_TIFF_700_IF_PRESENT; case IMAGE_TYPE_AVIF: case IMAGE_TYPE_HEIC: + case IMAGE_TYPE_PNG: // RAF stores XMP/Exif in JPEG, but we have no documented backwards-compat obligations // so we can implement the spec to store XMP in a separate APP1 segment. case IMAGE_TYPE_RAF: @@ -4309,10 +4376,8 @@ public class ExifInterfaceFork { case IMAGE_TYPE_PEF: case IMAGE_TYPE_RW2: case IMAGE_TYPE_UNKNOWN: - // PNG and WebP support a separate XMP chunk (so should be - // XMP_HANDLING_PREFER_SEPARATE), but ExifInterface doesn't currently read or write - // them. - case IMAGE_TYPE_PNG: + // WebP supports a separate XMP chunk (so should be XMP_HANDLING_PREFER_SEPARATE), but + // ExifInterface doesn't currently read or write it. case IMAGE_TYPE_WEBP: default: return XMP_HANDLING_TIFF_700_ONLY; @@ -5160,14 +5225,18 @@ public class ExifInterfaceFork { } /** - * Returns the offset and length of the requested tag inside the image file, - * or {@code null} if the tag is not contained. + * Returns the offset and length of the requested tag inside the image file, or {@code null} if + * the tag is not contained. * - * @return two-element array, the offset in the first value, and length in - * the second, or {@code null} if no tag was found. - * @throws IllegalStateException if {@link #saveAttributes()} has been - * called since the underlying file was initially parsed, since - * that means offsets may have changed. + *

    If the attribute has been modified with {@link #setAttribute(String, String)} but not yet + * written to disk with {@link #saveAttributes()}, the returned range will have the correct + * length for the modified value, but an offset of {@code -1} to indicate its position in the + * file isn't known. + * + * @return two-element array, the offset in the first value, and length in the second, or {@code + * null} if no tag was found. + * @throws IllegalStateException if {@link #saveAttributes()} has been called since the + * underlying file was initially parsed, since that means offsets may have changed. */ public long @Nullable [] getAttributeRange(@NonNull String tag) { if (tag == null) { @@ -5841,6 +5910,7 @@ public class ExifInterfaceFork { IDENTIFIER_XMP_APP1.length, bytes.length); mXmpFromSeparateMarker = new ExifAttribute(IFD_FORMAT_BYTE, value.length, offset, value); + mFileOnDiskContainsSeparateXmpMarker = true; } break; } @@ -6165,6 +6235,7 @@ public class ExifInterfaceFork { in.readFully(xmpBytes); mXmpFromSeparateMarker = new ExifAttribute(IFD_FORMAT_BYTE, xmpBytes.length, offset, xmpBytes); + mFileOnDiskContainsSeparateXmpMarker = true; } if (DEBUG) { @@ -6352,10 +6423,12 @@ public class ExifInterfaceFork { // See PNG (Portable Network Graphics) Specification, Version 1.2, // 3.2. Chunk layout try { - while (true) { + boolean foundExif = false; + boolean foundXmpItxt = false; + while (!foundExif || !foundXmpItxt) { int length = in.readInt(); - int type = in.readInt(); + int startOfNextChunk = in.position() + length + PNG_CHUNK_CRC_BYTE_LENGTH; // The first chunk must be the IHDR chunk if (in.position() - startPosition == 16 && type != PNG_CHUNK_TYPE_IHDR) { @@ -6367,7 +6440,7 @@ public class ExifInterfaceFork { if (type == PNG_CHUNK_TYPE_IEND) { // IEND marks the end of the image. break; - } else if (type == PNG_CHUNK_TYPE_EXIF) { + } else if (type == PNG_CHUNK_TYPE_EXIF && !foundExif) { // Save offset to EXIF data for handling thumbnail and attribute offsets. mOffsetToExifData = in.position() - startPosition; @@ -6388,20 +6461,40 @@ public class ExifInterfaceFork { updateCrcWithInt(crc, type); crc.update(data); if ((int) crc.getValue() != dataCrcValue) { - throw new IOException("Encountered invalid CRC value for PNG-EXIF chunk." - + "\n recorded CRC value: " + dataCrcValue + ", calculated CRC " - + "value: " + crc.getValue()); + throw new IOException( + "Encountered invalid CRC value for PNG-EXIF chunk." + + "\n recorded CRC value: " + + dataCrcValue + + ", calculated CRC " + + "value: " + + crc.getValue()); } readExifSegment(data, IFD_TYPE_PRIMARY); validateImages(); setThumbnailData(new ByteOrderedDataInputStream(data)); - break; - } else { - // Skip to next chunk - in.skipFully(length + PNG_CHUNK_CRC_BYTE_LENGTH); + foundExif = true; + } else if (type == PNG_CHUNK_TYPE_ITXT + && !foundXmpItxt + && length >= PNG_ITXT_XMP_KEYWORD.length) { + // Read the 17 byte keyword and 5 expected null bytes. + byte[] keyword = new byte[PNG_ITXT_XMP_KEYWORD.length]; + in.readFully(keyword); + if (Arrays.equals(keyword, PNG_ITXT_XMP_KEYWORD)) { + int xmpDataOffset = in.position() - startPosition; + int xmpLength = length - keyword.length; + byte[] xmpData = new byte[xmpLength]; + in.readFully(xmpData); + mXmpFromSeparateMarker = + new ExifAttribute( + IFD_FORMAT_BYTE, xmpLength, xmpDataOffset, xmpData); + foundXmpItxt = true; + } } + // Skip to next chunk + in.skipFully(startOfNextChunk - in.position()); } + mFileOnDiskContainsSeparateXmpMarker = foundXmpItxt; } catch (EOFException e) { // Should not reach here. Will only reach here if the file is corrupted or // does not follow the PNG specifications @@ -6464,9 +6557,8 @@ public class ExifInterfaceFork { // Exif data in WebP images (e.g. // https://github.com/ImageMagick/ImageMagick/issues/3140) if (startsWith(payload, IDENTIFIER_EXIF_APP1)) { - int adjustedChunkSize = chunkSize - IDENTIFIER_EXIF_APP1.length; payload = Arrays.copyOfRange(payload, IDENTIFIER_EXIF_APP1.length, - adjustedChunkSize); + payload.length); } // Save offset to EXIF data for handling thumbnail and attribute offsets. @@ -6522,7 +6614,7 @@ public class ExifInterfaceFork { // Write EXIF APP1 segment dataOutputStream.writeByte(MARKER); dataOutputStream.writeByte(MARKER_APP1); - writeExifSegment(dataOutputStream); + mOffsetToExifData = writeExifSegment(dataOutputStream); if (mXmpFromSeparateMarker != null) { // Write XMP APP1 segment. The XMP spec (part 3, section 1.1.3) recommends for this to @@ -6533,6 +6625,7 @@ public class ExifInterfaceFork { dataOutputStream.writeUnsignedShort(length); dataOutputStream.write(IDENTIFIER_XMP_APP1); dataOutputStream.write(mXmpFromSeparateMarker.bytes); + mFileOnDiskContainsSeparateXmpMarker = true; } byte[] bytes = new byte[4096]; @@ -6627,60 +6720,76 @@ public class ExifInterfaceFork { // Copy PNG signature bytes copy(dataInputStream, dataOutputStream, PNG_SIGNATURE.length); - // EXIF chunk can appear anywhere between the first (IHDR) and last (IEND) chunks, except - // between IDAT chunks. - // Adhering to these rules, - // 1) if EXIF chunk did not exist in the original file, it will be stored right after the - // first chunk, - // 2) if EXIF chunk existed in the original file, it will be stored in the same location. - if (mOffsetToExifData == 0) { - // Copy IHDR chunk bytes - int ihdrChunkLength = dataInputStream.readInt(); - dataOutputStream.writeInt(ihdrChunkLength); - copy(dataInputStream, dataOutputStream, PNG_CHUNK_TYPE_BYTE_LENGTH - + ihdrChunkLength + PNG_CHUNK_CRC_BYTE_LENGTH); - } else { - // Copy up until the point where EXIF chunk length information is stored. - int copyLength = mOffsetToExifData - PNG_SIGNATURE.length - - 4 /* PNG EXIF chunk length bytes */ - - PNG_CHUNK_TYPE_BYTE_LENGTH; - copy(dataInputStream, dataOutputStream, copyLength); - - // Skip to the start of the chunk after the EXIF chunk - int exifChunkLength = dataInputStream.readInt(); - dataInputStream.skipFully(PNG_CHUNK_TYPE_BYTE_LENGTH + exifChunkLength - + PNG_CHUNK_CRC_BYTE_LENGTH); - } - - // Write EXIF data - ByteArrayOutputStream exifByteArrayOutputStream = null; - try { - // A byte array is needed to calculate the CRC value of this chunk which requires - // the chunk type bytes and the chunk data bytes. - exifByteArrayOutputStream = new ByteArrayOutputStream(); - ByteOrderedDataOutputStream exifDataOutputStream = - new ByteOrderedDataOutputStream(exifByteArrayOutputStream, BIG_ENDIAN); - - // Store Exif data in separate byte array - writeExifSegment(exifDataOutputStream); - byte[] exifBytes = - ((ByteArrayOutputStream) exifDataOutputStream.mOutputStream).toByteArray(); - - // Write EXIF chunk data - dataOutputStream.write(exifBytes); - - // Write EXIF chunk CRC - CRC32 crc = new CRC32(); - crc.update(exifBytes, 4 /* skip length bytes */, exifBytes.length - 4); - dataOutputStream.writeInt((int) crc.getValue()); - } finally { - closeQuietly(exifByteArrayOutputStream); + boolean needToWriteExif = true; + boolean needToWriteXmp = mXmpFromSeparateMarker != null; + while (needToWriteExif || needToWriteXmp) { + int chunkLength = dataInputStream.readInt(); + int chunkType = dataInputStream.readInt(); + if (chunkType == PNG_CHUNK_TYPE_IHDR) { + dataOutputStream.writeInt(chunkLength); + dataOutputStream.writeInt(chunkType); + copy(dataInputStream, dataOutputStream, chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH); + if (mOffsetToExifData == 0) { + // There was no Exif segment in the original file, so we put it directly + // after the IHDR chunk. + writePngExifChunk(dataOutputStream); + needToWriteExif = false; + } + if (mXmpFromSeparateMarker != null && !mFileOnDiskContainsSeparateXmpMarker) { + writePngXmpItxtChunk(dataOutputStream); + needToWriteXmp = false; + } + continue; + } else if (chunkType == PNG_CHUNK_TYPE_EXIF && needToWriteExif) { + writePngExifChunk(dataOutputStream); + dataInputStream.skipFully(chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH); + needToWriteExif = false; + continue; + } else if (chunkType == PNG_CHUNK_TYPE_ITXT && needToWriteXmp) { + writePngXmpItxtChunk(dataOutputStream); + dataInputStream.skipFully(chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH); + needToWriteXmp = false; + continue; + } + dataOutputStream.writeInt(chunkLength); + dataOutputStream.writeInt(chunkType); + copy(dataInputStream, dataOutputStream, chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH); } // Copy the rest of the file copy(dataInputStream, dataOutputStream); } + private void writePngExifChunk(ByteOrderedDataOutputStream dataOutputStream) + throws IOException { + // Write the eXIF chunk out to an intermediate byte array so we can calculate the CRC value. + ByteArrayOutputStream exifByteArrayOutputStream = new ByteArrayOutputStream(); + // Write eXIF chunk data (including chunk type & length). + int exifOffset = + writeExifSegment( + new ByteOrderedDataOutputStream(exifByteArrayOutputStream, BIG_ENDIAN)); + mOffsetToExifData = dataOutputStream.mOutputStream.size() + exifOffset; + byte[] exifBytes = exifByteArrayOutputStream.toByteArray(); + dataOutputStream.write(exifBytes); + CRC32 crc = new CRC32(); + crc.update(exifBytes, 4 /* skip length bytes */, exifBytes.length - 4); + dataOutputStream.writeInt((int) crc.getValue()); + } + + private void writePngXmpItxtChunk(ByteOrderedDataOutputStream dataOutputStream) + throws IOException { + dataOutputStream.writeInt(mXmpFromSeparateMarker.bytes.length + 22); + CRC32 crc = new CRC32(); + dataOutputStream.writeInt(PNG_CHUNK_TYPE_ITXT); + updateCrcWithInt(crc, PNG_CHUNK_TYPE_ITXT); + dataOutputStream.write(PNG_ITXT_XMP_KEYWORD); + crc.update(PNG_ITXT_XMP_KEYWORD); + dataOutputStream.write(mXmpFromSeparateMarker.bytes); + crc.update(mXmpFromSeparateMarker.bytes); + dataOutputStream.writeInt((int) crc.getValue()); + mFileOnDiskContainsSeparateXmpMarker = true; + } + // A WebP file has a header and a series of chunks. // The header is composed of: // "RIFF" + File Size + "WEBP" @@ -6726,11 +6835,12 @@ public class ExifInterfaceFork { // WebP signature copy(totalInputStream, totalOutputStream, WEBP_SIGNATURE_1.length); - // File length will be written after all the chunks have been written - totalInputStream.skipFully(WEBP_FILE_SIZE_BYTE_LENGTH + WEBP_SIGNATURE_2.length); + int riffLength = totalInputStream.readInt(); + totalInputStream.skipFully(WEBP_SIGNATURE_2.length); // Create a separate byte array to calculate file length ByteArrayOutputStream nonHeaderByteArrayOutputStream = null; + int exifOffset = -1; try { nonHeaderByteArrayOutputStream = new ByteArrayOutputStream(); ByteOrderedDataOutputStream nonHeaderOutputStream = @@ -6756,7 +6866,7 @@ public class ExifInterfaceFork { totalInputStream.skipFully(exifChunkLength); // Write new EXIF chunk to output stream - writeExifSegment(nonHeaderOutputStream); + exifOffset = writeExifSegment(nonHeaderOutputStream); } else { // EXIF chunk does not exist in the original file byte[] firstChunkType = new byte[WEBP_CHUNK_TYPE_BYTE_LENGTH]; @@ -6801,7 +6911,7 @@ public class ExifInterfaceFork { animationFinished = true; } if (animationFinished) { - writeExifSegment(nonHeaderOutputStream); + exifOffset = writeExifSegment(nonHeaderOutputStream); break; } copyWebPChunk(totalInputStream, nonHeaderOutputStream, type); @@ -6810,7 +6920,7 @@ public class ExifInterfaceFork { // Skip until we find the VP8 or VP8L chunk copyChunksUpToGivenChunkType(totalInputStream, nonHeaderOutputStream, WEBP_CHUNK_TYPE_VP8, WEBP_CHUNK_TYPE_VP8L); - writeExifSegment(nonHeaderOutputStream); + exifOffset = writeExifSegment(nonHeaderOutputStream); } } else if (Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8) || Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8L)) { @@ -6897,18 +7007,24 @@ public class ExifInterfaceFork { copy(totalInputStream, nonHeaderOutputStream, bytesToRead); // Write EXIF chunk - writeExifSegment(nonHeaderOutputStream); + exifOffset = writeExifSegment(nonHeaderOutputStream); } } - // Copy the rest of the file - copy(totalInputStream, nonHeaderOutputStream); + // Copy the rest of the RIFF part of the file + int remainingRiffBytes = riffLength + 8 - totalInputStream.position(); + copy(totalInputStream, nonHeaderOutputStream, remainingRiffBytes); // Write file length + second signature totalOutputStream.writeInt(nonHeaderByteArrayOutputStream.size() + WEBP_SIGNATURE_2.length); totalOutputStream.write(WEBP_SIGNATURE_2); + if (exifOffset != -1) { + mOffsetToExifData = totalOutputStream.mOutputStream.size() + exifOffset; + } nonHeaderByteArrayOutputStream.writeTo(totalOutputStream); + // Copy any non-RIFF trailing data + copy(totalInputStream, totalOutputStream); } catch (Exception e) { throw new IOException("Failed to save WebP file", e); } finally { @@ -7624,7 +7740,12 @@ public class ExifInterfaceFork { } } - // Writes an Exif segment into the given output stream. + /** + * Writes an Exif segment into the given output stream. + * + * @return The offset of the start of the Exif data (the byte-order marker) written into {@code + * dataOutputStream}. + */ private int writeExifSegment(ByteOrderedDataOutputStream dataOutputStream) throws IOException { // The following variables are for calculating each IFD tag group size in bytes. int[] ifdOffsets = new int[EXIF_TAGS.length]; @@ -7772,6 +7893,8 @@ public class ExifInterfaceFork { break; } + int offsetToExifData = dataOutputStream.mOutputStream.size(); + // Write TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1. dataOutputStream.writeShort(mExifByteOrder == BIG_ENDIAN ? BYTE_ALIGN_MM : BYTE_ALIGN_II); dataOutputStream.setByteOrder(mExifByteOrder); @@ -7844,7 +7967,7 @@ public class ExifInterfaceFork { // Reset the byte order to big endian in order to write remaining parts of the JPEG file. dataOutputStream.setByteOrder(BIG_ENDIAN); - return totalSize; + return offsetToExifData; } /** @@ -8240,12 +8363,12 @@ public class ExifInterfaceFork { // An output stream to write EXIF data area, which can be written in either little or big endian // order. private static class ByteOrderedDataOutputStream extends FilterOutputStream { - final OutputStream mOutputStream; + final DataOutputStream mOutputStream; private ByteOrder mByteOrder; public ByteOrderedDataOutputStream(OutputStream out, ByteOrder byteOrder) { super(out); - mOutputStream = out; + mOutputStream = new DataOutputStream(out); mByteOrder = byteOrder; }