Merge "ExifInterface: add support for reading metadata from RAW images"

This commit is contained in:
Jaesung Chung
2016-01-19 00:37:13 +00:00
committed by Android (Google) Code Review
4 changed files with 486 additions and 42 deletions

View File

@@ -17,7 +17,7 @@
package android.media;
import java.io.IOException;
import java.util.regex.Matcher;
import java.io.RandomAccessFile;
import java.util.regex.Pattern;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
@@ -27,7 +27,9 @@ import java.util.Map;
import java.util.TimeZone;
/**
* This is a class for reading and writing Exif tags in a JPEG file.
* 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.
*/
public class ExifInterface {
// The Exif tag names
@@ -68,8 +70,6 @@ public class ExifInterface {
/** Type is int. */
public static final String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized";
/**
* @hide
*/
@@ -98,15 +98,22 @@ public class ExifInterface {
/** Type is String. Name of GPS processing method used for location finding. */
public static final String TAG_GPS_PROCESSING_METHOD = "GPSProcessingMethod";
// Private tags used for thumbnail information.
private static final String TAG_HAS_THUMBNAIL = "hasThumbnail";
private static final String TAG_THUMBNAIL_OFFSET = "thumbnailOffset";
private static final String TAG_THUMBNAIL_LENGTH = "thumbnailLength";
// Constants used for the Orientation Exif tag.
public static final int ORIENTATION_UNDEFINED = 0;
public static final int ORIENTATION_NORMAL = 1;
public static final int ORIENTATION_FLIP_HORIZONTAL = 2; // left right reversed mirror
public static final int ORIENTATION_ROTATE_180 = 3;
public static final int ORIENTATION_FLIP_VERTICAL = 4; // upside down mirror
public static final int ORIENTATION_TRANSPOSE = 5; // flipped about top-left <--> bottom-right axis
// flipped about top-left <--> bottom-right axis
public static final int ORIENTATION_TRANSPOSE = 5;
public static final int ORIENTATION_ROTATE_90 = 6; // rotate 90 cw to right it
public static final int ORIENTATION_TRANSVERSE = 7; // flipped about top-right <--> bottom-left axis
// flipped about top-right <--> bottom-left axis
public static final int ORIENTATION_TRANSVERSE = 7;
public static final int ORIENTATION_ROTATE_270 = 8; // rotate 270 to right it
// Constants used for white balance
@@ -116,13 +123,20 @@ public class ExifInterface {
static {
System.loadLibrary("jhead_jni");
System.loadLibrary("media_jni");
initRawNative();
sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
sFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private String mFilename;
private HashMap<String, String> mAttributes;
private final String mFilename;
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;
private int mThumbnailLength;
// Because the underlying implementation (jhead) uses static variables,
// there can only be one user at a time for the native functions (and
@@ -134,19 +148,20 @@ public class ExifInterface {
private static final Pattern sNonZeroTimePattern = Pattern.compile(".*[1-9].*");
/**
* Reads Exif tags from the specified JPEG file.
* Reads Exif tags from the specified image file.
*/
public ExifInterface(String filename) throws IOException {
if (filename == null) {
throw new IllegalArgumentException("filename cannot be null");
}
mFilename = filename;
// First test whether a given file is a one of RAW format or not.
loadAttributes();
}
/**
* Returns the value of the specified tag or {@code null} if there
* is no such tag in the JPEG file.
* is no such tag in the image file.
*
* @param tag the name of the tag.
*/
@@ -156,7 +171,7 @@ public class ExifInterface {
/**
* Returns the integer value of the specified tag. If there is no such tag
* in the JPEG file or the value cannot be parsed as integer, return
* in the image file or the value cannot be parsed as integer, return
* <var>defaultValue</var>.
*
* @param tag the name of the tag.
@@ -174,7 +189,7 @@ public class ExifInterface {
/**
* Returns the double value of the specified rational tag. If there is no
* such tag in the JPEG file or the value cannot be parsed as double, return
* such tag in the image file or the value cannot be parsed as double, return
* <var>defaultValue</var>.
*
* @param tag the name of the tag.
@@ -210,17 +225,42 @@ public class ExifInterface {
*
* 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.
* Model -&gt; Nikon. Numeric values are stored as strings.
*
* This function also initialize mHasThumbnail to indicate whether the
* file has a thumbnail inside.
*/
private void loadAttributes() throws IOException {
HashMap map = getRawAttributesNative(mFilename);
mIsRaw = map != null;
if (mIsRaw) {
for (Object o : map.entrySet()) {
Map.Entry entry = (Map.Entry) o;
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;
}
}
return;
}
// format of string passed from native C code:
// "attrCnt attr1=valueLen value1attr2=value2Len value2..."
// example:
// "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
mAttributes = new HashMap<String, String>();
String attrStr;
synchronized (sLock) {
@@ -248,7 +288,7 @@ public class ExifInterface {
String attrValue = attrStr.substring(ptr, ptr + attrLen);
ptr += attrLen;
if (attrName.equals("hasThumbnail")) {
if (attrName.equals(TAG_HAS_THUMBNAIL)) {
mHasThumbnail = attrValue.equalsIgnoreCase("true");
} else {
mAttributes.put(attrName, attrValue);
@@ -257,32 +297,36 @@ public class ExifInterface {
}
/**
* Save the tag data into the JPEG file. This is expensive because it involves
* copying all the JPG data from one file to another and deleting the old file
* and renaming the other. It's best to use {@link #setAttribute(String,String)}
* to set all attributes to write and make a single call rather than multiple
* calls for each attribute.
* Save the tag data into the original image file. This is expensive because it involves
* copying all the data from one file to another and deleting the old file and renaming the
* other. It's best to use{@link #setAttribute(String,String)} to set all attributes to write
* and make a single call rather than multiple calls for each attribute.
*/
public void saveAttributes() throws IOException {
if (mIsRaw) {
throw new UnsupportedOperationException(
"ExifInterface does not support saving attributes on RAW formats.");
}
// format of string passed to native C code:
// "attrCnt attr1=valueLen value1attr2=value2Len value2..."
// example:
// "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
StringBuilder sb = new StringBuilder();
int size = mAttributes.size();
if (mAttributes.containsKey("hasThumbnail")) {
if (mAttributes.containsKey(TAG_HAS_THUMBNAIL)) {
--size;
}
sb.append(size + " ");
for (Map.Entry<String, String> iter : mAttributes.entrySet()) {
String key = iter.getKey();
if (key.equals("hasThumbnail")) {
sb.append(size).append(" ");
for (Map.Entry<String, String> entry : mAttributes.entrySet()) {
String key = entry.getKey();
if (key.equals(TAG_HAS_THUMBNAIL)) {
// this is a fake attribute not saved as an exif tag
continue;
}
String val = iter.getValue();
sb.append(key + "=");
sb.append(val.length() + " ");
String val = entry.getValue();
sb.append(key).append("=");
sb.append(val.length()).append(" ");
sb.append(val);
}
String s = sb.toString();
@@ -293,25 +337,43 @@ public class ExifInterface {
}
/**
* Returns true if the JPEG file has a thumbnail.
* Returns true if the image file has a thumbnail.
*/
public boolean hasThumbnail() {
return mHasThumbnail;
}
/**
* Returns the thumbnail inside the JPEG file, or {@code null} if there is no thumbnail.
* 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
* {@link android.graphics.BitmapFactory#decodeByteArray(byte[],int,int)}
*/
public byte[] getThumbnail() {
if (mIsRaw) {
if (mHasThumbnail) {
try (RandomAccessFile file = new RandomAccessFile(mFilename, "r")) {
if (file.length() < mThumbnailLength + mThumbnailOffset) {
throw new IOException("Corrupted image.");
}
file.seek(mThumbnailOffset);
byte[] buffer = new byte[mThumbnailLength];
file.readFully(buffer);
return buffer;
} catch (IOException e) {
// Couldn't get a thumbnail image.
}
}
return null;
}
synchronized (sLock) {
return getThumbnailNative(mFilename);
}
}
/**
* Returns the offset and length of thumbnail inside the JPEG file, or
* Returns the offset and length of thumbnail inside the image file, or
* {@code null} if there is no thumbnail.
*
* @return two-element array, the offset in the first value, and length in
@@ -319,6 +381,13 @@ public class ExifInterface {
* @hide
*/
public long[] getThumbnailRange() {
if (mIsRaw) {
long[] range = new long[2];
range[0] = mThumbnailOffset;
range[1] = mThumbnailLength;
return range;
}
synchronized (sLock) {
return getThumbnailRangeNative(mFilename);
}
@@ -447,26 +516,28 @@ public class ExifInterface {
return (float) -result;
}
return (float) result;
} catch (NumberFormatException e) {
// Some of the nubmers are not valid
throw new IllegalArgumentException();
} catch (ArrayIndexOutOfBoundsException e) {
// Some of the rational does not follow the correct format
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
// Not valid
throw new IllegalArgumentException();
}
}
private native boolean appendThumbnailNative(String fileName,
// JNI methods for JPEG.
private static native boolean appendThumbnailNative(String fileName,
String thumbnailFileName);
private native void saveAttributesNative(String fileName,
private static native void saveAttributesNative(String fileName,
String compressedAttributes);
private native String getAttributesNative(String fileName);
private static native String getAttributesNative(String fileName);
private native void commitChangesNative(String fileName);
private static native void commitChangesNative(String fileName);
private native byte[] getThumbnailNative(String fileName);
private static native byte[] getThumbnailNative(String fileName);
private native long[] getThumbnailRangeNative(String fileName);
private static native long[] getThumbnailRangeNative(String fileName);
// JNI methods for RAW formats.
private static native void initRawNative();
private static native HashMap getRawAttributesNative(String filename);
}

View File

@@ -3,6 +3,7 @@ include $(CLEAR_VARS)
LOCAL_SRC_FILES:= \
android_media_AmrInputStream.cpp \
android_media_ExifInterface.cpp \
android_media_ImageWriter.cpp \
android_media_ImageReader.cpp \
android_media_MediaCrypto.cpp \
@@ -44,6 +45,7 @@ LOCAL_SHARED_LIBRARIES := \
libusbhost \
libjhead \
libexif \
libpiex \
libstagefright_amrnb_common
LOCAL_REQUIRED_MODULES := \
@@ -54,6 +56,7 @@ LOCAL_STATIC_LIBRARIES := \
LOCAL_C_INCLUDES += \
external/libexif/ \
external/piex/ \
external/tremor/Tremor \
frameworks/base/core/jni \
frameworks/base/libs/hwui \

View File

@@ -0,0 +1,364 @@
/*
* Copyright 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//#define LOG_NDEBUG 0
#define LOG_TAG "ExifInterface_JNI"
#include <utils/Log.h>
#include <utils/String8.h>
#include <utils/KeyedVector.h>
#include <android_runtime/AndroidRuntime.h>
#include <jni.h>
#include <JNIHelp.h>
#include <nativehelper/ScopedLocalRef.h>
#include "src/piex_types.h"
#include "src/piex.h"
// ----------------------------------------------------------------------------
using namespace android;
class FileStream : public piex::StreamInterface {
private:
FILE *mFile;
size_t mPosition;
size_t mSize;
public:
FileStream(const String8 filename)
: mPosition(0),
mSize(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() {
if (mFile != NULL) {
fclose(mFile);
mFile = NULL;
}
}
// 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 {
if (mFile == NULL) {
return piex::Error::kFail;
}
// Seek first.
if (mPosition != offset) {
fseek(mFile, offset, SEEK_SET);
}
// Read bytes.
size_t size = fread((void*)data, sizeof(std::uint8_t), length, mFile);
mPosition += size;
// Handle errors.
if (ferror(mFile)) {
return piex::Error::kFail;
}
if (size == 0 && feof(mFile)) {
return piex::Error::kFail;
}
return piex::Error::kOk;
}
bool exists() {
return mFile != NULL;
}
size_t size() {
return mSize;
}
};
#define FIND_CLASS(var, className) \
var = env->FindClass(className); \
LOG_FATAL_IF(! var, "Unable to find class " className);
#define GET_METHOD_ID(var, clazz, fieldName, fieldDescriptor) \
var = env->GetMethodID(clazz, fieldName, fieldDescriptor); \
LOG_FATAL_IF(! var, "Unable to find method " fieldName);
struct HashMapFields {
jmethodID init;
jmethodID put;
};
struct fields_t {
HashMapFields hashMap;
jclass hashMapClassId;
};
static fields_t gFields;
static jobject KeyedVectorToHashMap(JNIEnv *env, KeyedVector<String8, String8> const &map) {
jclass clazz = gFields.hashMapClassId;
jobject hashMap = env->NewObject(clazz, gFields.hashMap.init);
for (size_t i = 0; i < map.size(); ++i) {
jstring jkey = env->NewStringUTF(map.keyAt(i).string());
jstring jvalue = env->NewStringUTF(map.valueAt(i).string());
env->CallObjectMethod(hashMap, gFields.hashMap.put, jkey, jvalue);
env->DeleteLocalRef(jkey);
env->DeleteLocalRef(jvalue);
}
return hashMap;
}
extern "C" {
// -------------------------- ExifInterface methods ---------------------------
static void ExifInterface_initRaw(JNIEnv *env) {
jclass clazz;
FIND_CLASS(clazz, "java/util/HashMap");
gFields.hashMapClassId = static_cast<jclass>(env->NewGlobalRef(clazz));
GET_METHOD_ID(gFields.hashMap.init, clazz, "<init>", "()V");
GET_METHOD_ID(gFields.hashMap.put, clazz, "put",
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
}
static jobject ExifInterface_getRawMetadata(
JNIEnv* env, jclass /* clazz */, jstring jfilename) {
const char* filenameChars = env->GetStringUTFChars(jfilename, NULL);
if (filenameChars == NULL) {
return NULL;
}
String8 filename(filenameChars);
env->ReleaseStringUTFChars(jfilename, filenameChars);
piex::PreviewImageData image_data;
memset(&image_data, 0, sizeof(image_data));
std::unique_ptr<FileStream> stream(new FileStream(filename));
if (!stream.get()->exists()) {
// File is not exists.
ALOGI("File is not exists: %s", filename.string());
return NULL;
}
if (!piex::IsRaw(stream.get())) {
// Format not supported.
ALOGI("Format not supported: %s", filename.string());
return NULL;
}
piex::Error err = piex::GetPreviewImageData(stream.get(), &image_data);
if (err != piex::Error::kOk) {
// The input data seems to be broken.
ALOGI("Raw image not detected: %s (error code: %d)", filename.string(), (int32_t)err);
return NULL;
}
if (image_data.thumbnail_offset + image_data.thumbnail_length > stream.get()->size()) {
// Corrupted file.
ALOGI("Corrupted file: %s", filename.string());
return NULL;
}
KeyedVector<String8, String8> map;
if (image_data.thumbnail_length > 0) {
map.add(String8("hasThumbnail"), String8("true"));
map.add(String8("thumbnailOffset"), String8::format("%d", image_data.thumbnail_offset));
map.add(String8("thumbnailLength"), String8::format("%d", image_data.thumbnail_length));
} else {
map.add(String8("hasThumbnail"), String8("false"));
}
map.add(
String8("Orientation"),
String8::format("%u", image_data.exif_orientation));
map.add(
String8("ImageWidth"),
String8::format("%u", image_data.full_width));
map.add(
String8("ImageLength"),
String8::format("%u", image_data.full_height));
// Current PIEX does not have LightSource information while JPEG version of
// EXIFInterface always declares the light source field. For the
// compatibility, it provides the default value of the light source field.
map.add(String8("LightSource"), String8("0"));
if (!image_data.maker.empty()) {
map.add(String8("Make"), String8(image_data.maker.c_str()));
}
if (!image_data.model.empty()) {
map.add(String8("Model"), String8(image_data.model.c_str()));
}
if (!image_data.date_time.empty()) {
map.add(String8("DateTime"), String8(image_data.date_time.c_str()));
}
if (image_data.iso) {
map.add(
String8("ISOSpeedRatings"),
String8::format("%u", image_data.iso));
}
if (image_data.exposure_time.numerator != 0
&& image_data.exposure_time.denominator != 0) {
double exposureTime =
(double)image_data.exposure_time.numerator
/ image_data.exposure_time.denominator;
const char* format;
if (exposureTime < 0.01) {
format = "%6.4f";
} else {
format = "%5.3f";
}
map.add(String8("ExposureTime"), String8::format(format, exposureTime));
}
if (image_data.fnumber.numerator != 0
&& image_data.fnumber.denominator != 0) {
double fnumber =
(double)image_data.fnumber.numerator
/ image_data.fnumber.denominator;
map.add(String8("FNumber"), String8::format("%5.3f", fnumber));
}
if (image_data.focal_length.numerator != 0
&& image_data.focal_length.denominator != 0) {
map.add(
String8("FocalLength"),
String8::format(
"%u/%u",
image_data.focal_length.numerator,
image_data.focal_length.denominator));
}
if (image_data.gps.is_valid) {
if (image_data.gps.latitude[0].denominator != 0
&& image_data.gps.latitude[1].denominator != 0
&& image_data.gps.latitude[2].denominator != 0) {
map.add(
String8("GPSLatitude"),
String8::format(
"%u/%u,%u/%u,%u/%u",
image_data.gps.latitude[0].numerator,
image_data.gps.latitude[0].denominator,
image_data.gps.latitude[1].numerator,
image_data.gps.latitude[1].denominator,
image_data.gps.latitude[2].numerator,
image_data.gps.latitude[2].denominator));
}
if (image_data.gps.latitude_ref) {
char str[2];
str[0] = image_data.gps.latitude_ref;
str[1] = 0;
map.add(String8("GPSLatitudeRef"), String8(str));
}
if (image_data.gps.longitude[0].denominator != 0
&& image_data.gps.longitude[1].denominator != 0
&& image_data.gps.longitude[2].denominator != 0) {
map.add(
String8("GPSLongitude"),
String8::format(
"%u/%u,%u/%u,%u/%u",
image_data.gps.longitude[0].numerator,
image_data.gps.longitude[0].denominator,
image_data.gps.longitude[1].numerator,
image_data.gps.longitude[1].denominator,
image_data.gps.longitude[2].numerator,
image_data.gps.longitude[2].denominator));
}
if (image_data.gps.longitude_ref) {
char str[2];
str[0] = image_data.gps.longitude_ref;
str[1] = 0;
map.add(String8("GPSLongitudeRef"), String8(str));
}
if (image_data.gps.altitude.denominator != 0) {
map.add(
String8("GPSAltitude"),
String8::format("%u/%u",
image_data.gps.altitude.numerator,
image_data.gps.altitude.denominator));
map.add(
String8("GPSAltitudeRef"),
String8(image_data.gps.altitude_ref ? "1" : "0"));
}
if (image_data.gps.time_stamp[0].denominator != 0
&& image_data.gps.time_stamp[1].denominator != 0
&& image_data.gps.time_stamp[2].denominator != 0) {
map.add(
String8("GPSTimeStamp"),
String8::format(
"%2u:%2u:%2u",
image_data.gps.time_stamp[0].numerator
/ image_data.gps.time_stamp[0].denominator,
image_data.gps.time_stamp[1].numerator
/ image_data.gps.time_stamp[1].denominator,
image_data.gps.time_stamp[2].numerator
/ image_data.gps.time_stamp[2].denominator));
}
if (!image_data.gps.date_stamp.empty()) {
map.add(
String8("GPSDateStamp"),
String8(image_data.gps.date_stamp.c_str()));
}
}
return KeyedVectorToHashMap(env, map);
}
} // extern "C"
// ----------------------------------------------------------------------------
static JNINativeMethod gMethods[] = {
{ "initRawNative", "()V", (void *)ExifInterface_initRaw },
{ "getRawAttributesNative", "(Ljava/lang/String;)Ljava/util/HashMap;",
(void*)ExifInterface_getRawMetadata },
};
int register_android_media_ExifInterface(JNIEnv *env) {
return AndroidRuntime::registerNativeMethods(
env,
"android/media/ExifInterface",
gMethods,
NELEM(gMethods));
}

View File

@@ -1088,6 +1088,7 @@ static int register_android_media_MediaPlayer(JNIEnv *env)
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaPlayer", gMethods, NELEM(gMethods));
}
extern int register_android_media_ExifInterface(JNIEnv *env);
extern int register_android_media_ImageReader(JNIEnv *env);
extern int register_android_media_ImageWriter(JNIEnv *env);
extern int register_android_media_Crypto(JNIEnv *env);
@@ -1219,6 +1220,11 @@ jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
goto bail;
}
if (register_android_media_ExifInterface(env) < 0) {
ALOGE("ERROR: ExifInterface native registration failed");
goto bail;
}
/* success -- return valid version number */
result = JNI_VERSION_1_4;