Merge "ExifInterface: Process uncompressed thumbnail"

This commit is contained in:
Jin Seok Park
2016-08-08 00:53:08 +00:00
committed by Android (Google) Code Review
5 changed files with 172 additions and 79 deletions

View File

@@ -20131,8 +20131,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

View File

@@ -21652,8 +21652,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

View File

@@ -20201,8 +20201,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

View File

@@ -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,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 {

View File

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