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.
      */