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