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;
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
/** Type is int. */
public static final String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized";
-
-
/**
* @hide
*/
/** 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
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
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.
*/
/**
* 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.
/**
* 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.
*
* 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, String>();
String attrStr;
synchronized (sLock) {
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);
}
/**
- * 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();
}
/**
- * 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
* @hide
*/
public long[] getThumbnailRange() {
+ if (mIsRaw) {
+ long[] range = new long[2];
+ range[0] = mThumbnailOffset;
+ range[1] = mThumbnailLength;
+ return range;
+ }
+
synchronized (sLock) {
return getThumbnailRangeNative(mFilename);
}
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 static native void commitChangesNative(String fileName);
- private native void commitChangesNative(String fileName);
+ private static native byte[] getThumbnailNative(String fileName);
- private native byte[] getThumbnailNative(String fileName);
+ private static native long[] getThumbnailRangeNative(String fileName);
- private native long[] getThumbnailRangeNative(String fileName);
+ // JNI methods for RAW formats.
+ private static native void initRawNative();
+ private static native HashMap getRawAttributesNative(String filename);
}
--- /dev/null
+/*
+ * 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));
+}