Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 740558e4 authored by Jin Seok Park's avatar Jin Seok Park
Browse files

Add support for parsing Exif chunk in PNG

Bug: 138376314
Test: Cannot find PNG file with Exif chunk but manually checked that
      the chunks were being read correctly

Change-Id: Ifa54a4bb9bfde7ea02d82821a5a3b2b4cb5a8f71
parent 47fc7357
Loading
Loading
Loading
Loading
+140 −19
Original line number Diff line number Diff line
@@ -493,6 +493,19 @@ public class ExifInterface {
    // See http://www.exiv2.org/makernote.html#R11
    private static final int PEF_MAKER_NOTE_SKIP_SIZE = 6;

    // See PNG (Portable Network Graphics) Specification, Version 1.2,
    // 3.1. PNG file signature
    private static final byte[] PNG_SIGNATURE = new byte[] {(byte) 0x89, (byte) 0x50, (byte) 0x4e,
            (byte) 0x47, (byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a};
    // See PNG (Portable Network Graphics) Specification, Version 1.2,
    // 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_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_CRC_BYTE_LENGTH = 4;

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    private static SimpleDateFormat sFormatter;
    private static SimpleDateFormat sFormatterTz;
@@ -1310,6 +1323,7 @@ public class ExifInterface {
    private static final int IMAGE_TYPE_RW2 = 10;
    private static final int IMAGE_TYPE_SRW = 11;
    private static final int IMAGE_TYPE_HEIF = 12;
    private static final int IMAGE_TYPE_PNG = 13;

    static {
        sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
@@ -1779,6 +1793,10 @@ public class ExifInterface {
                    getRw2Attributes(inputStream);
                    break;
                }
                case IMAGE_TYPE_PNG: {
                    getPngAttributes(inputStream);
                    break;
                }
                case IMAGE_TYPE_ARW:
                case IMAGE_TYPE_CR2:
                case IMAGE_TYPE_DNG:
@@ -1992,6 +2010,7 @@ public class ExifInterface {
            if (in.skip(mThumbnailOffset) != mThumbnailOffset) {
                throw new IOException("Corrupted image");
            }
            // TODO: Need to handle potential OutOfMemoryError
            byte[] buffer = new byte[mThumbnailLength];
            if (in.read(buffer) != mThumbnailLength) {
                throw new IOException("Corrupted image");
@@ -2331,6 +2350,8 @@ public class ExifInterface {
            return IMAGE_TYPE_ORF;
        } else if (isRw2Format(signatureCheckBytes)) {
            return IMAGE_TYPE_RW2;
        } else if (isPngFormat(signatureCheckBytes)) {
            return IMAGE_TYPE_PNG;
        }
        // Certain file formats (PEF) are identified in readImageFileDirectory()
        return IMAGE_TYPE_UNKNOWN;
@@ -2446,16 +2467,24 @@ public class ExifInterface {
     * http://fileformats.archiveteam.org/wiki/Olympus_ORF
     */
    private boolean isOrfFormat(byte[] signatureCheckBytes) throws IOException {
        ByteOrderedDataInputStream signatureInputStream =
                new ByteOrderedDataInputStream(signatureCheckBytes);
        ByteOrderedDataInputStream signatureInputStream = null;

        try {
            signatureInputStream = new ByteOrderedDataInputStream(signatureCheckBytes);

            // Read byte order
            mExifByteOrder = readByteOrder(signatureInputStream);
            // Set byte order
            signatureInputStream.setByteOrder(mExifByteOrder);

            short orfSignature = signatureInputStream.readShort();
        if (orfSignature == ORF_SIGNATURE_1 || orfSignature == ORF_SIGNATURE_2) {
            return true;
            return orfSignature == ORF_SIGNATURE_1 || orfSignature == ORF_SIGNATURE_2;
        } catch (Exception e) {
            // Do nothing
        } finally {
            if (signatureInputStream != null) {
                signatureInputStream.close();
            }
        }
        return false;
    }
@@ -2465,20 +2494,42 @@ public class ExifInterface {
     * See http://lclevy.free.fr/raw/
     */
    private boolean isRw2Format(byte[] signatureCheckBytes) throws IOException {
        ByteOrderedDataInputStream signatureInputStream =
                new ByteOrderedDataInputStream(signatureCheckBytes);
        ByteOrderedDataInputStream signatureInputStream = null;

        try {
            signatureInputStream = new ByteOrderedDataInputStream(signatureCheckBytes);

            // Read byte order
            mExifByteOrder = readByteOrder(signatureInputStream);
            // Set byte order
            signatureInputStream.setByteOrder(mExifByteOrder);

            short signatureByte = signatureInputStream.readShort();
        if (signatureByte == RW2_SIGNATURE) {
            return true;
            signatureInputStream.close();
            return signatureByte == RW2_SIGNATURE;
        } catch (Exception e) {
            // Do nothing
        } finally {
            if (signatureInputStream != null) {
                signatureInputStream.close();
            }
        }
        return false;
    }

    /**
     * PNG's file signature is first 8 bytes.
     * See PNG (Portable Network Graphics) Specification, Version 1.2, 3.1. PNG file signature
     */
    private boolean isPngFormat(byte[] signatureCheckBytes) throws IOException {
        for (int i = 0; i < PNG_SIGNATURE.length; i++) {
            if (signatureCheckBytes[i] != PNG_SIGNATURE[i]) {
                return false;
            }
        }
        return true;
    }

    /**
     * Loads EXIF attributes from a JPEG input stream.
     *
@@ -2553,7 +2604,7 @@ public class ExifInterface {

                        readExifSegment(value, imageType);

                        // Save offset values for createJpegThumbnailBitmap() function
                        // Save offset values for handleThumbnailFromJfif() function
                        mExifOffset = (int) offset;
                    } else if (ArrayUtils.startsWith(bytes, IDENTIFIER_XMP_APP1)) {
                        // See XMP Specification Part 3: Storage in Files, 1.1.3 JPEG, Table 6
@@ -2854,6 +2905,7 @@ public class ExifInterface {
                    throw new IOException("Invalid identifier");
                }

                // TODO: Need to handle potential OutOfMemoryError
                byte[] bytes = new byte[length];
                if (in.read(bytes) != length) {
                    throw new IOException("Can't read exif");
@@ -2980,6 +3032,64 @@ public class ExifInterface {
        }
    }

    // PNG contains the EXIF data as a Special-Purpose Chunk
    private void getPngAttributes(ByteOrderedDataInputStream in) throws IOException {
        if (DEBUG) {
            Log.d(TAG, "getPngAttributes starting with: " + in);
        }

        // PNG uses Big Endian by default.
        // See PNG (Portable Network Graphics) Specification, Version 1.2,
        // 2.1. Integers and byte order
        in.setByteOrder(ByteOrder.BIG_ENDIAN);

        // Skip the signature bytes
        in.seek(PNG_SIGNATURE.length);

        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();

                byte[] type = new byte[PNG_CHUNK_LENGTH_BYTE_LENGTH];
                if (in.read(type) != type.length) {
                    throw new IOException("Encountered invalid length while parsing PNG chunk"
                            + "type");
                }

                if (Arrays.equals(type, PNG_CHUNK_TYPE_IEND)) {
                    // IEND marks the end of the image.
                    break;
                } else if (Arrays.equals(type, PNG_CHUNK_TYPE_EXIF)) {
                    // TODO: Need to handle potential OutOfMemoryError
                    byte[] data = new byte[length];
                    if (in.read(data) != length) {
                        throw new IOException("Failed to read given length for given PNG chunk "
                                + "type: " + byteArrayToHexString(type));
                    }
                    readExifSegment(data, IFD_TYPE_PRIMARY);
                    break;
                } else {
                    // Skip to next chunk
                    in.skipBytes(length + PNG_CHUNK_CRC_BYTE_LENGTH);
                }
            }
        } catch (EOFException e) {
            // Should not reach here. Will only reach here if the file is corrupted or
            // does not follow the PNG specifications
            throw new IOException("Encountered corrupt PNG file.");
        }
    }

    // Stores a new JPEG image with EXIF attributes into a given output stream.
    private void saveJpegAttributes(InputStream inputStream, OutputStream outputStream)
            throws IOException {
@@ -3485,6 +3595,7 @@ public class ExifInterface {

                if (mFilename == null && mAssetInputStream == null
                        && mSeekableFileDescriptor == null) {
                    // TODO: Need to handle potential OutOfMemoryError
                    // Save the thumbnail in memory if the input doesn't support reading again.
                    byte[] thumbnailBytes = new byte[thumbnailLength];
                    in.seek(thumbnailOffset);
@@ -3518,6 +3629,7 @@ public class ExifInterface {
                return;
            }

            // TODO: Need to handle potential OutOfMemoryError
            // Set thumbnail byte array data for non-consecutive strip bytes
            byte[] totalStripBytes =
                    new byte[(int) Arrays.stream(stripByteCounts).sum()];
@@ -3536,6 +3648,7 @@ public class ExifInterface {
                in.seek(skipBytes);
                bytesRead += skipBytes;

                // TODO: Need to handle potential OutOfMemoryError
                // Read strip bytes
                byte[] stripBytes = new byte[stripByteCount];
                in.read(stripBytes);
@@ -4335,4 +4448,12 @@ public class ExifInterface {
        }
        return null;
    }

    private static String byteArrayToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (int i = 0; i < bytes.length; i++) {
            sb.append(String.format("%02x", bytes[i]));
        }
        return sb.toString();
    }
}