diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java index 445ee6f91508c..7fb67ee5581d9 100644 --- a/media/java/android/media/ExifInterface.java +++ b/media/java/android/media/ExifInterface.java @@ -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. + *

+ * 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 mAttributes; + private final String mFilename; + private final HashMap 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 * defaultValue. * * @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 * defaultValue. * * @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 -> 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 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 iter : mAttributes.entrySet()) { - String key = iter.getKey(); - if (key.equals("hasThumbnail")) { + sb.append(size).append(" "); + for (Map.Entry 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); } diff --git a/media/jni/Android.mk b/media/jni/Android.mk index 79557bc928385..a326f6feba450 100644 --- a/media/jni/Android.mk +++ b/media/jni/Android.mk @@ -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 \ diff --git a/media/jni/android_media_ExifInterface.cpp b/media/jni/android_media_ExifInterface.cpp new file mode 100644 index 0000000000000..463c17eec77af --- /dev/null +++ b/media/jni/android_media_ExifInterface.cpp @@ -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 +#include +#include + +#include + +#include +#include + +#include + +#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 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(env->NewGlobalRef(clazz)); + + GET_METHOD_ID(gFields.hashMap.init, clazz, "", "()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 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 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)); +} diff --git a/media/jni/android_media_MediaPlayer.cpp b/media/jni/android_media_MediaPlayer.cpp index be36729683982..e9d62dec80a47 100644 --- a/media/jni/android_media_MediaPlayer.cpp +++ b/media/jni/android_media_MediaPlayer.cpp @@ -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;