Merge "ExifInterface: add RAW input stream support" into nyc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
bd1dc40a3b
@@ -16,6 +16,8 @@
|
||||
|
||||
package android.media;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.content.res.AssetManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.system.ErrnoException;
|
||||
@@ -24,6 +26,7 @@ import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
@@ -145,6 +148,7 @@ public class ExifInterface {
|
||||
private static final String TAG_HAS_THUMBNAIL = "hasThumbnail";
|
||||
private static final String TAG_THUMBNAIL_OFFSET = "thumbnailOffset";
|
||||
private static final String TAG_THUMBNAIL_LENGTH = "thumbnailLength";
|
||||
private static final String TAG_THUMBNAIL_DATA = "thumbnailData";
|
||||
|
||||
// Constants used for the Orientation Exif tag.
|
||||
public static final int ORIENTATION_UNDEFINED = 0;
|
||||
@@ -163,6 +167,9 @@ public class ExifInterface {
|
||||
public static final int WHITEBALANCE_AUTO = 0;
|
||||
public static final int WHITEBALANCE_MANUAL = 1;
|
||||
|
||||
private static final byte[] JPEG_SIGNATURE = new byte[] {(byte) 0xff, (byte) 0xd8, (byte) 0xff};
|
||||
private static final int JPEG_SIGNATURE_SIZE = 3;
|
||||
|
||||
private static SimpleDateFormat sFormatter;
|
||||
|
||||
// See Exchangeable image file format for digital still cameras: Exif version 2.2.
|
||||
@@ -408,8 +415,8 @@ public class ExifInterface {
|
||||
// Mappings from tag number to tag name and each item represents one IFD tag group.
|
||||
private static final HashMap[] sExifTagMapsForReading = new HashMap[EXIF_TAGS.length];
|
||||
// Mapping from tag name to tag number and the corresponding tag group.
|
||||
private static final HashMap<String, Pair<Integer, Integer>> sExifTagMapForWriting
|
||||
= new HashMap<>();
|
||||
private static final HashMap<String, Pair<Integer, Integer>> sExifTagMapForWriting =
|
||||
new HashMap<>();
|
||||
|
||||
// See JPEG File Interchange Format Version 1.02.
|
||||
// The following values are defined for handling JPEG streams. In this implementation, we are
|
||||
@@ -443,7 +450,7 @@ public class ExifInterface {
|
||||
|
||||
static {
|
||||
System.loadLibrary("media_jni");
|
||||
initRawNative();
|
||||
nativeInitRaw();
|
||||
sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
|
||||
sFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
|
||||
@@ -468,10 +475,10 @@ public class ExifInterface {
|
||||
}
|
||||
|
||||
private final String mFilename;
|
||||
private final FileDescriptor mFileDescriptor;
|
||||
private final InputStream mInputStream;
|
||||
private boolean mIsRaw;
|
||||
private final FileDescriptor mSeekableFileDescriptor;
|
||||
private final AssetManager.AssetInputStream mAssetInputStream;
|
||||
private final HashMap<String, String> mAttributes = new HashMap<>();
|
||||
private boolean mIsRaw;
|
||||
private boolean mHasThumbnail;
|
||||
// The following values used for indicating a thumbnail position.
|
||||
private int mThumbnailOffset;
|
||||
@@ -488,23 +495,33 @@ public class ExifInterface {
|
||||
if (filename == null) {
|
||||
throw new IllegalArgumentException("filename cannot be null");
|
||||
}
|
||||
FileInputStream in = new FileInputStream(filename);
|
||||
mAssetInputStream = null;
|
||||
mFilename = filename;
|
||||
mFileDescriptor = null;
|
||||
mInputStream = new FileInputStream(filename);
|
||||
loadAttributes();
|
||||
if (isSeekableFD(in.getFD())) {
|
||||
mSeekableFileDescriptor = in.getFD();
|
||||
} else {
|
||||
mSeekableFileDescriptor = null;
|
||||
}
|
||||
loadAttributes(in);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads Exif tags from the specified image file descriptor.
|
||||
* Reads Exif tags from the specified image file descriptor. Attribute mutation is supported
|
||||
* for seekable file descriptors only.
|
||||
*/
|
||||
public ExifInterface(FileDescriptor fileDescriptor) throws IOException {
|
||||
if (fileDescriptor == null) {
|
||||
throw new IllegalArgumentException("parcelFileDescriptor cannot be null");
|
||||
}
|
||||
mAssetInputStream = null;
|
||||
mFilename = null;
|
||||
mFileDescriptor = fileDescriptor;
|
||||
mInputStream = new FileInputStream(fileDescriptor);
|
||||
loadAttributes();
|
||||
if (isSeekableFD(fileDescriptor)) {
|
||||
mSeekableFileDescriptor = fileDescriptor;
|
||||
} else {
|
||||
mSeekableFileDescriptor = null;
|
||||
}
|
||||
loadAttributes(new FileInputStream(fileDescriptor));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -516,9 +533,18 @@ public class ExifInterface {
|
||||
throw new IllegalArgumentException("inputStream cannot be null");
|
||||
}
|
||||
mFilename = null;
|
||||
mFileDescriptor = null;
|
||||
mInputStream = inputStream;
|
||||
loadAttributes();
|
||||
if (inputStream instanceof AssetManager.AssetInputStream) {
|
||||
mAssetInputStream = (AssetManager.AssetInputStream) inputStream;
|
||||
mSeekableFileDescriptor = null;
|
||||
} else if (inputStream instanceof FileInputStream
|
||||
&& isSeekableFD(((FileInputStream) inputStream).getFD())) {
|
||||
mAssetInputStream = null;
|
||||
mSeekableFileDescriptor = ((FileInputStream) inputStream).getFD();
|
||||
} else {
|
||||
mAssetInputStream = null;
|
||||
mSeekableFileDescriptor = null;
|
||||
}
|
||||
loadAttributes(inputStream);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -587,77 +613,93 @@ public class ExifInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize mAttributes with the attributes from the file mFilename.
|
||||
*
|
||||
* mAttributes is a HashMap which stores the Exif attributes of the file.
|
||||
* The key is the standard tag name and the value is the tag's value: e.g.
|
||||
* Model -> Nikon. Numeric values are stored as strings.
|
||||
*
|
||||
* This function also initialize mHasThumbnail to indicate whether the
|
||||
* file has a thumbnail inside.
|
||||
* This function decides which parser to read the image data according to the given input stream
|
||||
* type and the content of the input stream. In each case, it reads the first three bytes to
|
||||
* determine whether the image data format is JPEG or not.
|
||||
*/
|
||||
private void loadAttributes() throws IOException {
|
||||
FileInputStream in = null;
|
||||
try {
|
||||
if (mFilename != null) {
|
||||
in = new FileInputStream(mFilename);
|
||||
private void loadAttributes(@NonNull InputStream in) throws IOException {
|
||||
// Process RAW input stream
|
||||
if (mAssetInputStream != null) {
|
||||
long asset = mAssetInputStream.getNativeAsset();
|
||||
if (handleRawResult(nativeGetRawAttributesFromAsset(asset))) {
|
||||
return;
|
||||
}
|
||||
if (mFileDescriptor != null) {
|
||||
in = new FileInputStream(mFileDescriptor);
|
||||
} else if (mSeekableFileDescriptor != null) {
|
||||
if (handleRawResult(nativeGetRawAttributesFromFileDescriptor(
|
||||
mSeekableFileDescriptor))) {
|
||||
return;
|
||||
}
|
||||
if (in != null) {
|
||||
// First test whether a given file is a one of RAW format or not.
|
||||
HashMap map = getRawAttributesNative(Os.dup(in.getFD()));
|
||||
mIsRaw = map != null;
|
||||
if (mIsRaw) {
|
||||
for (Object obj : map.entrySet()) {
|
||||
Map.Entry entry = (Map.Entry) obj;
|
||||
String attrName = (String) entry.getKey();
|
||||
String attrValue = (String) entry.getValue();
|
||||
|
||||
switch (attrName) {
|
||||
case TAG_HAS_THUMBNAIL:
|
||||
mHasThumbnail = attrValue.equalsIgnoreCase("true");
|
||||
break;
|
||||
case TAG_THUMBNAIL_OFFSET:
|
||||
mThumbnailOffset = Integer.parseInt(attrValue);
|
||||
break;
|
||||
case TAG_THUMBNAIL_LENGTH:
|
||||
mThumbnailLength = Integer.parseInt(attrValue);
|
||||
break;
|
||||
default:
|
||||
mAttributes.put(attrName, attrValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
printAttributes();
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
in = new BufferedInputStream(in, JPEG_SIGNATURE_SIZE);
|
||||
if (!isJpegInputStream((BufferedInputStream) in) && handleRawResult(
|
||||
nativeGetRawAttributesFromInputStream(in))) {
|
||||
return;
|
||||
}
|
||||
} catch (ErrnoException e) {
|
||||
e.rethrowAsIOException();
|
||||
} finally {
|
||||
IoUtils.closeQuietly(in);
|
||||
}
|
||||
|
||||
try {
|
||||
if (mFileDescriptor != null) {
|
||||
Os.lseek(mFileDescriptor, 0, OsConstants.SEEK_SET);
|
||||
}
|
||||
// Process JPEG input stream
|
||||
getJpegAttributes(in);
|
||||
|
||||
getJpegAttributes(mInputStream);
|
||||
} catch (ErrnoException e) {
|
||||
e.rethrowAsIOException();
|
||||
} finally {
|
||||
IoUtils.closeQuietly(mInputStream);
|
||||
if (DEBUG) {
|
||||
printAttributes();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isJpegInputStream(BufferedInputStream in) throws IOException {
|
||||
in.mark(JPEG_SIGNATURE_SIZE);
|
||||
byte[] signatureBytes = new byte[JPEG_SIGNATURE_SIZE];
|
||||
if (in.read(signatureBytes) != JPEG_SIGNATURE_SIZE) {
|
||||
throw new EOFException();
|
||||
}
|
||||
boolean isJpeg = Arrays.equals(JPEG_SIGNATURE, signatureBytes);
|
||||
in.reset();
|
||||
return isJpeg;
|
||||
}
|
||||
|
||||
private boolean handleRawResult(HashMap map) {
|
||||
if (map == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark for disabling the save feature.
|
||||
mIsRaw = true;
|
||||
|
||||
for (Object obj : map.entrySet()) {
|
||||
Map.Entry entry = (Map.Entry) obj;
|
||||
String attrName = (String) entry.getKey();
|
||||
|
||||
switch (attrName) {
|
||||
case TAG_HAS_THUMBNAIL:
|
||||
mHasThumbnail = ((String) entry.getValue()).equalsIgnoreCase("true");
|
||||
break;
|
||||
case TAG_THUMBNAIL_OFFSET:
|
||||
mThumbnailOffset = Integer.parseInt((String) entry.getValue());
|
||||
break;
|
||||
case TAG_THUMBNAIL_LENGTH:
|
||||
mThumbnailLength = Integer.parseInt((String) entry.getValue());
|
||||
break;
|
||||
case TAG_THUMBNAIL_DATA:
|
||||
mThumbnailBytes = (byte[]) entry.getValue();
|
||||
break;
|
||||
default:
|
||||
mAttributes.put(attrName, (String) entry.getValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
printAttributes();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isSeekableFD(FileDescriptor fd) throws IOException {
|
||||
try {
|
||||
Os.lseek(fd, 0, OsConstants.SEEK_CUR);
|
||||
return true;
|
||||
} catch (ErrnoException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Prints out attributes for debugging.
|
||||
@@ -679,9 +721,9 @@ public class ExifInterface {
|
||||
throw new UnsupportedOperationException(
|
||||
"ExifInterface does not support saving attributes on RAW formats.");
|
||||
}
|
||||
if (mFileDescriptor == null && mFilename == null) {
|
||||
if (mSeekableFileDescriptor == null && mFilename == null) {
|
||||
throw new UnsupportedOperationException(
|
||||
"ExifInterface does not support saving attributes for input streams.");
|
||||
"ExifInterface does not support saving attributes for the current input.");
|
||||
}
|
||||
|
||||
// Keep the thumbnail in memory
|
||||
@@ -698,11 +740,10 @@ public class ExifInterface {
|
||||
if (!originalFile.renameTo(tempFile)) {
|
||||
throw new IOException("Could'nt rename to " + tempFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
if (mFileDescriptor != null) {
|
||||
} else if (mSeekableFileDescriptor != null) {
|
||||
tempFile = File.createTempFile("temp", "jpg");
|
||||
Os.lseek(mFileDescriptor, 0, OsConstants.SEEK_SET);
|
||||
in = new FileInputStream(mFileDescriptor);
|
||||
Os.lseek(mSeekableFileDescriptor, 0, OsConstants.SEEK_SET);
|
||||
in = new FileInputStream(mSeekableFileDescriptor);
|
||||
out = new FileOutputStream(tempFile);
|
||||
Streams.copy(in, out);
|
||||
}
|
||||
@@ -720,10 +761,9 @@ public class ExifInterface {
|
||||
in = new FileInputStream(tempFile);
|
||||
if (mFilename != null) {
|
||||
out = new FileOutputStream(mFilename);
|
||||
}
|
||||
if (mFileDescriptor != null) {
|
||||
Os.lseek(mFileDescriptor, 0, OsConstants.SEEK_SET);
|
||||
out = new FileOutputStream(mFileDescriptor);
|
||||
} else if (mSeekableFileDescriptor != null) {
|
||||
Os.lseek(mSeekableFileDescriptor, 0, OsConstants.SEEK_SET);
|
||||
out = new FileOutputStream(mSeekableFileDescriptor);
|
||||
}
|
||||
saveJpegAttributes(in, out);
|
||||
} catch (ErrnoException e) {
|
||||
@@ -760,13 +800,15 @@ public class ExifInterface {
|
||||
|
||||
// Read the thumbnail.
|
||||
FileInputStream in = null;
|
||||
try {
|
||||
if (mFileDescriptor != null) {
|
||||
Os.lseek(mFileDescriptor, 0, OsConstants.SEEK_SET);
|
||||
in = new FileInputStream(mFileDescriptor);
|
||||
}
|
||||
if (mFilename != null) {
|
||||
try {
|
||||
if (mAssetInputStream != null) {
|
||||
return nativeGetThumbnailFromAsset(
|
||||
mAssetInputStream.getNativeAsset(), mThumbnailOffset, mThumbnailLength);
|
||||
} else if (mFilename != null) {
|
||||
in = new FileInputStream(mFilename);
|
||||
} else if (mSeekableFileDescriptor != null) {
|
||||
Os.lseek(mSeekableFileDescriptor, 0, OsConstants.SEEK_SET);
|
||||
in = new FileInputStream(mSeekableFileDescriptor);
|
||||
}
|
||||
if (in == null) {
|
||||
// Should not be reached this.
|
||||
@@ -1180,8 +1222,9 @@ public class ExifInterface {
|
||||
mThumbnailOffset = exifOffsetFromBeginning + jpegInterchangeFormat;
|
||||
mThumbnailLength = jpegInterchangeFormatLength;
|
||||
|
||||
// Do not store a thumbnail in memory if the given input can be re-read.
|
||||
if (mFileDescriptor == null && mFilename == 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[jpegInterchangeFormatLength];
|
||||
dataInputStream.seek(jpegInterchangeFormat);
|
||||
dataInputStream.readFully(thumbnailBytes);
|
||||
@@ -1988,6 +2031,10 @@ public class ExifInterface {
|
||||
}
|
||||
|
||||
// JNI methods for RAW formats.
|
||||
private static native void initRawNative();
|
||||
private static native HashMap getRawAttributesNative(FileDescriptor fileDescriptor);
|
||||
private static native void nativeInitRaw();
|
||||
private static native byte[] nativeGetThumbnailFromAsset(
|
||||
long asset, int thumbnailOffset, int thumbnailLength);
|
||||
private static native HashMap nativeGetRawAttributesFromAsset(long asset);
|
||||
private static native HashMap nativeGetRawAttributesFromFileDescriptor(FileDescriptor fd);
|
||||
private static native HashMap nativeGetRawAttributesFromInputStream(InputStream in);
|
||||
}
|
||||
|
||||
@@ -19,12 +19,15 @@
|
||||
|
||||
#include "android_media_Utils.h"
|
||||
|
||||
#include "android/graphics/CreateJavaOutputStreamAdaptor.h"
|
||||
#include "src/piex_types.h"
|
||||
#include "src/piex.h"
|
||||
|
||||
#include <jni.h>
|
||||
#include <JNIHelp.h>
|
||||
#include <androidfw/Asset.h>
|
||||
#include <android_runtime/AndroidRuntime.h>
|
||||
#include <android/graphics/Utils.h>
|
||||
#include <nativehelper/ScopedLocalRef.h>
|
||||
|
||||
#include <utils/Log.h>
|
||||
@@ -35,6 +38,9 @@
|
||||
|
||||
using namespace android;
|
||||
|
||||
static const char kJpegSignatureChars[] = {(char)0xff, (char)0xd8, (char)0xff};
|
||||
static const int kJpegSignatureSize = 3;
|
||||
|
||||
#define FIND_CLASS(var, className) \
|
||||
var = env->FindClass(className); \
|
||||
LOG_FATAL_IF(! var, "Unable to find class " className);
|
||||
@@ -82,18 +88,48 @@ static void ExifInterface_initRaw(JNIEnv *env) {
|
||||
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
|
||||
}
|
||||
|
||||
static jobject ExifInterface_getRawMetadata(
|
||||
JNIEnv* env, jclass /* clazz */, jobject jfileDescriptor) {
|
||||
int fd = jniGetFDFromFileDescriptor(env, jfileDescriptor);
|
||||
if (fd < 0) {
|
||||
ALOGI("Invalid file descriptor");
|
||||
static bool is_asset_stream(const SkStream& stream) {
|
||||
return stream.hasLength() && stream.hasPosition();
|
||||
}
|
||||
|
||||
static jobject ExifInterface_getThumbnailFromAsset(
|
||||
JNIEnv* env, jclass /* clazz */, jlong jasset, jint jthumbnailOffset,
|
||||
jint jthumbnailLength) {
|
||||
Asset* asset = reinterpret_cast<Asset*>(jasset);
|
||||
std::unique_ptr<AssetStreamAdaptor> stream(new AssetStreamAdaptor(asset));
|
||||
|
||||
std::unique_ptr<jbyte[]> thumbnailData(new jbyte[(int)jthumbnailLength]);
|
||||
if (thumbnailData.get() == NULL) {
|
||||
ALOGI("No memory to get thumbnail");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
piex::PreviewImageData image_data;
|
||||
std::unique_ptr<FileStream> stream(new FileStream(fd));
|
||||
// Do not know the current offset. So rewind it.
|
||||
stream->rewind();
|
||||
|
||||
if (!GetExifFromRawImage(stream.get(), String8("[file descriptor]"), image_data)) {
|
||||
// Read thumbnail.
|
||||
stream->skip((int)jthumbnailOffset);
|
||||
stream->read((void*)thumbnailData.get(), (int)jthumbnailLength);
|
||||
|
||||
// Copy to the byte array.
|
||||
jbyteArray byteArray = env->NewByteArray(jthumbnailLength);
|
||||
env->SetByteArrayRegion(byteArray, 0, jthumbnailLength, thumbnailData.get());
|
||||
return byteArray;
|
||||
}
|
||||
|
||||
static jobject getRawAttributes(JNIEnv* env, SkStream* stream, bool returnThumbnail) {
|
||||
std::unique_ptr<SkStream> streamDeleter(stream);
|
||||
|
||||
std::unique_ptr<::piex::StreamInterface> piexStream;
|
||||
if (is_asset_stream(*stream)) {
|
||||
piexStream.reset(new AssetStream(streamDeleter.release()));
|
||||
} else {
|
||||
piexStream.reset(new BufferedStream(streamDeleter.release()));
|
||||
}
|
||||
|
||||
piex::PreviewImageData image_data;
|
||||
|
||||
if (!GetExifFromRawImage(piexStream.get(), String8("[piex stream]"), image_data)) {
|
||||
ALOGI("Raw image not detected");
|
||||
return NULL;
|
||||
}
|
||||
@@ -253,7 +289,117 @@ static jobject ExifInterface_getRawMetadata(
|
||||
}
|
||||
}
|
||||
|
||||
return KeyedVectorToHashMap(env, map);
|
||||
jobject hashMap = KeyedVectorToHashMap(env, map);
|
||||
|
||||
if (returnThumbnail) {
|
||||
std::unique_ptr<jbyte[]> thumbnailData(new jbyte[image_data.thumbnail.length]);
|
||||
if (thumbnailData.get() == NULL) {
|
||||
ALOGE("No memory to parse a thumbnail");
|
||||
return NULL;
|
||||
}
|
||||
jbyteArray jthumbnailByteArray = env->NewByteArray(image_data.thumbnail.length);
|
||||
if (jthumbnailByteArray == NULL) {
|
||||
ALOGE("No memory to parse a thumbnail");
|
||||
return NULL;
|
||||
}
|
||||
piexStream.get()->GetData(image_data.thumbnail.offset, image_data.thumbnail.length,
|
||||
(uint8_t*)thumbnailData.get());
|
||||
env->SetByteArrayRegion(
|
||||
jthumbnailByteArray, 0, image_data.thumbnail.length, thumbnailData.get());
|
||||
jstring jkey = env->NewStringUTF(String8("thumbnailData"));
|
||||
env->CallObjectMethod(hashMap, gFields.hashMap.put, jkey, jthumbnailByteArray);
|
||||
env->DeleteLocalRef(jkey);
|
||||
env->DeleteLocalRef(jthumbnailByteArray);
|
||||
}
|
||||
return hashMap;
|
||||
}
|
||||
|
||||
static jobject ExifInterface_getRawAttributesFromAsset(
|
||||
JNIEnv* env, jclass /* clazz */, jlong jasset) {
|
||||
std::unique_ptr<char[]> jpegSignature(new char[kJpegSignatureSize]);
|
||||
if (jpegSignature.get() == NULL) {
|
||||
ALOGE("No enough memory to parse");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Asset* asset = reinterpret_cast<Asset*>(jasset);
|
||||
std::unique_ptr<AssetStreamAdaptor> stream(new AssetStreamAdaptor(asset));
|
||||
|
||||
if (stream.get()->read(jpegSignature.get(), kJpegSignatureSize) != kJpegSignatureSize) {
|
||||
// Rewind the stream.
|
||||
stream.get()->rewind();
|
||||
|
||||
ALOGI("Corrupted image.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Rewind the stream.
|
||||
stream.get()->rewind();
|
||||
|
||||
if (memcmp(jpegSignature.get(), kJpegSignatureChars, kJpegSignatureSize) == 0) {
|
||||
ALOGI("Should be a JPEG stream.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Try to parse from the given stream.
|
||||
jobject result = getRawAttributes(env, stream.get(), false);
|
||||
|
||||
// Rewind the stream for the chance to read JPEG.
|
||||
if (result == NULL) {
|
||||
stream.get()->rewind();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static jobject ExifInterface_getRawAttributesFromFileDescriptor(
|
||||
JNIEnv* env, jclass /* clazz */, jobject jfileDescriptor) {
|
||||
std::unique_ptr<char[]> jpegSignature(new char[kJpegSignatureSize]);
|
||||
if (jpegSignature.get() == NULL) {
|
||||
ALOGE("No enough memory to parse");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int fd = jniGetFDFromFileDescriptor(env, jfileDescriptor);
|
||||
if (fd < 0) {
|
||||
ALOGI("Invalid file descriptor");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Restore the file descriptor's offset on exiting this function.
|
||||
AutoFDSeek autoRestore(fd);
|
||||
|
||||
int dupFd = dup(fd);
|
||||
|
||||
FILE* file = fdopen(dupFd, "r");
|
||||
if (file == NULL) {
|
||||
ALOGI("Failed to open the file descriptor");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (fgets(jpegSignature.get(), kJpegSignatureSize, file) == NULL) {
|
||||
ALOGI("Corrupted image.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (memcmp(jpegSignature.get(), kJpegSignatureChars, kJpegSignatureSize) == 0) {
|
||||
ALOGI("Should be a JPEG stream.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Rewind the file descriptor.
|
||||
fseek(file, 0L, SEEK_SET);
|
||||
|
||||
std::unique_ptr<SkFILEStream> fileStream(new SkFILEStream(file,
|
||||
SkFILEStream::kCallerPasses_Ownership));
|
||||
return getRawAttributes(env, fileStream.get(), false);
|
||||
}
|
||||
|
||||
static jobject ExifInterface_getRawAttributesFromInputStream(
|
||||
JNIEnv* env, jclass /* clazz */, jobject jinputStream) {
|
||||
jbyteArray byteArray = env->NewByteArray(8*1024);
|
||||
ScopedLocalRef<jbyteArray> scoper(env, byteArray);
|
||||
std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, jinputStream, scoper.get()));
|
||||
return getRawAttributes(env, stream.get(), true);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -261,9 +407,14 @@ static jobject ExifInterface_getRawMetadata(
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
static JNINativeMethod gMethods[] = {
|
||||
{ "initRawNative", "()V", (void *)ExifInterface_initRaw },
|
||||
{ "getRawAttributesNative", "(Ljava/io/FileDescriptor;)Ljava/util/HashMap;",
|
||||
(void*)ExifInterface_getRawMetadata },
|
||||
{ "nativeInitRaw", "()V", (void *)ExifInterface_initRaw },
|
||||
{ "nativeGetThumbnailFromAsset", "(JII)[B", (void *)ExifInterface_getThumbnailFromAsset },
|
||||
{ "nativeGetRawAttributesFromAsset", "(J)Ljava/util/HashMap;",
|
||||
(void*)ExifInterface_getRawAttributesFromAsset },
|
||||
{ "nativeGetRawAttributesFromFileDescriptor", "(Ljava/io/FileDescriptor;)Ljava/util/HashMap;",
|
||||
(void*)ExifInterface_getRawAttributesFromFileDescriptor },
|
||||
{ "nativeGetRawAttributesFromInputStream", "(Ljava/io/InputStream;)Ljava/util/HashMap;",
|
||||
(void*)ExifInterface_getRawAttributesFromInputStream },
|
||||
};
|
||||
|
||||
int register_android_media_ExifInterface(JNIEnv *env) {
|
||||
|
||||
@@ -30,30 +30,78 @@
|
||||
|
||||
namespace android {
|
||||
|
||||
AssetStream::AssetStream(SkStream* stream)
|
||||
: mStream(stream) {
|
||||
}
|
||||
|
||||
AssetStream::~AssetStream() {
|
||||
}
|
||||
|
||||
piex::Error AssetStream::GetData(
|
||||
const size_t offset, const size_t length, std::uint8_t* data) {
|
||||
// Seek first.
|
||||
if (mPosition != offset) {
|
||||
if (!mStream->seek(offset)) {
|
||||
return piex::Error::kFail;
|
||||
}
|
||||
}
|
||||
|
||||
// Read bytes.
|
||||
size_t size = mStream->read((void*)data, length);
|
||||
mPosition += size;
|
||||
|
||||
return size == length ? piex::Error::kOk : piex::Error::kFail;
|
||||
}
|
||||
|
||||
BufferedStream::BufferedStream(SkStream* stream)
|
||||
: mStream(stream) {
|
||||
}
|
||||
|
||||
BufferedStream::~BufferedStream() {
|
||||
}
|
||||
|
||||
piex::Error BufferedStream::GetData(
|
||||
const size_t offset, const size_t length, std::uint8_t* data) {
|
||||
// Seek first.
|
||||
if (offset + length > mStreamBuffer.bytesWritten()) {
|
||||
size_t sizeToRead = offset + length - mStreamBuffer.bytesWritten();
|
||||
if (sizeToRead <= kMinSizeToRead) {
|
||||
sizeToRead = kMinSizeToRead;
|
||||
}
|
||||
void* tempBuffer = malloc(sizeToRead);
|
||||
if (tempBuffer != NULL) {
|
||||
size_t bytesRead = mStream->read(tempBuffer, sizeToRead);
|
||||
if (bytesRead != sizeToRead) {
|
||||
free(tempBuffer);
|
||||
return piex::Error::kFail;
|
||||
}
|
||||
mStreamBuffer.write(tempBuffer, bytesRead);
|
||||
free(tempBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
// Read bytes.
|
||||
if (mStreamBuffer.read((void*)data, offset, length)) {
|
||||
return piex::Error::kOk;
|
||||
} else {
|
||||
return piex::Error::kFail;
|
||||
}
|
||||
}
|
||||
|
||||
FileStream::FileStream(const int fd)
|
||||
: mPosition(0),
|
||||
mSize(0) {
|
||||
: mPosition(0) {
|
||||
mFile = fdopen(fd, "r");
|
||||
if (mFile == NULL) {
|
||||
return;
|
||||
}
|
||||
// Get the size.
|
||||
fseek(mFile, 0l, SEEK_END);
|
||||
mSize = ftell(mFile);
|
||||
fseek(mFile, 0l, SEEK_SET);
|
||||
}
|
||||
|
||||
FileStream::FileStream(const String8 filename)
|
||||
: mPosition(0),
|
||||
mSize(0) {
|
||||
: mPosition(0) {
|
||||
mFile = fopen(filename.string(), "r");
|
||||
if (mFile == NULL) {
|
||||
return;
|
||||
}
|
||||
// Get the size.
|
||||
fseek(mFile, 0l, SEEK_END);
|
||||
mSize = ftell(mFile);
|
||||
fseek(mFile, 0l, SEEK_SET);
|
||||
}
|
||||
|
||||
FileStream::~FileStream() {
|
||||
@@ -79,7 +127,7 @@ piex::Error FileStream::GetData(
|
||||
mPosition += size;
|
||||
|
||||
// Handle errors.
|
||||
if (ferror(mFile) || (size == 0 && feof(mFile))) {
|
||||
if (ferror(mFile)) {
|
||||
ALOGV("GetData read failed: (offset: %zu, length: %zu)", offset, length);
|
||||
return piex::Error::kFail;
|
||||
}
|
||||
@@ -90,21 +138,12 @@ bool FileStream::exists() const {
|
||||
return mFile != NULL;
|
||||
}
|
||||
|
||||
size_t FileStream::size() const {
|
||||
return mSize;
|
||||
}
|
||||
|
||||
bool GetExifFromRawImage(
|
||||
FileStream* stream, const String8& filename, piex::PreviewImageData& image_data) {
|
||||
piex::StreamInterface* stream, const String8& filename,
|
||||
piex::PreviewImageData& image_data) {
|
||||
// Reset the PreviewImageData to its default.
|
||||
image_data = piex::PreviewImageData();
|
||||
|
||||
if (!stream->exists()) {
|
||||
// File is not exists.
|
||||
ALOGV("File is not exists: %s", filename.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!piex::IsRaw(stream)) {
|
||||
// Format not supported.
|
||||
ALOGV("Format not supported: %s", filename.string());
|
||||
@@ -119,12 +158,6 @@ bool GetExifFromRawImage(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (image_data.thumbnail_offset + image_data.thumbnail_length > stream->size()) {
|
||||
// Corrupted image.
|
||||
ALOGV("Corrupted file: %s", filename.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,20 +21,62 @@
|
||||
#include "src/piex.h"
|
||||
|
||||
#include <android_runtime/AndroidRuntime.h>
|
||||
#include <camera3.h>
|
||||
#include <gui/CpuConsumer.h>
|
||||
#include <jni.h>
|
||||
#include <JNIHelp.h>
|
||||
#include <utils/KeyedVector.h>
|
||||
#include <utils/String8.h>
|
||||
#include <gui/CpuConsumer.h>
|
||||
#include <camera3.h>
|
||||
#include <SkStream.h>
|
||||
|
||||
namespace android {
|
||||
|
||||
class AssetStream : public piex::StreamInterface {
|
||||
private:
|
||||
SkStream *mStream;
|
||||
size_t mPosition;
|
||||
|
||||
public:
|
||||
AssetStream(SkStream* stream);
|
||||
~AssetStream();
|
||||
|
||||
// Reads 'length' amount of bytes from 'offset' to 'data'. The 'data' buffer
|
||||
// provided by the caller, guaranteed to be at least "length" bytes long.
|
||||
// On 'kOk' the 'data' pointer contains 'length' valid bytes beginning at
|
||||
// 'offset' bytes from the start of the stream.
|
||||
// Returns 'kFail' if 'offset' + 'length' exceeds the stream and does not
|
||||
// change the contents of 'data'.
|
||||
piex::Error GetData(
|
||||
const size_t offset, const size_t length, std::uint8_t* data) override;
|
||||
};
|
||||
|
||||
class BufferedStream : public piex::StreamInterface {
|
||||
private:
|
||||
SkStream *mStream;
|
||||
// Growable memory stream
|
||||
SkDynamicMemoryWStream mStreamBuffer;
|
||||
|
||||
// Minimum size to read on filling the buffer.
|
||||
const size_t kMinSizeToRead = 8192;
|
||||
|
||||
public:
|
||||
BufferedStream(SkStream* stream);
|
||||
~BufferedStream();
|
||||
|
||||
// Reads 'length' amount of bytes from 'offset' to 'data'. The 'data' buffer
|
||||
// provided by the caller, guaranteed to be at least "length" bytes long.
|
||||
// On 'kOk' the 'data' pointer contains 'length' valid bytes beginning at
|
||||
// 'offset' bytes from the start of the stream.
|
||||
// Returns 'kFail' if 'offset' + 'length' exceeds the stream and does not
|
||||
// change the contents of 'data'.
|
||||
piex::Error GetData(
|
||||
const size_t offset, const size_t length, std::uint8_t* data) override;
|
||||
};
|
||||
|
||||
class FileStream : public piex::StreamInterface {
|
||||
private:
|
||||
FILE *mFile;
|
||||
size_t mPosition;
|
||||
size_t mSize;
|
||||
|
||||
public:
|
||||
FileStream(const int fd);
|
||||
@@ -50,13 +92,12 @@ public:
|
||||
piex::Error GetData(
|
||||
const size_t offset, const size_t length, std::uint8_t* data) override;
|
||||
bool exists() const;
|
||||
size_t size() const;
|
||||
};
|
||||
|
||||
// Reads EXIF metadata from a given raw image via piex.
|
||||
// And returns true if the operation is successful; otherwise, false.
|
||||
bool GetExifFromRawImage(
|
||||
FileStream* stream, const String8& filename, piex::PreviewImageData& image_data);
|
||||
piex::StreamInterface* stream, const String8& filename, piex::PreviewImageData& image_data);
|
||||
|
||||
// Returns true if the conversion is successful; otherwise, false.
|
||||
bool ConvertKeyValueArraysToKeyedVector(
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
BIN
media/tests/MediaFrameworkTest/assets/lg_g4_iso_800.dng
Normal file
BIN
media/tests/MediaFrameworkTest/assets/lg_g4_iso_800.dng
Normal file
Binary file not shown.
@@ -76,9 +76,9 @@
|
||||
<item>0</item>
|
||||
</array>
|
||||
<array name="lg_g4_iso_800_dng">
|
||||
<item>false</item>
|
||||
<item>0</item>
|
||||
<item>0</item>
|
||||
<item>true</item>
|
||||
<item>256</item>
|
||||
<item>144</item>
|
||||
<item>true</item>
|
||||
<item>53.834507</item>
|
||||
<item>10.69585</item>
|
||||
|
||||
@@ -23,20 +23,20 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.ExifInterface;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.test.AndroidTestCase;
|
||||
import android.util.Log;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.OsConstants;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import libcore.io.IoUtils;
|
||||
import libcore.io.Streams;
|
||||
@@ -46,11 +46,10 @@ public class ExifInterfaceTest extends AndroidTestCase {
|
||||
private static final boolean VERBOSE = false; // lots of logging
|
||||
|
||||
private static final double DIFFERENCE_TOLERANCE = .005;
|
||||
private static final int BUFFER_SIZE = 32768;
|
||||
|
||||
// List of files.
|
||||
private static final String EXIF_BYTE_ORDER_II_JPEG = "ExifByteOrderII.jpg";
|
||||
private static final String EXIF_BYTE_ORDER_MM_JPEG = "ExifByteOrderMM.jpg";
|
||||
private static final String EXIF_BYTE_ORDER_II_JPEG = "image_exif_byte_order_ii.jpg";
|
||||
private static final String EXIF_BYTE_ORDER_MM_JPEG = "image_exif_byte_order_mm.jpg";
|
||||
private static final String LG_G4_ISO_800_DNG = "lg_g4_iso_800.dng";
|
||||
private static final int[] IMAGE_RESOURCES = new int[] {
|
||||
R.raw.image_exif_byte_order_ii, R.raw.image_exif_byte_order_mm, R.raw.lg_g4_iso_800 };
|
||||
@@ -165,8 +164,6 @@ public class ExifInterfaceTest extends AndroidTestCase {
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
|
||||
for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
|
||||
String outputPath = new File(Environment.getExternalStorageDirectory(),
|
||||
IMAGE_FILENAMES[i]).getAbsolutePath();
|
||||
@@ -314,26 +311,30 @@ public class ExifInterfaceTest extends AndroidTestCase {
|
||||
expectedValue.whiteBalance);
|
||||
}
|
||||
|
||||
private void testExifInterfaceForJpeg(String fileName, int typedArrayResourceId)
|
||||
private void testExifInterfaceCommon(File imageFile, ExpectedValue expectedValue)
|
||||
throws IOException {
|
||||
ExpectedValue expectedValue = new ExpectedValue(
|
||||
getContext().getResources().obtainTypedArray(typedArrayResourceId));
|
||||
File imageFile = new File(Environment.getExternalStorageDirectory(), fileName);
|
||||
|
||||
// Created via path.
|
||||
ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
|
||||
if (VERBOSE) {
|
||||
printExifTagsAndValues(fileName, exifInterface);
|
||||
printExifTagsAndValues(imageFile.getName(), exifInterface);
|
||||
}
|
||||
compareWithExpectedValue(exifInterface, expectedValue);
|
||||
|
||||
// Created from an asset file.
|
||||
InputStream in = mContext.getAssets().open(imageFile.getName());
|
||||
exifInterface = new ExifInterface(in);
|
||||
if (VERBOSE) {
|
||||
printExifTagsAndValues(imageFile.getName(), exifInterface);
|
||||
}
|
||||
compareWithExpectedValue(exifInterface, expectedValue);
|
||||
|
||||
// Created via InputStream.
|
||||
FileInputStream in = null;
|
||||
in = null;
|
||||
try {
|
||||
in = new FileInputStream(imageFile.getAbsolutePath());
|
||||
in = new BufferedInputStream(new FileInputStream(imageFile.getAbsolutePath()));
|
||||
exifInterface = new ExifInterface(in);
|
||||
if (VERBOSE) {
|
||||
printExifTagsAndValues(fileName, exifInterface);
|
||||
printExifTagsAndValues(imageFile.getName(), exifInterface);
|
||||
}
|
||||
compareWithExpectedValue(exifInterface, expectedValue);
|
||||
} finally {
|
||||
@@ -345,18 +346,30 @@ public class ExifInterfaceTest extends AndroidTestCase {
|
||||
FileDescriptor fd = Os.open(imageFile.getAbsolutePath(), OsConstants.O_RDONLY, 0600);
|
||||
exifInterface = new ExifInterface(fd);
|
||||
if (VERBOSE) {
|
||||
printExifTagsAndValues(fileName, exifInterface);
|
||||
printExifTagsAndValues(imageFile.getName(), exifInterface);
|
||||
}
|
||||
compareWithExpectedValue(exifInterface, expectedValue);
|
||||
} catch (ErrnoException e) {
|
||||
e.rethrowAsIOException();
|
||||
}
|
||||
}
|
||||
|
||||
private void testExifInterfaceForJpeg(String fileName, int typedArrayResourceId)
|
||||
throws IOException {
|
||||
ExpectedValue expectedValue = new ExpectedValue(
|
||||
getContext().getResources().obtainTypedArray(typedArrayResourceId));
|
||||
File imageFile = new File(Environment.getExternalStorageDirectory(), fileName);
|
||||
|
||||
// Test for reading from various inputs.
|
||||
testExifInterfaceCommon(imageFile, expectedValue);
|
||||
|
||||
// Test for saving attributes.
|
||||
ExifInterface exifInterface;
|
||||
try {
|
||||
FileDescriptor fd = Os.open(imageFile.getAbsolutePath(), OsConstants.O_RDWR, 0600);
|
||||
exifInterface = new ExifInterface(fd);
|
||||
exifInterface.saveAttributes();
|
||||
fd = Os.open(imageFile.getAbsolutePath(), OsConstants.O_RDWR, 0600);
|
||||
exifInterface = new ExifInterface(fd);
|
||||
if (VERBOSE) {
|
||||
printExifTagsAndValues(fileName, exifInterface);
|
||||
@@ -383,25 +396,11 @@ public class ExifInterfaceTest extends AndroidTestCase {
|
||||
getContext().getResources().obtainTypedArray(typedArrayResourceId));
|
||||
File imageFile = new File(Environment.getExternalStorageDirectory(), fileName);
|
||||
|
||||
// Created via path.
|
||||
ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
|
||||
if (VERBOSE) {
|
||||
printExifTagsAndValues(fileName, exifInterface);
|
||||
}
|
||||
compareWithExpectedValue(exifInterface, expectedValue);
|
||||
// Test for reading from various inputs.
|
||||
testExifInterfaceCommon(imageFile, expectedValue);
|
||||
|
||||
// Created via FileDescriptor.
|
||||
FileInputStream in = null;
|
||||
try {
|
||||
in = new FileInputStream(imageFile);
|
||||
exifInterface = new ExifInterface(in.getFD());
|
||||
if (VERBOSE) {
|
||||
printExifTagsAndValues(fileName, exifInterface);
|
||||
}
|
||||
compareWithExpectedValue(exifInterface, expectedValue);
|
||||
} finally {
|
||||
IoUtils.closeQuietly(in);
|
||||
}
|
||||
// Since ExifInterface does not support for saving attributes for RAW files, do not test
|
||||
// about writing back in here.
|
||||
}
|
||||
|
||||
public void testReadExifDataFromExifByteOrderIIJpeg() throws Throwable {
|
||||
@@ -415,4 +414,14 @@ public class ExifInterfaceTest extends AndroidTestCase {
|
||||
public void testReadExifDataFromLgG4Iso800Dng() throws Throwable {
|
||||
testExifInterfaceForRaw(LG_G4_ISO_800_DNG, R.array.lg_g4_iso_800_dng);
|
||||
}
|
||||
|
||||
public void testCorruptedImage() {
|
||||
byte[] bytes = new byte[1024];
|
||||
try {
|
||||
new ExifInterface(new ByteArrayInputStream(bytes));
|
||||
fail("Should not reach here!");
|
||||
} catch (IOException e) {
|
||||
// Success
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user