Merge "ExifInterface: Process uncompressed thumbnail"
This commit is contained in:
committed by
Android (Google) Code Review
commit
54e43b23e1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user