From 043785660ca6450c96408377fe563f627d036353 Mon Sep 17 00:00:00 2001 From: Jin Park Date: Thu, 21 Jul 2016 10:21:41 +0900 Subject: [PATCH] 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 --- api/current.txt | 3 + api/system-current.txt | 3 + api/test-current.txt | 3 + media/java/android/media/ExifInterface.java | 239 ++++++++++++------ .../unit/ExifInterfaceTest.java | 3 +- 5 files changed, 172 insertions(+), 79 deletions(-) diff --git a/api/current.txt b/api/current.txt index 64838a4ef0533..a756c6a1c397f 100644 --- a/api/current.txt +++ b/api/current.txt @@ -20126,8 +20126,11 @@ package android.media { 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 c7a05b2cd826f..357041f49ff31 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -21638,8 +21638,11 @@ package android.media { 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 881d290536c52..ed8217bb4a4cf 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -20196,8 +20196,11 @@ package android.media { 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 591ceba371f7f..b675afe70199f 100644 --- a/media/java/android/media/ExifInterface.java +++ b/media/java/android/media/ExifInterface.java @@ -46,6 +46,7 @@ import java.nio.charset.StandardCharsets; 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 @@ import libcore.io.Streams; /** * This is a class for reading and writing Exif tags in a JPEG file or a RAW image file. *

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

* Attribute mutation is supported for JPEG image files. */ @@ -515,6 +516,12 @@ public class ExifInterface { private static final int DATA_PACK_BITS_COMPRESSED = 32773; 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 @@ public class ExifInterface { 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 @@ public class ExifInterface { } /** - * 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 @@ public class ExifInterface { if (in.read(buffer) != mThumbnailLength) { throw new IOException("Corrupted image"); } + mThumbnailBytes = buffer; return buffer; } catch (IOException | ErrnoException e) { // Couldn't get a thumbnail image. @@ -1888,6 +1909,52 @@ public class ExifInterface { return null; } + /** + * 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 @@ public class ExifInterface { 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 @@ public class ExifInterface { (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 db326ba6e2c20..7ee36340694ee 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 @@ public class ExifInterfaceTest extends AndroidTestCase { 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());