summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Jin Seok Park <jinpark@google.com> 2020-04-03 14:46:45 +0900
committer Harish Mahendrakar <harish.mahendrakar@ittiam.com> 2020-09-17 16:43:34 -0700
commitc6cf32b62310ad9b09c737d480ce42a1fa6297a9 (patch)
tree5e14c331cbf06da8a3b590214e1b8a76a4f2e37d
parent40b7a9f873731830ead7320cb327fdb8f4f6b9e7 (diff)
Add function for adding EXIF to WebP files
Code copied from aosp/1148409 and aosp/1236647 with some minor changes. Original commit message: Added code and test for WebP files 1) with EXIF data 2) without EXIF data but with other extra data Added code and test for saving EXIF to WebP files without EXIF data 1) and have lossy encoding 2) and have lossless encoding Also refactored test method names for better readability. Bug: 138786288 Test: atest CtsMediaTestCases:android.media.cts.ExifInterfaceTest Change-Id: Ib6319e1b7b6711b071542d94ebfdc4a9f80e9c16 Merged-In: Ib6319e1b7b6711b071542d94ebfdc4a9f80e9c16
-rw-r--r--media/java/android/media/ExifInterface.java344
1 files changed, 322 insertions, 22 deletions
diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java
index 6d690f0aa397..5fce5ee9407d 100644
--- a/media/java/android/media/ExifInterface.java
+++ b/media/java/android/media/ExifInterface.java
@@ -75,11 +75,11 @@ 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.
+ * This is a class for reading and writing Exif tags in various image file formats.
* <p>
- * Supported formats are: JPEG, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW, RAF and HEIF.
+ * Supported for reading: JPEG, PNG, WebP, HEIF, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW, RAF.
* <p>
- * Attribute mutation is supported for JPEG image files.
+ * Supported for writing: JPEG, PNG, WebP.
* <p>
* Note: It is recommended to use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
* <a href="{@docRoot}reference/androidx/exifinterface/media/ExifInterface.html">ExifInterface
@@ -585,6 +585,15 @@ public class ExifInterface {
private static final int WEBP_FILE_SIZE_BYTE_LENGTH = 4;
private static final byte[] WEBP_CHUNK_TYPE_EXIF = new byte[]{(byte) 0x45, (byte) 0x58,
(byte) 0x49, (byte) 0x46};
+ private static final byte[] WEBP_VP8_SIGNATURE = new byte[]{(byte) 0x9d, (byte) 0x01,
+ (byte) 0x2a};
+ private static final byte WEBP_VP8L_SIGNATURE = (byte) 0x2f;
+ private static final byte[] WEBP_CHUNK_TYPE_VP8X = "VP8X".getBytes(Charset.defaultCharset());
+ private static final byte[] WEBP_CHUNK_TYPE_VP8L = "VP8L".getBytes(Charset.defaultCharset());
+ private static final byte[] WEBP_CHUNK_TYPE_VP8 = "VP8 ".getBytes(Charset.defaultCharset());
+ private static final byte[] WEBP_CHUNK_TYPE_ANIM = "ANIM".getBytes(Charset.defaultCharset());
+ private static final byte[] WEBP_CHUNK_TYPE_ANMF = "ANMF".getBytes(Charset.defaultCharset());
+ private static final int WEBP_CHUNK_TYPE_VP8X_DEFAULT_LENGTH = 10;
private static final int WEBP_CHUNK_TYPE_BYTE_LENGTH = 4;
private static final int WEBP_CHUNK_SIZE_BYTE_LENGTH = 4;
@@ -1609,7 +1618,7 @@ public class ExifInterface {
}
/**
- * Returns whether ExifInterface currently supports parsing data from the specified mime type
+ * Returns whether ExifInterface currently supports reading data from the specified mime type
* or not.
*
* @param mimeType the string value of mime type
@@ -1633,6 +1642,8 @@ public class ExifInterface {
case "image/x-fuji-raf":
case "image/heic":
case "image/heif":
+ case "image/png":
+ case "image/webp":
return true;
default:
return false;
@@ -2046,18 +2057,21 @@ 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 and PNG files.
+ * This method is supported for JPEG, PNG and WebP files.
* <p class="note">
* Note: after calling this method, any attempts to obtain range information
* from {@link #getAttributeRange(String)} or {@link #getThumbnailRange()}
* will throw {@link IllegalStateException}, since the offsets may have
* changed in the newly written file.
+ * <p>
+ * For WebP format, the Exif data will be stored as an Extended File Format, and it may not be
+ * supported for older readers.
* </p>
*/
public void saveAttributes() throws IOException {
- if (!mIsSupportedFile || (mMimeType != IMAGE_TYPE_JPEG && mMimeType != IMAGE_TYPE_PNG)) {
- throw new IOException("ExifInterface only supports saving attributes on JPEG or PNG "
- + "formats.");
+ if (!isSupportedFormatForSavingAttributes()) {
+ throw new IOException("ExifInterface only supports saving attributes on JPEG, PNG, "
+ + "or WebP formats.");
}
if (mIsInputStream || (mSeekableFileDescriptor == null && mFilename == null)) {
throw new IOException(
@@ -2086,11 +2100,7 @@ public class ExifInterface {
throw new IOException("Couldn't rename to " + tempFile.getAbsolutePath());
}
} else if (mSeekableFileDescriptor != null) {
- if (mMimeType == IMAGE_TYPE_JPEG) {
- tempFile = File.createTempFile("temp", "jpg");
- } else if (mMimeType == IMAGE_TYPE_PNG) {
- tempFile = File.createTempFile("temp", "png");
- }
+ tempFile = File.createTempFile("temp", "tmp");
Os.lseek(mSeekableFileDescriptor, 0, OsConstants.SEEK_SET);
in = new FileInputStream(mSeekableFileDescriptor);
out = new FileOutputStream(tempFile);
@@ -2120,6 +2130,8 @@ public class ExifInterface {
saveJpegAttributes(bufferedIn, bufferedOut);
} else if (mMimeType == IMAGE_TYPE_PNG) {
savePngAttributes(bufferedIn, bufferedOut);
+ } else if (mMimeType == IMAGE_TYPE_WEBP) {
+ saveWebpAttributes(bufferedIn, bufferedOut);
}
}
} catch (Exception e) {
@@ -2601,7 +2613,6 @@ public class ExifInterface {
ByteOrderedDataInputStream signatureInputStream = null;
try {
signatureInputStream = new ByteOrderedDataInputStream(signatureCheckBytes);
- signatureInputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
long chunkSize = signatureInputStream.readInt();
byte[] chunkType = new byte[4];
@@ -3392,6 +3403,9 @@ public class ExifInterface {
bytesRead += in.skipBytes(WEBP_SIGNATURE_2.length);
try {
while (true) {
+ // TODO: Check the first Chunk Type, and if it is VP8X, check if the chunks are
+ // ordered properly.
+
// Each chunk is made up of three parts:
// 1) Chunk FourCC: 4-byte concatenating four ASCII characters.
// 2) Chunk Size: 4-byte unsigned integer indicating the size of the chunk.
@@ -3417,6 +3431,9 @@ public class ExifInterface {
// Save offset values for handling thumbnail and attribute offsets.
mExifOffset = bytesRead;
readExifSegment(payload, IFD_TYPE_PRIMARY);
+
+ // Save offset values for handleThumbnailFromJfif() function
+ mExifOffset = bytesRead;
break;
} else {
// Add a single padding byte at end if chunk size is odd
@@ -3610,6 +3627,263 @@ public class ExifInterface {
copy(dataInputStream, dataOutputStream);
}
+ // A WebP file has a header and a series of chunks.
+ // The header is composed of:
+ // "RIFF" + File Size + "WEBP"
+ //
+ // The structure of the chunks can be divided largely into two categories:
+ // 1) Contains only image data,
+ // 2) Contains image data and extra data.
+ // In the first category, there is only one chunk: type "VP8" (compression with loss) or "VP8L"
+ // (lossless compression).
+ // In the second category, the first chunk will be of type "VP8X", which contains flags
+ // indicating which extra data exist in later chunks. The proceeding chunks must conform to
+ // the following order based on type (if they exist):
+ // Color Profile ("ICCP") + Animation Control Data ("ANIM") + Image Data ("VP8"/"VP8L")
+ // + Exif metadata ("EXIF") + XMP metadata ("XMP")
+ //
+ // And in order to have EXIF data, a WebP file must be of the second structure and thus follow
+ // the following rules:
+ // 1) "VP8X" chunk as the first chunk,
+ // 2) flag for EXIF inside "VP8X" chunk set to 1, and
+ // 3) contain the "EXIF" chunk in the correct order amongst other chunks.
+ //
+ // Based on these rules, this API will support three different cases depending on the contents
+ // of the original file:
+ // 1) "EXIF" chunk already exists
+ // -> replace it with the new "EXIF" chunk
+ // 2) "EXIF" chunk does not exist and the first chunk is "VP8" or "VP8L"
+ // -> add "VP8X" before the "VP8"/"VP8L" chunk (with EXIF flag set to 1), and add new
+ // "EXIF" chunk after the "VP8"/"VP8L" chunk.
+ // 3) "EXIF" chunk does not exist and the first chunk is "VP8X"
+ // -> set EXIF flag in "VP8X" chunk to 1, and add new "EXIF" chunk at the proper location.
+ //
+ // See https://developers.google.com/speed/webp/docs/riff_container for more details.
+ private void saveWebpAttributes(InputStream inputStream, OutputStream outputStream)
+ throws IOException {
+ if (DEBUG) {
+ Log.d(TAG, "saveWebpAttributes starting with (inputStream: " + inputStream
+ + ", outputStream: " + outputStream + ")");
+ }
+ ByteOrderedDataInputStream totalInputStream =
+ new ByteOrderedDataInputStream(inputStream, ByteOrder.LITTLE_ENDIAN);
+ ByteOrderedDataOutputStream totalOutputStream =
+ new ByteOrderedDataOutputStream(outputStream, ByteOrder.LITTLE_ENDIAN);
+
+ // WebP signature
+ copy(totalInputStream, totalOutputStream, WEBP_SIGNATURE_1.length);
+ // File length will be written after all the chunks have been written
+ totalInputStream.skipBytes(WEBP_FILE_SIZE_BYTE_LENGTH + WEBP_SIGNATURE_2.length);
+
+ // Create a separate byte array to calculate file length
+ ByteArrayOutputStream nonHeaderByteArrayOutputStream = null;
+ try {
+ nonHeaderByteArrayOutputStream = new ByteArrayOutputStream();
+ ByteOrderedDataOutputStream nonHeaderOutputStream =
+ new ByteOrderedDataOutputStream(nonHeaderByteArrayOutputStream,
+ ByteOrder.LITTLE_ENDIAN);
+
+ if (mExifOffset != 0) {
+ // EXIF chunk exists in the original file
+ // Tested by webp_with_exif.webp
+ int bytesRead = WEBP_SIGNATURE_1.length + WEBP_FILE_SIZE_BYTE_LENGTH
+ + WEBP_SIGNATURE_2.length;
+ copy(totalInputStream, nonHeaderOutputStream,
+ mExifOffset - bytesRead - WEBP_CHUNK_TYPE_BYTE_LENGTH
+ - WEBP_CHUNK_SIZE_BYTE_LENGTH);
+
+ // Skip input stream to the end of the EXIF chunk
+ totalInputStream.skipBytes(WEBP_CHUNK_TYPE_BYTE_LENGTH);
+ int exifChunkLength = totalInputStream.readInt();
+ totalInputStream.skipBytes(exifChunkLength);
+
+ // Write new EXIF chunk to output stream
+ int exifSize = writeExifSegment(nonHeaderOutputStream);
+ } else {
+ // EXIF chunk does not exist in the original file
+ byte[] firstChunkType = new byte[WEBP_CHUNK_TYPE_BYTE_LENGTH];
+ if (totalInputStream.read(firstChunkType) != firstChunkType.length) {
+ throw new IOException("Encountered invalid length while parsing WebP chunk "
+ + "type");
+ }
+
+ if (Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8X)) {
+ // Original file already includes other extra data
+ int size = totalInputStream.readInt();
+ // WebP files have a single padding byte at the end if the chunk size is odd.
+ byte[] data = new byte[(size % 2) == 1 ? size + 1 : size];
+ totalInputStream.read(data);
+
+ // Set the EXIF flag to 1
+ data[0] = (byte) (data[0] | (1 << 3));
+
+ // Retrieve Animation flag--in order to check where EXIF data should start
+ boolean containsAnimation = ((data[0] >> 1) & 1) == 1;
+
+ // Write the original VP8X chunk
+ nonHeaderOutputStream.write(WEBP_CHUNK_TYPE_VP8X);
+ nonHeaderOutputStream.writeInt(size);
+ nonHeaderOutputStream.write(data);
+
+ // Animation control data is composed of 1 ANIM chunk and multiple ANMF
+ // chunks and since the image data (VP8/VP8L) chunks are included in the ANMF
+ // chunks, EXIF data should come after the last ANMF chunk.
+ // Also, because there is no value indicating the amount of ANMF chunks, we need
+ // to keep iterating through chunks until we either reach the end of the file or
+ // the XMP chunk (if it exists).
+ // Tested by webp_with_anim_without_exif.webp
+ if (containsAnimation) {
+ copyChunksUpToGivenChunkType(totalInputStream, nonHeaderOutputStream,
+ WEBP_CHUNK_TYPE_ANIM, null);
+
+ while (true) {
+ byte[] type = new byte[WEBP_CHUNK_TYPE_BYTE_LENGTH];
+ int read = inputStream.read(type);
+ if (!Arrays.equals(type, WEBP_CHUNK_TYPE_ANMF)) {
+ // Either we have reached EOF or the start of a non-ANMF chunk
+ writeExifSegment(nonHeaderOutputStream);
+ break;
+ }
+ copyWebPChunk(totalInputStream, nonHeaderOutputStream, type);
+ }
+ } else {
+ // Skip until we find the VP8 or VP8L chunk
+ copyChunksUpToGivenChunkType(totalInputStream, nonHeaderOutputStream,
+ WEBP_CHUNK_TYPE_VP8, WEBP_CHUNK_TYPE_VP8L);
+ writeExifSegment(nonHeaderOutputStream);
+ }
+ } else if (Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8)
+ || Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8L)) {
+ int size = totalInputStream.readInt();
+ int bytesToRead = size;
+ // WebP files have a single padding byte at the end if the chunk size is odd.
+ if (size % 2 == 1) {
+ bytesToRead += 1;
+ }
+
+ // Retrieve image width/height
+ int widthAndHeight = 0;
+ int width = 0;
+ int height = 0;
+ int alpha = 0;
+ // Save VP8 frame data for later
+ byte[] vp8Frame = new byte[3];
+
+ if (Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8)) {
+ totalInputStream.read(vp8Frame);
+
+ // Check signature
+ byte[] vp8Signature = new byte[3];
+ if (totalInputStream.read(vp8Signature) != vp8Signature.length
+ || !Arrays.equals(WEBP_VP8_SIGNATURE, vp8Signature)) {
+ throw new IOException("Encountered error while checking VP8 "
+ + "signature");
+ }
+
+ // Retrieve image width/height
+ widthAndHeight = totalInputStream.readInt();
+ width = (widthAndHeight << 18) >> 18;
+ height = (widthAndHeight << 2) >> 18;
+ bytesToRead -= (vp8Frame.length + vp8Signature.length + 4);
+ } else if (Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8L)) {
+ // Check signature
+ byte vp8lSignature = totalInputStream.readByte();
+ if (vp8lSignature != WEBP_VP8L_SIGNATURE) {
+ throw new IOException("Encountered error while checking VP8L "
+ + "signature");
+ }
+
+ // Retrieve image width/height
+ widthAndHeight = totalInputStream.readInt();
+ // VP8L stores width - 1 and height - 1 values. See "2 RIFF Header" of
+ // "WebP Lossless Bitstream Specification"
+ width = ((widthAndHeight << 18) >> 18) + 1;
+ height = ((widthAndHeight << 4) >> 18) + 1;
+ // Retrieve alpha bit
+ alpha = widthAndHeight & (1 << 3);
+ bytesToRead -= (1 /* VP8L signature */ + 4);
+ }
+
+ // Create VP8X with Exif flag set to 1
+ nonHeaderOutputStream.write(WEBP_CHUNK_TYPE_VP8X);
+ nonHeaderOutputStream.writeInt(WEBP_CHUNK_TYPE_VP8X_DEFAULT_LENGTH);
+ byte[] data = new byte[WEBP_CHUNK_TYPE_VP8X_DEFAULT_LENGTH];
+ // EXIF flag
+ data[0] = (byte) (data[0] | (1 << 3));
+ // ALPHA flag
+ data[0] = (byte) (data[0] | (alpha << 4));
+ // VP8X stores Width - 1 and Height - 1 values
+ width -= 1;
+ height -= 1;
+ data[4] = (byte) width;
+ data[5] = (byte) (width >> 8);
+ data[6] = (byte) (width >> 16);
+ data[7] = (byte) height;
+ data[8] = (byte) (height >> 8);
+ data[9] = (byte) (height >> 16);
+ nonHeaderOutputStream.write(data);
+
+ // Write VP8 or VP8L data
+ nonHeaderOutputStream.write(firstChunkType);
+ nonHeaderOutputStream.writeInt(size);
+ if (Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8)) {
+ nonHeaderOutputStream.write(vp8Frame);
+ nonHeaderOutputStream.write(WEBP_VP8_SIGNATURE);
+ nonHeaderOutputStream.writeInt(widthAndHeight);
+ } else if (Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8L)) {
+ nonHeaderOutputStream.write(WEBP_VP8L_SIGNATURE);
+ nonHeaderOutputStream.writeInt(widthAndHeight);
+ }
+ copy(totalInputStream, nonHeaderOutputStream, bytesToRead);
+
+ // Write EXIF chunk
+ writeExifSegment(nonHeaderOutputStream);
+ }
+ }
+
+ // Copy the rest of the file
+ copy(totalInputStream, nonHeaderOutputStream);
+
+ // Write file length + second signature
+ totalOutputStream.writeInt(nonHeaderByteArrayOutputStream.size()
+ + WEBP_SIGNATURE_2.length);
+ totalOutputStream.write(WEBP_SIGNATURE_2);
+ nonHeaderByteArrayOutputStream.writeTo(totalOutputStream);
+ } catch (Exception e) {
+ throw new IOException("Failed to save WebP file", e);
+ } finally {
+ closeQuietly(nonHeaderByteArrayOutputStream);
+ }
+ }
+
+ private void copyChunksUpToGivenChunkType(ByteOrderedDataInputStream inputStream,
+ ByteOrderedDataOutputStream outputStream, byte[] firstGivenType,
+ byte[] secondGivenType) throws IOException {
+ while (true) {
+ byte[] type = new byte[WEBP_CHUNK_TYPE_BYTE_LENGTH];
+ if (inputStream.read(type) != type.length) {
+ throw new IOException("Encountered invalid length while copying WebP chunks up to"
+ + "chunk type " + new String(firstGivenType, ASCII)
+ + ((secondGivenType == null) ? "" : " or " + new String(secondGivenType,
+ ASCII)));
+ }
+ copyWebPChunk(inputStream, outputStream, type);
+ if (Arrays.equals(type, firstGivenType)
+ || (secondGivenType != null && Arrays.equals(type, secondGivenType))) {
+ break;
+ }
+ }
+ }
+
+ private void copyWebPChunk(ByteOrderedDataInputStream inputStream,
+ ByteOrderedDataOutputStream outputStream, byte[] type) throws IOException {
+ int size = inputStream.readInt();
+ outputStream.write(type);
+ outputStream.writeInt(size);
+ // WebP files have a single padding byte at the end if the chunk size is odd.
+ copy(inputStream, outputStream, (size % 2) == 1 ? size + 1 : size);
+ }
+
// Reads the given EXIF byte area and save its tag data into attributes.
private void readExifSegment(byte[] exifBytes, int imageType) throws IOException {
ByteOrderedDataInputStream dataInputStream =
@@ -4360,14 +4634,22 @@ 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);
+ switch (mMimeType) {
+ case IMAGE_TYPE_JPEG:
+ // Write JPEG specific data (APP1 size, APP1 identifier)
+ dataOutputStream.writeUnsignedShort(totalSize);
+ dataOutputStream.write(IDENTIFIER_EXIF_APP1);
+ break;
+ case IMAGE_TYPE_PNG:
+ // Write PNG specific data (chunk size, chunk type)
+ dataOutputStream.writeInt(totalSize);
+ dataOutputStream.write(PNG_CHUNK_TYPE_EXIF);
+ break;
+ case IMAGE_TYPE_WEBP:
+ // Write WebP specific data (chunk type, chunk size)
+ dataOutputStream.write(WEBP_CHUNK_TYPE_EXIF);
+ dataOutputStream.writeInt(totalSize);
+ break;
}
// Write TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1.
@@ -4436,6 +4718,11 @@ public class ExifInterface {
dataOutputStream.write(getThumbnailBytes());
}
+ // For WebP files, add a single padding byte at end if chunk size is odd
+ if (mMimeType == IMAGE_TYPE_WEBP && totalSize % 2 == 1) {
+ dataOutputStream.writeByte(0);
+ }
+
// Reset the byte order to big endian in order to write remaining parts of the JPEG file.
dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
@@ -4537,12 +4824,17 @@ public class ExifInterface {
private int mPosition;
public ByteOrderedDataInputStream(InputStream in) throws IOException {
+ this(in, ByteOrder.BIG_ENDIAN);
+ }
+
+ ByteOrderedDataInputStream(InputStream in, ByteOrder byteOrder) throws IOException {
mInputStream = in;
mDataInputStream = new DataInputStream(in);
mLength = mDataInputStream.available();
mPosition = 0;
// TODO (b/142218289): Need to handle case where input stream does not support mark
mDataInputStream.mark(mLength);
+ mByteOrder = byteOrder;
}
public ByteOrderedDataInputStream(byte[] bytes) throws IOException {
@@ -4867,4 +5159,12 @@ public class ExifInterface {
}
}
}
+
+ private boolean isSupportedFormatForSavingAttributes() {
+ if (mIsSupportedFile && (mMimeType == IMAGE_TYPE_JPEG || mMimeType == IMAGE_TYPE_PNG
+ || mMimeType == IMAGE_TYPE_WEBP)) {
+ return true;
+ }
+ return false;
+ }
}