ExifInterface: Process uncompressed thumbnail
An uncompressed thumbnail may use 24-bit RGB format to store image
data. This CL handles uncompressed thumbnail images and creates a
bitmap object by using the given byte data.
Bug: 28156704
Change-Id: Ie650de4398004dfa74519817e417c7002d4fbdbb
diff --git a/api/current.txt b/api/current.txt
index 64838a4..a756c6a 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -20126,8 +20126,11 @@
method public int getAttributeInt(java.lang.String, int);
method public boolean getLatLong(float[]);
method public byte[] getThumbnail();
+ method public android.graphics.Bitmap getThumbnailBitmap();
+ method public byte[] getThumbnailBytes();
method public long[] getThumbnailRange();
method public boolean hasThumbnail();
+ method public boolean isThumbnailCompressed();
method public void saveAttributes() throws java.io.IOException;
method public void setAttribute(java.lang.String, java.lang.String);
field public static final int ORIENTATION_FLIP_HORIZONTAL = 2; // 0x2
diff --git a/api/system-current.txt b/api/system-current.txt
index c7a05b2..357041f 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -21638,8 +21638,11 @@
method public int getAttributeInt(java.lang.String, int);
method public boolean getLatLong(float[]);
method public byte[] getThumbnail();
+ method public android.graphics.Bitmap getThumbnailBitmap();
+ method public byte[] getThumbnailBytes();
method public long[] getThumbnailRange();
method public boolean hasThumbnail();
+ method public boolean isThumbnailCompressed();
method public void saveAttributes() throws java.io.IOException;
method public void setAttribute(java.lang.String, java.lang.String);
field public static final int ORIENTATION_FLIP_HORIZONTAL = 2; // 0x2
diff --git a/api/test-current.txt b/api/test-current.txt
index 881d290..ed8217b 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -20196,8 +20196,11 @@
method public int getAttributeInt(java.lang.String, int);
method public boolean getLatLong(float[]);
method public byte[] getThumbnail();
+ method public android.graphics.Bitmap getThumbnailBitmap();
+ method public byte[] getThumbnailBytes();
method public long[] getThumbnailRange();
method public boolean hasThumbnail();
+ method public boolean isThumbnailCompressed();
method public void saveAttributes() throws java.io.IOException;
method public void setAttribute(java.lang.String, java.lang.String);
field public static final int ORIENTATION_FLIP_HORIZONTAL = 2; // 0x2
diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java
index 591ceba..b675afe 100644
--- a/media/java/android/media/ExifInterface.java
+++ b/media/java/android/media/ExifInterface.java
@@ -46,6 +46,7 @@
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Arrays;
+import java.util.LinkedList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
@@ -61,7 +62,7 @@
/**
* This is a class for reading and writing Exif tags in a JPEG file or a RAW image file.
* <p>
- * Supported formats are: JPEG, DNG, CR2, NEF, NRW, ARW, RW2, ORF and RAF.
+ * Supported formats are: JPEG, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW and RAF.
* <p>
* Attribute mutation is supported for JPEG image files.
*/
@@ -516,6 +517,12 @@
private static final int DATA_LOSSY_JPEG = 34892;
/**
+ * Constant used for BitsPerSample tag.
+ * See TIFF 6.0 Spec Section 6: RGB Full Color Images, Differences from Palette Color Images
+ */
+ private static final int[] BITS_PER_SAMPLE_RGB = new int[] {8, 8, 8};
+
+ /**
* Constants used for NewSubfileType tag.
* See TIFF 6.0 Spec Section 8
* */
@@ -1261,6 +1268,7 @@
private int mThumbnailOffset;
private int mThumbnailLength;
private byte[] mThumbnailBytes;
+ private int mThumbnailCompression;
private int mExifOffset;
private int mOrfMakerNoteOffset;
private int mOrfThumbnailOffset;
@@ -1843,11 +1851,23 @@
}
/**
- * Returns the thumbnail inside the image file, or {@code null} if there is no thumbnail.
- * The returned data is in JPEG format and can be decoded using
+ * Returns the JPEG compressed thumbnail inside the image file, or {@code null} if there is no
+ * JPEG compressed thumbnail.
+ * The returned data can be decoded using
* {@link android.graphics.BitmapFactory#decodeByteArray(byte[],int,int)}
*/
public byte[] getThumbnail() {
+ if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) {
+ return getThumbnailBytes();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the thumbnail bytes inside the image file, regardless of the compression type of the
+ * thumbnail image.
+ */
+ public byte[] getThumbnailBytes() {
if (!mHasThumbnail) {
return null;
}
@@ -1879,6 +1899,7 @@
if (in.read(buffer) != mThumbnailLength) {
throw new IOException("Corrupted image");
}
+ mThumbnailBytes = buffer;
return buffer;
} catch (IOException | ErrnoException e) {
// Couldn't get a thumbnail image.
@@ -1889,6 +1910,52 @@
}
/**
+ * Creates and returns a Bitmap object of the thumbnail image based on the byte array and the
+ * thumbnail compression value, or {@code null} if the compression type is unsupported.
+ */
+ public Bitmap getThumbnailBitmap() {
+ if (!mHasThumbnail) {
+ return null;
+ } else if (mThumbnailBytes == null) {
+ mThumbnailBytes = getThumbnailBytes();
+ }
+
+ if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) {
+ return BitmapFactory.decodeByteArray(mThumbnailBytes, 0, mThumbnailLength);
+ } else if (mThumbnailCompression == DATA_UNCOMPRESSED) {
+ int[] rgbValues = new int[mThumbnailBytes.length / 3];
+ byte alpha = (byte) 0xff000000;
+ for (int i = 0; i < rgbValues.length; i++) {
+ rgbValues[i] = alpha + (mThumbnailBytes[3 * i] << 16)
+ + (mThumbnailBytes[3 * i + 1] << 8) + mThumbnailBytes[3 * i + 2];
+ }
+
+ ExifAttribute imageLengthAttribute =
+ (ExifAttribute) mAttributes[IFD_THUMBNAIL_HINT].get(TAG_IMAGE_LENGTH);
+ ExifAttribute imageWidthAttribute =
+ (ExifAttribute) mAttributes[IFD_THUMBNAIL_HINT].get(TAG_IMAGE_WIDTH);
+ if (imageLengthAttribute != null && imageWidthAttribute != null) {
+ int imageLength = imageLengthAttribute.getIntValue(mExifByteOrder);
+ int imageWidth = imageWidthAttribute.getIntValue(mExifByteOrder);
+ return Bitmap.createBitmap(
+ rgbValues, imageWidth, imageLength, Bitmap.Config.ARGB_8888);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns true if thumbnail image is JPEG Compressed, or false if either thumbnail image does
+ * not exist or thumbnail image is uncompressed.
+ */
+ public boolean isThumbnailCompressed() {
+ if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
* Returns the offset and length of thumbnail inside the image file, or
* {@code null} if there is no thumbnail.
*
@@ -2944,33 +3011,27 @@
ExifAttribute compressionAttribute =
(ExifAttribute) thumbnailData.get(TAG_COMPRESSION);
if (compressionAttribute != null) {
- int compressionValue = compressionAttribute.getIntValue(mExifByteOrder);
- switch (compressionValue) {
- case DATA_UNCOMPRESSED: {
- // TODO: add implementation for reading uncompressed thumbnail data (b/28156704)
- Log.d(TAG, "Uncompressed thumbnail data cannot be processed");
- break;
- }
+ mThumbnailCompression = compressionAttribute.getIntValue(mExifByteOrder);
+ switch (mThumbnailCompression) {
case DATA_JPEG: {
handleThumbnailFromJfif(in, thumbnailData);
break;
}
+ case DATA_UNCOMPRESSED:
case DATA_JPEG_COMPRESSED: {
handleThumbnailFromStrips(in, thumbnailData);
break;
}
- default: {
- break;
- }
}
} else {
// Thumbnail data may not contain Compression tag value
+ mThumbnailCompression = DATA_JPEG;
handleThumbnailFromJfif(in, thumbnailData);
}
}
- // Check JpegInterchangeFormat(JFIF) tags to retrieve thumbnail offset & length values and
- // create a bitmap based on those values
+ // Check JpegInterchangeFormat(JFIF) tags to retrieve thumbnail offset & length values
+ // and reads the corresponding bytes if stream does not support seek function
private void handleThumbnailFromJfif(InputStream in, HashMap thumbnailData) throws IOException {
ExifAttribute jpegInterchangeFormatAttribute =
(ExifAttribute) thumbnailData.get(TAG_JPEG_INTERCHANGE_FORMAT);
@@ -2978,75 +3039,99 @@
(ExifAttribute) thumbnailData.get(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
if (jpegInterchangeFormatAttribute != null
&& jpegInterchangeFormatLengthAttribute != null) {
- int jpegInterchangeFormat =
- jpegInterchangeFormatAttribute.getIntValue(mExifByteOrder);
- int jpegInterchangeFormatLength =
- jpegInterchangeFormatLengthAttribute.getIntValue(mExifByteOrder);
- createJpegThumbnailBitmap(in, jpegInterchangeFormat, jpegInterchangeFormatLength);
- }
- }
+ int thumbnailOffset = jpegInterchangeFormatAttribute.getIntValue(mExifByteOrder);
+ int thumbnailLength = jpegInterchangeFormatLengthAttribute.getIntValue(mExifByteOrder);
- // Check StripOffsets & StripByteCounts tags to retrieve thumbnail offset & length values and
- // create a bitmap based on those values
- private void handleThumbnailFromStrips(InputStream in, HashMap thumbnailData)
- throws IOException {
- ExifAttribute stripOffsetsAttribute =
- (ExifAttribute) thumbnailData.get(TAG_STRIP_OFFSETS);
- ExifAttribute stripByteCountsAttribute =
- (ExifAttribute) thumbnailData.get(TAG_STRIP_BYTE_COUNTS);
- if (stripOffsetsAttribute != null && stripByteCountsAttribute != null) {
- long[] stripOffsetsArray =
- (long[]) stripOffsetsAttribute.getValue(mExifByteOrder);
- long[] stripByteCountsArray =
- (long[]) stripByteCountsAttribute.getValue(mExifByteOrder);
- if (stripOffsetsArray.length == 1) {
- int stripOffsetsSum = (int) Arrays.stream(stripOffsetsArray).sum();
- int stripByteCountsSum = (int) Arrays.stream(stripByteCountsArray).sum();
- createJpegThumbnailBitmap(in, stripOffsetsSum, stripByteCountsSum);
- } else {
- // TODO: implement method to read multiple strips (b/29737797)
- Log.d(TAG, "Multiple strip thumbnail data cannot be processed");
+ // The following code limits the size of thumbnail size not to overflow EXIF data area.
+ thumbnailLength = Math.min(thumbnailLength, in.available() - thumbnailOffset);
+ if (mMimeType == IMAGE_TYPE_JPEG || mMimeType == IMAGE_TYPE_RAF
+ || mMimeType == IMAGE_TYPE_RW2) {
+ thumbnailOffset += mExifOffset;
+ } else if (mMimeType == IMAGE_TYPE_ORF) {
+ // Update offset value since RAF files have IFD data preceding MakerNote data.
+ thumbnailOffset += mOrfMakerNoteOffset;
}
- }
- }
-
- // Creates a bitmap data based on thumbnail offset and length for JPEG Compression
- private void createJpegThumbnailBitmap(InputStream in, int thumbnailOffset, int thumbnailLength)
- throws IOException {
- // The following code limits the size of thumbnail size not to overflow EXIF data area.
- thumbnailLength = Math.min(thumbnailLength, in.available() - thumbnailOffset);
- if (mMimeType == IMAGE_TYPE_JPEG || mMimeType == IMAGE_TYPE_RAF
- || mMimeType == IMAGE_TYPE_RW2) {
- thumbnailOffset += mExifOffset;
- } else if (mMimeType == IMAGE_TYPE_ORF) {
- // Update offset value since RAF files have IFD data preceding MakerNote data.
- thumbnailOffset += mOrfMakerNoteOffset;
- }
- if (DEBUG) {
- Log.d(TAG, "Creating JPEG Thumbnail with offset: " + thumbnailOffset);
- }
- if (thumbnailOffset > 0 && thumbnailLength > 0) {
- mHasThumbnail = true;
- mThumbnailOffset = thumbnailOffset;
- mThumbnailLength = thumbnailLength;
- if (mFilename == null && mAssetInputStream == null && mSeekableFileDescriptor == null) {
- // Save the thumbnail in memory if the input doesn't support reading again.
- byte[] thumbnailBytes = new byte[thumbnailLength];
- in.skip(thumbnailOffset);
- in.read(thumbnailBytes);
- mThumbnailBytes = thumbnailBytes;
- if (DEBUG) {
- Bitmap bitmap = BitmapFactory.decodeByteArray(
- thumbnailBytes, 0, thumbnailBytes.length);
- Log.d(TAG, "Thumbnail offset: " + mThumbnailOffset + ", length: "
- + mThumbnailLength + ", width: " + bitmap.getWidth()
- + ", height: "
- + bitmap.getHeight());
+ if (DEBUG) {
+ Log.d(TAG, "Setting thumbnail attributes with offset: " + thumbnailOffset);
+ }
+ if (thumbnailOffset > 0 && thumbnailLength > 0) {
+ mHasThumbnail = true;
+ mThumbnailOffset = thumbnailOffset;
+ mThumbnailLength = thumbnailLength;
+ if (mFilename == null && mAssetInputStream == null
+ && mSeekableFileDescriptor == null) {
+ // Save the thumbnail in memory if the input doesn't support reading again.
+ byte[] thumbnailBytes = new byte[thumbnailLength];
+ in.skip(thumbnailOffset);
+ in.read(thumbnailBytes);
+ mThumbnailBytes = thumbnailBytes;
}
}
}
}
+ // Check StripOffsets & StripByteCounts tags to retrieve thumbnail offset & length values
+ private void handleThumbnailFromStrips(InputStream in, HashMap thumbnailData)
+ throws IOException {
+ ExifAttribute bitsPerSampleAttribute =
+ (ExifAttribute) thumbnailData.get(TAG_BITS_PER_SAMPLE);
+
+ if (bitsPerSampleAttribute != null) {
+ int[] bitsPerSampleValue = (int[]) bitsPerSampleAttribute.getValue(mExifByteOrder);
+
+ if (Arrays.equals(BITS_PER_SAMPLE_RGB, bitsPerSampleValue)) {
+ ExifAttribute stripOffsetsAttribute =
+ (ExifAttribute) thumbnailData.get(TAG_STRIP_OFFSETS);
+ ExifAttribute stripByteCountsAttribute =
+ (ExifAttribute) thumbnailData.get(TAG_STRIP_BYTE_COUNTS);
+
+ if (stripOffsetsAttribute != null && stripByteCountsAttribute != null) {
+ long[] stripOffsets =
+ (long[]) stripOffsetsAttribute.getValue(mExifByteOrder);
+ long[] stripByteCounts =
+ (long[]) stripByteCountsAttribute.getValue(mExifByteOrder);
+
+ // Set thumbnail byte array data for non-consecutive strip bytes
+ byte[] totalStripBytes =
+ new byte[(int) Arrays.stream(stripByteCounts).sum()];
+
+ int bytesRead = 0;
+ int bytesAdded = 0;
+ for (int i = 0; i < stripOffsets.length; i++) {
+ int stripOffset = (int) stripOffsets[i];
+ int stripByteCount = (int) stripByteCounts[i];
+
+ // Skip to offset
+ int skipBytes = stripOffset - bytesRead;
+ if (skipBytes < 0) {
+ Log.d(TAG, "Invalid strip offset value");
+ }
+ in.skip(skipBytes);
+ bytesRead += skipBytes;
+
+ // Read strip bytes
+ byte[] stripBytes = new byte[stripByteCount];
+ in.read(stripBytes);
+ bytesRead += stripByteCount;
+
+ // Add bytes to array
+ System.arraycopy(stripBytes, 0, totalStripBytes, bytesAdded,
+ stripBytes.length);
+ bytesAdded += stripBytes.length;
+ }
+
+ mHasThumbnail = true;
+ mThumbnailBytes = totalStripBytes;
+ }
+ }
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "Only Uncompressed RGB data process is supported");
+ }
+ return;
+ }
+ }
+
// Returns true if the image length and width values are <= 512.
// See Section 4.8 of http://standardsproposals.bsigroup.com/Home/getPDF/567
private boolean isThumbnail(HashMap map) throws IOException {
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/ExifInterfaceTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/ExifInterfaceTest.java
index db326ba..7ee3634 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/ExifInterfaceTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/ExifInterfaceTest.java
@@ -267,8 +267,7 @@
if (expectedValue.hasThumbnail) {
byte[] thumbnailBytes = exifInterface.getThumbnail();
assertNotNull(thumbnailBytes);
- Bitmap thumbnailBitmap =
- BitmapFactory.decodeByteArray(thumbnailBytes, 0, thumbnailBytes.length);
+ Bitmap thumbnailBitmap = exifInterface.getThumbnailBitmap();
assertNotNull(thumbnailBitmap);
assertEquals(expectedValue.thumbnailWidth, thumbnailBitmap.getWidth());
assertEquals(expectedValue.thumbnailHeight, thumbnailBitmap.getHeight());