ExifInterface upgrade

This commit is contained in:
Thibault Deckers 2025-01-21 14:31:33 +01:00
parent a0f7af96e0
commit 8e5d971a6f

View file

@ -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 {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #DATA_UNCOMPRESSED
* @see #DATA_JPEG
*/
@ -205,6 +208,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #PHOTOMETRIC_INTERPRETATION_RGB
* @see #PHOTOMETRIC_INTERPRETATION_YCBCR
*/
@ -219,6 +223,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #ORIENTATION_NORMAL}</li>
* </ul>
* <p>
*
* @see #ORIENTATION_UNDEFINED
* @see #ORIENTATION_NORMAL
* @see #ORIENTATION_FLIP_HORIZONTAL
@ -254,6 +259,7 @@ public class ExifInterfaceFork {
* <li>Count = 1</li>
* </ul>
* <p>
*
* @see #FORMAT_CHUNKY
* @see #FORMAT_PLANAR
*/
@ -294,6 +300,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #Y_CB_CR_POSITIONING_CENTERED}</li>
* </ul>
* <p>
*
* @see #Y_CB_CR_POSITIONING_CENTERED
* @see #Y_CB_CR_POSITIONING_CO_SITED
*/
@ -309,6 +316,7 @@ public class ExifInterfaceFork {
* <li>Default = 72</li>
* </ul>
* <p>
*
* @see #TAG_Y_RESOLUTION
* @see #TAG_RESOLUTION_UNIT
*/
@ -324,6 +332,7 @@ public class ExifInterfaceFork {
* <li>Default = 72</li>
* </ul>
* <p>
*
* @see #TAG_X_RESOLUTION
* @see #TAG_RESOLUTION_UNIT
*/
@ -340,6 +349,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #RESOLUTION_UNIT_INCHES}</li>
* </ul>
* <p>
*
* @see #RESOLUTION_UNIT_INCHES
* @see #RESOLUTION_UNIT_CENTIMETERS
* @see #TAG_X_RESOLUTION
@ -365,6 +375,7 @@ public class ExifInterfaceFork {
* <p>StripsPerImage = floor(({@link #TAG_IMAGE_LENGTH} + {@link #TAG_ROWS_PER_STRIP} - 1)
* / {@link #TAG_ROWS_PER_STRIP})</p>
* <p>
*
* @see #TAG_ROWS_PER_STRIP
* @see #TAG_STRIP_BYTE_COUNTS
*/
@ -381,6 +392,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #TAG_STRIP_OFFSETS
* @see #TAG_STRIP_BYTE_COUNTS
*/
@ -656,6 +668,7 @@ public class ExifInterfaceFork {
* <li>Count = 1</li>
* </ul>
* <p>
*
* @see #COLOR_SPACE_S_RGB
* @see #COLOR_SPACE_UNCALIBRATED
*/
@ -962,6 +975,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #EXPOSURE_PROGRAM_NOT_DEFINED}</li>
* </ul>
* <p>
*
* @see #EXPOSURE_PROGRAM_NOT_DEFINED
* @see #EXPOSURE_PROGRAM_MANUAL
* @see #EXPOSURE_PROGRAM_NORMAL
@ -1031,6 +1045,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #SENSITIVITY_TYPE_UNKNOWN
* @see #SENSITIVITY_TYPE_SOS
* @see #SENSITIVITY_TYPE_REI
@ -1197,6 +1212,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #METERING_MODE_UNKNOWN}</li>
* </ul>
* <p>
*
* @see #METERING_MODE_UNKNOWN
* @see #METERING_MODE_AVERAGE
* @see #METERING_MODE_CENTER_WEIGHT_AVERAGE
@ -1217,6 +1233,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #LIGHT_SOURCE_UNKNOWN}</li>
* </ul>
* <p>
*
* @see #LIGHT_SOURCE_UNKNOWN
* @see #LIGHT_SOURCE_DAYLIGHT
* @see #LIGHT_SOURCE_FLUORESCENT
@ -1253,6 +1270,7 @@ public class ExifInterfaceFork {
* <li>Count = 1</li>
* </ul>
* <p>
*
* @see #FLAG_FLASH_FIRED
* @see #FLAG_FLASH_RETURN_LIGHT_NOT_DETECTED
* @see #FLAG_FLASH_RETURN_LIGHT_DETECTED
@ -1365,6 +1383,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #RESOLUTION_UNIT_INCHES}</li>
* </ul>
* <p>
*
* @see #TAG_RESOLUTION_UNIT
* @see #RESOLUTION_UNIT_INCHES
* @see #RESOLUTION_UNIT_CENTIMETERS
@ -1407,6 +1426,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #SENSOR_TYPE_NOT_DEFINED
* @see #SENSOR_TYPE_ONE_CHIP
* @see #SENSOR_TYPE_TWO_CHIP
@ -1427,6 +1447,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #FILE_SOURCE_DSC}</li>
* </ul>
* <p>
*
* @see #FILE_SOURCE_OTHER
* @see #FILE_SOURCE_TRANSPARENT_SCANNER
* @see #FILE_SOURCE_REFLEX_SCANNER
@ -1444,6 +1465,7 @@ public class ExifInterfaceFork {
* <li>Default = 1</li>
* </ul>
* <p>
*
* @see #SCENE_TYPE_DIRECTLY_PHOTOGRAPHED
*/
public static final String TAG_SCENE_TYPE = "SceneType";
@ -1457,6 +1479,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #TAG_SENSING_METHOD
* @see #SENSOR_TYPE_ONE_CHIP
*/
@ -1473,6 +1496,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #RENDERED_PROCESS_NORMAL}</li>
* </ul>
* <p>
*
* @see #RENDERED_PROCESS_NORMAL
* @see #RENDERED_PROCESS_CUSTOM
*/
@ -1489,6 +1513,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #EXPOSURE_MODE_AUTO
* @see #EXPOSURE_MODE_MANUAL
* @see #EXPOSURE_MODE_AUTO_BRACKET
@ -1504,6 +1529,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #WHITEBALANCE_AUTO
* @see #WHITEBALANCE_MANUAL
*/
@ -1553,6 +1579,7 @@ public class ExifInterfaceFork {
* <li>Default = 0</li>
* </ul>
* <p>
*
* @see #SCENE_CAPTURE_TYPE_STANDARD
* @see #SCENE_CAPTURE_TYPE_LANDSCAPE
* @see #SCENE_CAPTURE_TYPE_PORTRAIT
@ -1569,6 +1596,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #GAIN_CONTROL_NONE
* @see #GAIN_CONTROL_LOW_GAIN_UP
* @see #GAIN_CONTROL_HIGH_GAIN_UP
@ -1587,6 +1615,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #CONTRAST_NORMAL}</li>
* </ul>
* <p>
*
* @see #CONTRAST_NORMAL
* @see #CONTRAST_SOFT
* @see #CONTRAST_HARD
@ -1603,6 +1632,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #SATURATION_NORMAL}</li>
* </ul>
* <p>
*
* @see #SATURATION_NORMAL
* @see #SATURATION_LOW
* @see #SATURATION_HIGH
@ -1619,6 +1649,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #SHARPNESS_NORMAL}</li>
* </ul>
* <p>
*
* @see #SHARPNESS_NORMAL
* @see #SHARPNESS_SOFT
* @see #SHARPNESS_HARD
@ -1646,6 +1677,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #SUBJECT_DISTANCE_RANGE_UNKNOWN
* @see #SUBJECT_DISTANCE_RANGE_MACRO
* @see #SUBJECT_DISTANCE_RANGE_CLOSE_VIEW
@ -1675,6 +1707,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @deprecated Use {@link #TAG_CAMERA_OWNER_NAME} instead.
*/
@Deprecated
@ -1780,6 +1813,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #LATITUDE_NORTH
* @see #LATITUDE_SOUTH
*/
@ -1809,6 +1843,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #LONGITUDE_EAST
* @see #LONGITUDE_WEST
*/
@ -1841,6 +1876,7 @@ public class ExifInterfaceFork {
* <li>Default = 0</li>
* </ul>
* <p>
*
* @see #ALTITUDE_ABOVE_SEA_LEVEL
* @see #ALTITUDE_BELOW_SEA_LEVEL
*/
@ -1899,6 +1935,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #GPS_MEASUREMENT_IN_PROGRESS
* @see #GPS_MEASUREMENT_INTERRUPTED
*/
@ -1915,6 +1952,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #GPS_MEASUREMENT_2D
* @see #GPS_MEASUREMENT_3D
*/
@ -1941,6 +1979,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #GPS_SPEED_KILOMETERS_PER_HOUR}</li>
* </ul>
* <p>
*
* @see #GPS_SPEED_KILOMETERS_PER_HOUR
* @see #GPS_SPEED_MILES_PER_HOUR
* @see #GPS_SPEED_KNOTS
@ -1968,6 +2007,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #GPS_DIRECTION_TRUE}</li>
* </ul>
* <p>
*
* @see #GPS_DIRECTION_TRUE
* @see #GPS_DIRECTION_MAGNETIC
*/
@ -1994,6 +2034,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #GPS_DIRECTION_TRUE}</li>
* </ul>
* <p>
*
* @see #GPS_DIRECTION_TRUE
* @see #GPS_DIRECTION_MAGNETIC
*/
@ -2032,6 +2073,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #LATITUDE_NORTH
* @see #LATITUDE_SOUTH
*/
@ -2061,6 +2103,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #LONGITUDE_EAST
* @see #LONGITUDE_WEST
*/
@ -2090,6 +2133,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #GPS_DIRECTION_TRUE}</li>
* </ul>
* <p>
*
* @see #GPS_DIRECTION_TRUE
* @see #GPS_DIRECTION_MAGNETIC
*/
@ -2116,6 +2160,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #GPS_DISTANCE_KILOMETERS}</li>
* </ul>
* <p>
*
* @see #GPS_DISTANCE_KILOMETERS
* @see #GPS_DISTANCE_MILES
* @see #GPS_DISTANCE_NAUTICAL_MILES
@ -2177,6 +2222,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @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.
*
* <p>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.
* <p>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 {
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(length + PNG_CHUNK_CRC_BYTE_LENGTH);
}
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.
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) {
// 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);
// There was no Exif segment in the original file, so we put it directly
// after the IHDR chunk.
writePngExifChunk(dataOutputStream);
needToWriteExif = false;
}
// 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);
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;
}