diff options
| author | 2019-10-24 17:35:31 +0900 | |
|---|---|---|
| committer | 2019-10-29 06:08:27 +0000 | |
| commit | 195c8ed1cc85ca3df6eee7c1244664abe789f3ab (patch) | |
| tree | 0a9e4819adb0ea5c2531ac432f4fc410e622b8c0 | |
| parent | 23e881bb503922057232830d5d5f02d7303f4ed5 (diff) | |
Add function for adding EXIF to PNG files
Added tests for both PNG files that do not have EXIF data and that
do have EXIF data.
Bug: 138376314
Test: Run atest ExifInterfaceTest
Change-Id: I3394940aa4466fac259117a4a0d226899068fd7d
| -rw-r--r-- | media/java/android/media/ExifInterface.java | 179 |
1 files changed, 149 insertions, 30 deletions
diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java index b61f5065c1c5..d6a4ea7cb39f 100644 --- a/media/java/android/media/ExifInterface.java +++ b/media/java/android/media/ExifInterface.java @@ -39,6 +39,7 @@ import libcore.io.Streams; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.DataInput; import java.io.DataInputStream; import java.io.EOFException; @@ -68,6 +69,7 @@ import java.util.Set; import java.util.TimeZone; 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. @@ -502,9 +504,11 @@ public class ExifInterface { // 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; // See https://developers.google.com/speed/webp/docs/riff_container, Section "WebP File Header" @@ -1948,7 +1952,7 @@ public class ExifInterface { * {@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()} @@ -1957,8 +1961,9 @@ public class ExifInterface { * </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 (mIsInputStream || (mSeekableFileDescriptor == null && mFilename == null)) { throw new IOException( @@ -1987,7 +1992,11 @@ public class ExifInterface { throw new IOException("Couldn't rename to " + tempFile.getAbsolutePath()); } } else if (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); @@ -2013,7 +2022,11 @@ public class ExifInterface { } try (BufferedInputStream bufferedIn = new BufferedInputStream(in); BufferedOutputStream 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) { @@ -3185,27 +3198,33 @@ public class ExifInterface { 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. @@ -3217,9 +3236,25 @@ public class ExifInterface { 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 @@ -3227,8 +3262,6 @@ public class ExifInterface { 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 @@ -3403,6 +3436,62 @@ public class ExifInterface { } } + 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 + try (ByteArrayOutputStream exifByteArrayOutputStream = new ByteArrayOutputStream()) { + // A byte array is needed to calculate the CRC value of this chunk which requires + // the chunk type bytes and the chunk data bytes. + 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()); + } + // Copy the rest of the file + Streams.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 = @@ -4102,7 +4191,7 @@ public class ExifInterface { } // 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; @@ -4117,13 +4206,16 @@ public class ExifInterface { 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)); } } @@ -4141,9 +4233,17 @@ public class ExifInterface { 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); @@ -4219,7 +4319,7 @@ public class ExifInterface { * 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) { @@ -4541,7 +4641,7 @@ public class ExifInterface { // 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) { @@ -4657,6 +4757,25 @@ public class ExifInterface { } /** + * 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. */ |