Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 04378566 authored by Jin Park's avatar Jin Park
Browse files

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
parent 909f6c48
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -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
+3 −0
Original line number Diff line number Diff line
@@ -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
+3 −0
Original line number Diff line number Diff line
@@ -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
+158 −73
Original line number Diff line number Diff line
@@ -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.
 * <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.
 */
@@ -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,41 +3039,9 @@ 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");
            }
        }
    }

    // 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
@@ -3023,30 +3052,86 @@ public class ExifInterface {
                thumbnailOffset += mOrfMakerNoteOffset;
            }
            if (DEBUG) {
            Log.d(TAG, "Creating JPEG Thumbnail with offset: " + thumbnailOffset);
                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) {
                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());
                }
            }
        }
    }

    // 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 {
+1 −2
Original line number Diff line number Diff line
@@ -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());