Merge "Add function for adding EXIF to PNG files" into androidx-master-dev
diff --git a/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java b/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
index ff64f2f..a6526cd 100644
--- a/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -84,15 +84,17 @@
private static final String EXIF_BYTE_ORDER_MM_JPEG = "image_exif_byte_order_mm.jpg";
private static final String LG_G4_ISO_800_DNG = "lg_g4_iso_800_dng.dng";
private static final String LG_G4_ISO_800_JPG = "lg_g4_iso_800_jpg.jpg";
- private static final String EXIF_BYTE_ORDER_II_PNG = "image_exif_byte_order_ii_png.png";
- private static final String EXIF_BYTE_ORDER_II_WEBP = "image_exif_byte_order_ii_webp.webp";
+ private static final String WEBP_WITH_EXIF = "webp_with_exif.webp";
+ private static final String PNG_WITH_EXIF_BYTE_ORDER_II = "png_with_exif_byte_order_ii.png";
+ private static final String PNG_WITHOUT_EXIF = "png_without_exif.png";
private static final int[] IMAGE_RESOURCES = new int[] {
R.raw.image_exif_byte_order_ii, R.raw.image_exif_byte_order_mm, R.raw.lg_g4_iso_800_dng,
- R.raw.lg_g4_iso_800_jpg, R.raw.image_exif_byte_order_ii_png,
- R.raw.image_exif_byte_order_ii_webp};
+ R.raw.lg_g4_iso_800_jpg, R.raw.png_with_exif_byte_order_ii, R.raw.png_without_exif,
+ R.raw.webp_with_exif};
private static final String[] IMAGE_FILENAMES = new String[] {
EXIF_BYTE_ORDER_II_JPEG, EXIF_BYTE_ORDER_MM_JPEG, LG_G4_ISO_800_DNG,
- LG_G4_ISO_800_JPG, EXIF_BYTE_ORDER_II_PNG, EXIF_BYTE_ORDER_II_WEBP};
+ LG_G4_ISO_800_JPG, PNG_WITH_EXIF_BYTE_ORDER_II, PNG_WITHOUT_EXIF,
+ WEBP_WITH_EXIF};
private static final int USER_READ_WRITE = 0600;
private static final String TEST_TEMP_FILE_NAME = "testImage";
@@ -389,45 +391,59 @@
@Test
@LargeTest
- public void testReadExifDataFromExifByteOrderIIJpeg() throws Throwable {
- testExifInterfaceForJpeg(EXIF_BYTE_ORDER_II_JPEG, R.array.exifbyteorderii_jpg);
+ public void testExifByteOrderIIJpegForReadAndWrite() throws Throwable {
+ testExifInterfaceForReadAndWrite(EXIF_BYTE_ORDER_II_JPEG, R.array.exifbyteorderii_jpg);
}
@Test
@LargeTest
- public void testReadExifDataFromExifByteOrderMMJpeg() throws Throwable {
- testExifInterfaceForJpeg(EXIF_BYTE_ORDER_MM_JPEG, R.array.exifbyteordermm_jpg);
+ public void testExifByteOrderMMJpegForReadAndWrite() throws Throwable {
+ testExifInterfaceForReadAndWrite(EXIF_BYTE_ORDER_MM_JPEG, R.array.exifbyteordermm_jpg);
}
@Test
@LargeTest
- public void testReadExifDataFromLgG4Iso800Dng() throws Throwable {
- testExifInterface(LG_G4_ISO_800_DNG, R.array.lg_g4_iso_800_dng);
+ public void testLgG4Iso800DngForRead() throws Throwable {
+ testExifInterfaceForRead(LG_G4_ISO_800_DNG, R.array.lg_g4_iso_800_dng);
}
@Test
@LargeTest
- public void testReadExifDataFromLgG4Iso800Jpg() throws Throwable {
- testExifInterfaceForJpeg(LG_G4_ISO_800_JPG, R.array.lg_g4_iso_800_jpg);
+ public void testLgG4Iso800JpgForReadAndWrite() throws Throwable {
+ testExifInterfaceForReadAndWrite(LG_G4_ISO_800_JPG, R.array.lg_g4_iso_800_jpg);
}
@Test
@LargeTest
- public void testReadExifDataFromExifByteOrderIIPng() throws Throwable {
- testExifInterface(EXIF_BYTE_ORDER_II_PNG, R.array.exifbyteorderii_png);
+ public void testExifByteOrderIIPngForReadAndWrite() throws Throwable {
+ testExifInterfaceForReadAndWrite(PNG_WITH_EXIF_BYTE_ORDER_II, R.array.exifbyteorderii_png);
}
@Test
@LargeTest
- public void testReadExifDataFromStandaloneData() throws Throwable {
+ public void testStandaloneDataForRead() throws Throwable {
testExifInterfaceForStandalone(EXIF_BYTE_ORDER_II_JPEG, R.array.exifbyteorderii_standalone);
testExifInterfaceForStandalone(EXIF_BYTE_ORDER_MM_JPEG, R.array.exifbyteordermm_standalone);
}
@Test
@LargeTest
- public void testReadExifDataFromExifByteOrderIIWebp() throws Throwable {
- testExifInterface(EXIF_BYTE_ORDER_II_WEBP, R.array.exifbyteorderii_webp);
+ public void testExifByteOrderIIWebpForRead() throws Throwable {
+ testExifInterfaceForRead(WEBP_WITH_EXIF, R.array.exifbyteorderii_webp);
+ }
+
+ @Test
+ @LargeTest
+ public void testPngWithoutExifForWrite() throws Throwable {
+ File imageFile = new File(Environment.getExternalStorageDirectory(), PNG_WITHOUT_EXIF);
+
+ ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+ exifInterface.setAttribute(ExifInterface.TAG_MAKE, "abc");
+ exifInterface.saveAttributes();
+
+ exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+ String make = exifInterface.getAttribute(ExifInterface.TAG_MAKE);
+ assertEquals("abc", make);
}
@Test
@@ -976,7 +992,7 @@
compareWithExpectedValue(exifInterface, expectedValue, verboseTag, false);
}
- private void testExifInterfaceForJpeg(String fileName, int typedArrayResourceId)
+ private void testExifInterfaceForReadAndWrite(String fileName, int typedArrayResourceId)
throws IOException {
ExpectedValue expectedValue = new ExpectedValue(
getApplicationContext().getResources().obtainTypedArray(typedArrayResourceId));
@@ -988,7 +1004,7 @@
testSaveAttributes_withFileName(fileName, expectedValue);
}
- private void testExifInterface(String fileName, int typedArrayResourceId)
+ private void testExifInterfaceForRead(String fileName, int typedArrayResourceId)
throws IOException {
ExpectedValue expectedValue = new ExpectedValue(
getApplicationContext().getResources().obtainTypedArray(typedArrayResourceId));
diff --git a/exifinterface/src/androidTest/res/raw/image_exif_byte_order_ii_png.png b/exifinterface/src/androidTest/res/raw/png_with_exif_byte_order_ii.png
similarity index 100%
rename from exifinterface/src/androidTest/res/raw/image_exif_byte_order_ii_png.png
rename to exifinterface/src/androidTest/res/raw/png_with_exif_byte_order_ii.png
Binary files differ
diff --git a/exifinterface/src/androidTest/res/raw/png_without_exif.png b/exifinterface/src/androidTest/res/raw/png_without_exif.png
new file mode 100644
index 0000000..f3defab
--- /dev/null
+++ b/exifinterface/src/androidTest/res/raw/png_without_exif.png
Binary files differ
diff --git a/exifinterface/src/androidTest/res/raw/image_exif_byte_order_ii_webp.webp b/exifinterface/src/androidTest/res/raw/webp_with_exif.webp
similarity index 100%
rename from exifinterface/src/androidTest/res/raw/image_exif_byte_order_ii_webp.webp
rename to exifinterface/src/androidTest/res/raw/webp_with_exif.webp
Binary files differ
diff --git a/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
index 41b727a..f96f973 100644
--- a/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -36,6 +36,7 @@
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.DataInput;
import java.io.DataInputStream;
@@ -68,6 +69,7 @@
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.zip.CRC32;
/**
* This is a class for reading and writing Exif tags in a JPEG file or a RAW image file.
@@ -2903,10 +2905,13 @@
// 3.7. eXIf Exchangeable Image File (Exif) Profile
private static final byte[] PNG_CHUNK_TYPE_EXIF = new byte[]{(byte) 0x65, (byte) 0x58,
(byte) 0x49, (byte) 0x66};
+ private static final byte[] PNG_CHUNK_TYPE_IHDR = new byte[]{(byte) 0x49, (byte) 0x48,
+ (byte) 0x44, (byte) 0x52};
private static final byte[] PNG_CHUNK_TYPE_IEND = new byte[]{(byte) 0x49, (byte) 0x45,
(byte) 0x4e, (byte) 0x44};
- private static final int PNG_CHUNK_LENGTH_BYTE_LENGTH = 4;
+ private static final int PNG_CHUNK_TYPE_BYTE_LENGTH = 4;
private static final int PNG_CHUNK_CRC_BYTE_LENGTH = 4;
+ private static final int PNG_OFFSET_TO_IHDR_BYTES = 12;
// 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'};
@@ -4533,7 +4538,7 @@
* other. It's best to use {@link #setAttribute(String,String)} to set all attributes to write
* and make a single call rather than multiple calls for each attribute.
* <p>
- * This method is only supported for JPEG files.
+ * This method is only supported for JPEG and PNG files.
* <p class="note">
* Note: after calling this method, any attempts to obtain range information
* from {@link #getAttributeRange(String)} or {@link #getThumbnailRange()}
@@ -4542,8 +4547,9 @@
* </p>
*/
public void saveAttributes() throws IOException {
- if (!mIsSupportedFile || mMimeType != IMAGE_TYPE_JPEG) {
- throw new IOException("ExifInterface only supports saving attributes on JPEG formats.");
+ if (!mIsSupportedFile || (mMimeType != IMAGE_TYPE_JPEG && mMimeType != IMAGE_TYPE_PNG)) {
+ throw new IOException("ExifInterface only supports saving attributes on JPEG or PNG "
+ + "formats.");
}
if (mSeekableFileDescriptor == null && mFilename == null) {
throw new IOException(
@@ -4572,7 +4578,11 @@
throw new IOException("Couldn't rename to " + tempFile.getAbsolutePath());
}
} else if (Build.VERSION.SDK_INT >= 21 && mSeekableFileDescriptor != null) {
- tempFile = File.createTempFile("temp", "jpg");
+ if (mMimeType == IMAGE_TYPE_JPEG) {
+ tempFile = File.createTempFile("temp", "jpg");
+ } else if (mMimeType == IMAGE_TYPE_PNG) {
+ tempFile = File.createTempFile("temp", "png");
+ }
Os.lseek(mSeekableFileDescriptor, 0, OsConstants.SEEK_SET);
in = new FileInputStream(mSeekableFileDescriptor);
out = new FileOutputStream(tempFile);
@@ -4600,7 +4610,11 @@
}
bufferedIn = new BufferedInputStream(in);
bufferedOut = new BufferedOutputStream(out);
- saveJpegAttributes(bufferedIn, bufferedOut);
+ if (mMimeType == IMAGE_TYPE_JPEG) {
+ saveJpegAttributes(bufferedIn, bufferedOut);
+ } else if (mMimeType == IMAGE_TYPE_PNG) {
+ savePngAttributes(bufferedIn, bufferedOut);
+ }
} catch (Exception e) {
if (mFilename != null) {
if (!tempFile.renameTo(originalFile)) {
@@ -5889,27 +5903,33 @@
in.skipBytes(PNG_SIGNATURE.length);
bytesRead += PNG_SIGNATURE.length;
+ // Each chunk is made up of four parts:
+ // 1) Length: 4-byte unsigned integer indicating the number of bytes in the
+ // Chunk Data field. Excludes Chunk Type and CRC bytes.
+ // 2) Chunk Type: 4-byte chunk type code.
+ // 3) Chunk Data: The data bytes. Can be zero-length.
+ // 4) CRC: 4-byte data calculated on the preceding bytes in the chunk. Always
+ // present.
+ // --> 4 (length bytes) + 4 (type bytes) + X (data bytes) + 4 (CRC bytes)
+ // See PNG (Portable Network Graphics) Specification, Version 1.2,
+ // 3.2. Chunk layout
try {
while (true) {
- // Each chunk is made up of four parts:
- // 1) Length: 4-byte unsigned integer indicating the number of bytes in the
- // Chunk Data field. Excludes Chunk Type and CRC bytes.
- // 2) Chunk Type: 4-byte chunk type code.
- // 3) Chunk Data: The data bytes. Can be zero-length.
- // 4) CRC: 4-byte data calculated on the preceding bytes in the chunk. Always
- // present.
- // --> 4 (length bytes) + 4 (type bytes) + X (data bytes) + 4 (CRC bytes)
- // See PNG (Portable Network Graphics) Specification, Version 1.2,
- // 3.2. Chunk layout
int length = in.readInt();
bytesRead += 4;
- byte[] type = new byte[PNG_CHUNK_LENGTH_BYTE_LENGTH];
+ byte[] type = new byte[PNG_CHUNK_TYPE_BYTE_LENGTH];
if (in.read(type) != type.length) {
throw new IOException("Encountered invalid length while parsing PNG chunk"
+ "type");
}
- bytesRead += PNG_CHUNK_LENGTH_BYTE_LENGTH;
+ bytesRead += PNG_CHUNK_TYPE_BYTE_LENGTH;
+
+ // The first chunk must be the IHDR chunk
+ if (bytesRead == 16 && !Arrays.equals(type, PNG_CHUNK_TYPE_IHDR)) {
+ throw new IOException("Encountered invalid PNG file--IHDR chunk should appear"
+ + "as the first chunk");
+ }
if (Arrays.equals(type, PNG_CHUNK_TYPE_IEND)) {
// IEND marks the end of the image.
@@ -5921,9 +5941,25 @@
throw new IOException("Failed to read given length for given PNG chunk "
+ "type: " + byteArrayToHexString(type));
}
+
+ // Compare CRC values for potential data corruption.
+ int dataCrcValue = in.readInt();
+ // Cyclic Redundancy Code used to check for corruption of the data
+ CRC32 crc = new CRC32();
+ crc.update(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());
+ }
+
readExifSegment(data, IFD_TYPE_PRIMARY);
validateImages();
+
+ // Save offset values for handleThumbnailFromJfif() function
+ mExifOffset = bytesRead;
break;
} else {
// Skip to next chunk
@@ -5931,8 +5967,6 @@
bytesRead += length + PNG_CHUNK_CRC_BYTE_LENGTH;
}
}
- // Save offset values for handleThumbnailFromJfif() function
- mExifOffset = bytesRead;
} catch (EOFException e) {
// Should not reach here. Will only reach here if the file is corrupted or
// does not follow the PNG specifications
@@ -6114,6 +6148,74 @@
}
}
+ private void savePngAttributes(InputStream inputStream, OutputStream outputStream)
+ throws IOException {
+ if (DEBUG) {
+ Log.d(TAG, "savePngAttributes starting with (inputStream: " + inputStream
+ + ", outputStream: " + outputStream + ")");
+ }
+ DataInputStream dataInputStream = new DataInputStream(inputStream);
+ ByteOrderedDataOutputStream dataOutputStream =
+ new ByteOrderedDataOutputStream(outputStream, ByteOrder.BIG_ENDIAN);
+
+ // 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 (mExifOffset == 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 = mExifOffset - 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.skipBytes(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,
+ ByteOrder.BIG_ENDIAN);
+
+ // Store Exif data in separate byte array
+ writeExifSegment(exifDataOutputStream, 0);
+ 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);
+ }
+
+ // Copy the rest of the file
+ copy(dataInputStream, dataOutputStream);
+ }
+
// Reads the given EXIF byte area and save its tag data into attributes.
private void readExifSegment(byte[] exifBytes, int imageType) throws IOException {
ByteOrderedDataInputStream dataInputStream =
@@ -6834,7 +6936,7 @@
}
// Calculate IFD offsets.
- int position = 8;
+ int position = 8; // 8 bytes are for TIFF headers
for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
if (!mAttributes[ifdType].isEmpty()) {
ifdOffsets[ifdType] = position;
@@ -6849,13 +6951,16 @@
position += mThumbnailLength;
}
- // Calculate the total size
- int totalSize = position + 8; // eight bytes is for header part.
+ int totalSize = position;
+ if (mMimeType == IMAGE_TYPE_JPEG) {
+ // Add 8 bytes for APP1 size and identifier data
+ totalSize += 8;
+ }
if (DEBUG) {
- Log.d(TAG, "totalSize length: " + totalSize);
for (int i = 0; i < EXIF_TAGS.length; ++i) {
- Log.d(TAG, String.format("index: %d, offsets: %d, tag count: %d, data sizes: %d",
- i, ifdOffsets[i], mAttributes[i].size(), ifdDataSizes[i]));
+ Log.d(TAG, String.format("index: %d, offsets: %d, tag count: %d, data sizes: %d, "
+ + "total size: %d", i, ifdOffsets[i], mAttributes[i].size(),
+ ifdDataSizes[i], totalSize));
}
}
@@ -6873,9 +6978,17 @@
ifdOffsets[IFD_TYPE_INTEROPERABILITY], mExifByteOrder));
}
+ if (mMimeType == IMAGE_TYPE_JPEG) {
+ // Write JPEG specific data (APP1 size, APP1 identifier)
+ dataOutputStream.writeUnsignedShort(totalSize);
+ dataOutputStream.write(IDENTIFIER_EXIF_APP1);
+ } else if (mMimeType == IMAGE_TYPE_PNG) {
+ // Write PNG specific data (chunk size, chunk type)
+ dataOutputStream.writeInt(totalSize);
+ dataOutputStream.write(PNG_CHUNK_TYPE_EXIF);
+ }
+
// Write TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1.
- dataOutputStream.writeUnsignedShort(totalSize);
- dataOutputStream.write(IDENTIFIER_EXIF_APP1);
dataOutputStream.writeShort(mExifByteOrder == ByteOrder.BIG_ENDIAN
? BYTE_ALIGN_MM : BYTE_ALIGN_II);
dataOutputStream.setByteOrder(mExifByteOrder);
@@ -6950,7 +7063,7 @@
* Determines the data format of EXIF entry value.
*
* @param entryValue The value to be determined.
- * @return Returns two data formats gussed as a pair in integer. If there is no two candidate
+ * @return Returns two data formats guessed as a pair in integer. If there is no two candidate
data formats for the given entry value, returns {@code -1} in the second of the pair.
*/
private static Pair<Integer, Integer> guessDataFormat(String entryValue) {
@@ -7280,7 +7393,7 @@
// 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 {
- private final OutputStream mOutputStream;
+ final OutputStream mOutputStream;
private ByteOrder mByteOrder;
public ByteOrderedDataOutputStream(OutputStream out, ByteOrder byteOrder) {
@@ -7430,6 +7543,25 @@
}
/**
+ * Copies the given number of the bytes from {@code in} to {@code out}. Neither stream is
+ * closed.
+ */
+ private static void copy(InputStream in, OutputStream out, int numBytes) throws IOException {
+ int remainder = numBytes;
+ byte[] buffer = new byte[8192];
+ while (remainder > 0) {
+ int bytesToRead = Math.min(remainder, 8192);
+ int bytesRead = in.read(buffer, 0, bytesToRead);
+ if (bytesRead != bytesToRead) {
+ throw new IOException("Failed to copy the given amount of bytes from the input"
+ + "stream to the output stream.");
+ }
+ remainder -= bytesRead;
+ out.write(buffer, 0, bytesRead);
+ }
+ }
+
+ /**
* Convert given int[] to long[]. If long[] is given, just return it.
* Return null for other types of input.
*/