blob: 36330a575df318c5bd754b71fbeccfdf44c64e35 [file] [log] [blame]
/*
* Copyright 2020 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.
*/
package androidx.camera.core.impl.utils;
import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_BYTE;
import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_DOUBLE;
import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_SLONG;
import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_SRATIONAL;
import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_STRING;
import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_ULONG;
import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_UNDEFINED;
import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_URATIONAL;
import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_USHORT;
import static androidx.exifinterface.media.ExifInterface.CONTRAST_NORMAL;
import static androidx.exifinterface.media.ExifInterface.EXPOSURE_PROGRAM_NOT_DEFINED;
import static androidx.exifinterface.media.ExifInterface.FILE_SOURCE_DSC;
import static androidx.exifinterface.media.ExifInterface.FLAG_FLASH_FIRED;
import static androidx.exifinterface.media.ExifInterface.FLAG_FLASH_NO_FLASH_FUNCTION;
import static androidx.exifinterface.media.ExifInterface.GPS_DIRECTION_TRUE;
import static androidx.exifinterface.media.ExifInterface.GPS_DISTANCE_KILOMETERS;
import static androidx.exifinterface.media.ExifInterface.GPS_SPEED_KILOMETERS_PER_HOUR;
import static androidx.exifinterface.media.ExifInterface.LIGHT_SOURCE_FLASH;
import static androidx.exifinterface.media.ExifInterface.LIGHT_SOURCE_UNKNOWN;
import static androidx.exifinterface.media.ExifInterface.METERING_MODE_UNKNOWN;
import static androidx.exifinterface.media.ExifInterface.ORIENTATION_NORMAL;
import static androidx.exifinterface.media.ExifInterface.RENDERED_PROCESS_NORMAL;
import static androidx.exifinterface.media.ExifInterface.RESOLUTION_UNIT_INCHES;
import static androidx.exifinterface.media.ExifInterface.SATURATION_NORMAL;
import static androidx.exifinterface.media.ExifInterface.SCENE_CAPTURE_TYPE_STANDARD;
import static androidx.exifinterface.media.ExifInterface.SCENE_TYPE_DIRECTLY_PHOTOGRAPHED;
import static androidx.exifinterface.media.ExifInterface.SENSITIVITY_TYPE_ISO_SPEED;
import static androidx.exifinterface.media.ExifInterface.SHARPNESS_NORMAL;
import static androidx.exifinterface.media.ExifInterface.TAG_APERTURE_VALUE;
import static androidx.exifinterface.media.ExifInterface.TAG_BRIGHTNESS_VALUE;
import static androidx.exifinterface.media.ExifInterface.TAG_COLOR_SPACE;
import static androidx.exifinterface.media.ExifInterface.TAG_COMPONENTS_CONFIGURATION;
import static androidx.exifinterface.media.ExifInterface.TAG_CONTRAST;
import static androidx.exifinterface.media.ExifInterface.TAG_CUSTOM_RENDERED;
import static androidx.exifinterface.media.ExifInterface.TAG_DATETIME;
import static androidx.exifinterface.media.ExifInterface.TAG_DATETIME_DIGITIZED;
import static androidx.exifinterface.media.ExifInterface.TAG_DATETIME_ORIGINAL;
import static androidx.exifinterface.media.ExifInterface.TAG_EXIF_VERSION;
import static androidx.exifinterface.media.ExifInterface.TAG_EXPOSURE_BIAS_VALUE;
import static androidx.exifinterface.media.ExifInterface.TAG_EXPOSURE_MODE;
import static androidx.exifinterface.media.ExifInterface.TAG_EXPOSURE_PROGRAM;
import static androidx.exifinterface.media.ExifInterface.TAG_EXPOSURE_TIME;
import static androidx.exifinterface.media.ExifInterface.TAG_FILE_SOURCE;
import static androidx.exifinterface.media.ExifInterface.TAG_FLASH;
import static androidx.exifinterface.media.ExifInterface.TAG_FLASHPIX_VERSION;
import static androidx.exifinterface.media.ExifInterface.TAG_FOCAL_LENGTH;
import static androidx.exifinterface.media.ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT;
import static androidx.exifinterface.media.ExifInterface.TAG_F_NUMBER;
import static androidx.exifinterface.media.ExifInterface.TAG_GPS_ALTITUDE;
import static androidx.exifinterface.media.ExifInterface.TAG_GPS_ALTITUDE_REF;
import static androidx.exifinterface.media.ExifInterface.TAG_GPS_DEST_BEARING_REF;
import static androidx.exifinterface.media.ExifInterface.TAG_GPS_DEST_DISTANCE_REF;
import static androidx.exifinterface.media.ExifInterface.TAG_GPS_IMG_DIRECTION_REF;
import static androidx.exifinterface.media.ExifInterface.TAG_GPS_LATITUDE;
import static androidx.exifinterface.media.ExifInterface.TAG_GPS_LATITUDE_REF;
import static androidx.exifinterface.media.ExifInterface.TAG_GPS_LONGITUDE;
import static androidx.exifinterface.media.ExifInterface.TAG_GPS_LONGITUDE_REF;
import static androidx.exifinterface.media.ExifInterface.TAG_GPS_SPEED_REF;
import static androidx.exifinterface.media.ExifInterface.TAG_GPS_TIMESTAMP;
import static androidx.exifinterface.media.ExifInterface.TAG_GPS_TRACK_REF;
import static androidx.exifinterface.media.ExifInterface.TAG_GPS_VERSION_ID;
import static androidx.exifinterface.media.ExifInterface.TAG_IMAGE_LENGTH;
import static androidx.exifinterface.media.ExifInterface.TAG_IMAGE_WIDTH;
import static androidx.exifinterface.media.ExifInterface.TAG_INTEROPERABILITY_INDEX;
import static androidx.exifinterface.media.ExifInterface.TAG_ISO_SPEED_RATINGS;
import static androidx.exifinterface.media.ExifInterface.TAG_LIGHT_SOURCE;
import static androidx.exifinterface.media.ExifInterface.TAG_MAKE;
import static androidx.exifinterface.media.ExifInterface.TAG_MAX_APERTURE_VALUE;
import static androidx.exifinterface.media.ExifInterface.TAG_METERING_MODE;
import static androidx.exifinterface.media.ExifInterface.TAG_MODEL;
import static androidx.exifinterface.media.ExifInterface.TAG_ORIENTATION;
import static androidx.exifinterface.media.ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY;
import static androidx.exifinterface.media.ExifInterface.TAG_PIXEL_X_DIMENSION;
import static androidx.exifinterface.media.ExifInterface.TAG_PIXEL_Y_DIMENSION;
import static androidx.exifinterface.media.ExifInterface.TAG_RESOLUTION_UNIT;
import static androidx.exifinterface.media.ExifInterface.TAG_SATURATION;
import static androidx.exifinterface.media.ExifInterface.TAG_SCENE_CAPTURE_TYPE;
import static androidx.exifinterface.media.ExifInterface.TAG_SCENE_TYPE;
import static androidx.exifinterface.media.ExifInterface.TAG_SENSING_METHOD;
import static androidx.exifinterface.media.ExifInterface.TAG_SENSITIVITY_TYPE;
import static androidx.exifinterface.media.ExifInterface.TAG_SHARPNESS;
import static androidx.exifinterface.media.ExifInterface.TAG_SHUTTER_SPEED_VALUE;
import static androidx.exifinterface.media.ExifInterface.TAG_SOFTWARE;
import static androidx.exifinterface.media.ExifInterface.TAG_SUBSEC_TIME;
import static androidx.exifinterface.media.ExifInterface.TAG_SUBSEC_TIME_DIGITIZED;
import static androidx.exifinterface.media.ExifInterface.TAG_SUBSEC_TIME_ORIGINAL;
import static androidx.exifinterface.media.ExifInterface.TAG_WHITE_BALANCE;
import static androidx.exifinterface.media.ExifInterface.TAG_X_RESOLUTION;
import static androidx.exifinterface.media.ExifInterface.TAG_Y_CB_CR_POSITIONING;
import static androidx.exifinterface.media.ExifInterface.TAG_Y_RESOLUTION;
import static androidx.exifinterface.media.ExifInterface.WHITE_BALANCE_AUTO;
import static androidx.exifinterface.media.ExifInterface.WHITE_BALANCE_MANUAL;
import static androidx.exifinterface.media.ExifInterface.Y_CB_CR_POSITIONING_CENTERED;
import android.os.Build;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.camera.core.ImageInfo;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.Logger;
import androidx.camera.core.impl.CameraCaptureMetaData;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.core.util.Preconditions;
import androidx.exifinterface.media.ExifInterface;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This class stores the EXIF header in IFDs according to the JPEG specification.
*/
// Note: This class is adapted from {@link androidx.exifinterface.media.ExifInterface}, and is
// currently expected to be used for writing a subset of Exif values. Support for other mime
// types besides JPEG have been removed. Support for thumbnails/strips has been removed along
// with many exif tags. If more tags are required, the source code for ExifInterface should be
// referenced and can be adapted to this class.
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class ExifData {
private static final String TAG = "ExifData";
private static final boolean DEBUG = false;
/**
* Enum representing the white balance mode.
*/
public enum WhiteBalanceMode {
/** AWB is turned on. */
AUTO,
/** AWB is turned off. */
MANUAL
}
// Names for the data formats for debugging purpose.
static final String[] IFD_FORMAT_NAMES = new String[]{
"", "BYTE", "STRING", "USHORT", "ULONG", "URATIONAL", "SBYTE", "UNDEFINED", "SSHORT",
"SLONG", "SRATIONAL", "SINGLE", "DOUBLE", "IFD"
};
/**
* Private tags used for pointing the other IFD offsets.
* The types of the following tags are int.
* See JEITA CP-3451C Section 4.6.3: Exif-specific IFD.
* For SubIFD, see Note 1 of Adobe PageMaker® 6.0 TIFF Technical Notes.
*/
static final String TAG_EXIF_IFD_POINTER = "ExifIFDPointer";
static final String TAG_GPS_INFO_IFD_POINTER = "GPSInfoIFDPointer";
static final String TAG_INTEROPERABILITY_IFD_POINTER = "InteroperabilityIFDPointer";
static final String TAG_SUB_IFD_POINTER = "SubIFDPointer";
// Primary image IFD TIFF tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
// This is only a subset of the tags defined in ExifInterface
private static final ExifTag[] IFD_TIFF_TAGS = new ExifTag[]{
// For below two, see TIFF 6.0 Spec Section 3: Bilevel Images.
new ExifTag(TAG_IMAGE_WIDTH, 256, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
new ExifTag(TAG_IMAGE_LENGTH, 257, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
new ExifTag(TAG_MAKE, 271, IFD_FORMAT_STRING),
new ExifTag(TAG_MODEL, 272, IFD_FORMAT_STRING),
new ExifTag(TAG_ORIENTATION, 274, IFD_FORMAT_USHORT),
new ExifTag(TAG_X_RESOLUTION, 282, IFD_FORMAT_URATIONAL),
new ExifTag(TAG_Y_RESOLUTION, 283, IFD_FORMAT_URATIONAL),
new ExifTag(TAG_RESOLUTION_UNIT, 296, IFD_FORMAT_USHORT),
new ExifTag(TAG_SOFTWARE, 305, IFD_FORMAT_STRING),
new ExifTag(TAG_DATETIME, 306, IFD_FORMAT_STRING),
new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT),
new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG),
new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
};
// Primary image IFD Exif Private tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
// This is only a subset of the tags defined in ExifInterface
private static final ExifTag[] IFD_EXIF_TAGS = new ExifTag[]{
new ExifTag(TAG_EXPOSURE_TIME, 33434, IFD_FORMAT_URATIONAL),
new ExifTag(TAG_F_NUMBER, 33437, IFD_FORMAT_URATIONAL),
new ExifTag(TAG_EXPOSURE_PROGRAM, 34850, IFD_FORMAT_USHORT),
new ExifTag(TAG_PHOTOGRAPHIC_SENSITIVITY, 34855, IFD_FORMAT_USHORT),
new ExifTag(TAG_SENSITIVITY_TYPE, 34864, IFD_FORMAT_USHORT),
new ExifTag(TAG_EXIF_VERSION, 36864, IFD_FORMAT_STRING),
new ExifTag(TAG_DATETIME_ORIGINAL, 36867, IFD_FORMAT_STRING),
new ExifTag(TAG_DATETIME_DIGITIZED, 36868, IFD_FORMAT_STRING),
new ExifTag(TAG_COMPONENTS_CONFIGURATION, 37121, IFD_FORMAT_UNDEFINED),
new ExifTag(TAG_SHUTTER_SPEED_VALUE, 37377, IFD_FORMAT_SRATIONAL),
new ExifTag(TAG_APERTURE_VALUE, 37378, IFD_FORMAT_URATIONAL),
new ExifTag(TAG_BRIGHTNESS_VALUE, 37379, IFD_FORMAT_SRATIONAL),
new ExifTag(TAG_EXPOSURE_BIAS_VALUE, 37380, IFD_FORMAT_SRATIONAL),
new ExifTag(TAG_MAX_APERTURE_VALUE, 37381, IFD_FORMAT_URATIONAL),
new ExifTag(TAG_METERING_MODE, 37383, IFD_FORMAT_USHORT),
new ExifTag(TAG_LIGHT_SOURCE, 37384, IFD_FORMAT_USHORT),
new ExifTag(TAG_FLASH, 37385, IFD_FORMAT_USHORT),
new ExifTag(TAG_FOCAL_LENGTH, 37386, IFD_FORMAT_URATIONAL),
new ExifTag(TAG_SUBSEC_TIME, 37520, IFD_FORMAT_STRING),
new ExifTag(TAG_SUBSEC_TIME_ORIGINAL, 37521, IFD_FORMAT_STRING),
new ExifTag(TAG_SUBSEC_TIME_DIGITIZED, 37522, IFD_FORMAT_STRING),
new ExifTag(TAG_FLASHPIX_VERSION, 40960, IFD_FORMAT_UNDEFINED),
new ExifTag(TAG_COLOR_SPACE, 40961, IFD_FORMAT_USHORT),
new ExifTag(TAG_PIXEL_X_DIMENSION, 40962, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
new ExifTag(TAG_PIXEL_Y_DIMENSION, 40963, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG),
new ExifTag(TAG_FOCAL_PLANE_RESOLUTION_UNIT, 41488, IFD_FORMAT_USHORT),
new ExifTag(TAG_SENSING_METHOD, 41495, IFD_FORMAT_USHORT),
new ExifTag(TAG_FILE_SOURCE, 41728, IFD_FORMAT_UNDEFINED),
new ExifTag(TAG_SCENE_TYPE, 41729, IFD_FORMAT_UNDEFINED),
new ExifTag(TAG_CUSTOM_RENDERED, 41985, IFD_FORMAT_USHORT),
new ExifTag(TAG_EXPOSURE_MODE, 41986, IFD_FORMAT_USHORT),
new ExifTag(TAG_WHITE_BALANCE, 41987, IFD_FORMAT_USHORT),
new ExifTag(TAG_SCENE_CAPTURE_TYPE, 41990, IFD_FORMAT_USHORT),
new ExifTag(TAG_CONTRAST, 41992, IFD_FORMAT_USHORT),
new ExifTag(TAG_SATURATION, 41993, IFD_FORMAT_USHORT),
new ExifTag(TAG_SHARPNESS, 41994, IFD_FORMAT_USHORT)
};
// Primary image IFD GPS Info tags (See JEITA CP-3451C Section 4.6.6 Tag Support Levels)
// This is only a subset of the tags defined in ExifInterface
private static final ExifTag[] IFD_GPS_TAGS = new ExifTag[]{
new ExifTag(TAG_GPS_VERSION_ID, 0, IFD_FORMAT_BYTE),
new ExifTag(TAG_GPS_LATITUDE_REF, 1, IFD_FORMAT_STRING),
// Allow SRATIONAL to be compatible with apps using wrong format and
// even if it is negative, it may be valid latitude / longitude.
new ExifTag(TAG_GPS_LATITUDE, 2, IFD_FORMAT_URATIONAL, IFD_FORMAT_SRATIONAL),
new ExifTag(TAG_GPS_LONGITUDE_REF, 3, IFD_FORMAT_STRING),
new ExifTag(TAG_GPS_LONGITUDE, 4, IFD_FORMAT_URATIONAL, IFD_FORMAT_SRATIONAL),
new ExifTag(TAG_GPS_ALTITUDE_REF, 5, IFD_FORMAT_BYTE),
new ExifTag(TAG_GPS_ALTITUDE, 6, IFD_FORMAT_URATIONAL),
new ExifTag(TAG_GPS_TIMESTAMP, 7, IFD_FORMAT_URATIONAL),
new ExifTag(TAG_GPS_SPEED_REF, 12, IFD_FORMAT_STRING),
new ExifTag(TAG_GPS_TRACK_REF, 14, IFD_FORMAT_STRING),
new ExifTag(TAG_GPS_IMG_DIRECTION_REF, 16, IFD_FORMAT_STRING),
new ExifTag(TAG_GPS_DEST_BEARING_REF, 23, IFD_FORMAT_STRING),
new ExifTag(TAG_GPS_DEST_DISTANCE_REF, 25, IFD_FORMAT_STRING)
};
// List of tags for pointing to the other image file directory offset.
static final ExifTag[] EXIF_POINTER_TAGS = new ExifTag[]{
new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG),
new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG),
};
// Primary image IFD Interoperability tag (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
private static final ExifTag[] IFD_INTEROPERABILITY_TAGS = new ExifTag[]{
new ExifTag(TAG_INTEROPERABILITY_INDEX, 1, IFD_FORMAT_STRING)
};
// List of Exif tag groups
static final ExifTag[][] EXIF_TAGS = new ExifTag[][]{
IFD_TIFF_TAGS, IFD_EXIF_TAGS, IFD_GPS_TAGS, IFD_INTEROPERABILITY_TAGS
};
// Indices for the above tags. Note these must stay in sync with the order of EXIF_TAGS.
static final int IFD_TYPE_PRIMARY = 0;
static final int IFD_TYPE_EXIF = 1;
static final int IFD_TYPE_GPS = 2;
static final int IFD_TYPE_INTEROPERABILITY = 3;
// NOTE: This is a subset of the tags from ExifInterface. Only supports tags in this class.
static final HashSet<String> sTagSetForCompatibility = new HashSet<>(Arrays.asList(
TAG_F_NUMBER, TAG_EXPOSURE_TIME, TAG_GPS_TIMESTAMP));
private static final int MM_IN_MICRONS = 1000;
private final List<Map<String, ExifAttribute>> mAttributes;
private final ByteOrder mByteOrder;
ExifData(ByteOrder order, List<Map<String, ExifAttribute>> attributes) {
Preconditions.checkState(attributes.size() == EXIF_TAGS.length, "Malformed attributes "
+ "list. Number of IFDs mismatch.");
mByteOrder = order;
mAttributes = attributes;
}
/**
* Creates a {@link ExifData} from {@link ImageProxy} and rotation degrees.
*
* @param rotationDegrees overwrites the rotation degrees in the {@link ImageInfo}.
*/
@NonNull
public static ExifData create(@NonNull ImageProxy imageProxy,
@ImageOutputConfig.RotationDegreesValue int rotationDegrees) {
ExifData.Builder builder = ExifData.builderForDevice();
if (imageProxy.getImageInfo() != null) {
imageProxy.getImageInfo().populateExifData(builder);
}
// Overwrites the orientation degrees value of the output image because the capture
// results might not have correct value when capturing image in YUV_420_888 format. See
// b/204375890.
builder.setOrientationDegrees(rotationDegrees);
return builder.setImageWidth(imageProxy.getWidth())
.setImageHeight(imageProxy.getHeight())
.build();
}
/**
* Gets the byte order.
*/
@NonNull
public ByteOrder getByteOrder() {
return mByteOrder;
}
@NonNull
Map<String, ExifAttribute> getAttributes(int ifdIndex) {
Preconditions.checkArgumentInRange(ifdIndex, 0, EXIF_TAGS.length,
"Invalid IFD index: " + ifdIndex + ". Index should be between [0, EXIF_TAGS"
+ ".length] ");
return mAttributes.get(ifdIndex);
}
/**
* Returns the value of the specified tag or {@code null} if there
* is no such tag in the image file.
*
* @param tag the name of the tag.
*/
@Nullable
public String getAttribute(@NonNull String tag) {
ExifAttribute attribute = getExifAttribute(tag);
if (attribute != null) {
if (!sTagSetForCompatibility.contains(tag)) {
return attribute.getStringValue(mByteOrder);
}
if (tag.equals(TAG_GPS_TIMESTAMP)) {
// Convert the rational values to the custom formats for backwards compatibility.
if (attribute.format != IFD_FORMAT_URATIONAL
&& attribute.format != IFD_FORMAT_SRATIONAL) {
Logger.w(TAG,
"GPS Timestamp format is not rational. format=" + attribute.format);
return null;
}
LongRational[] array =
(LongRational[]) attribute.getValue(mByteOrder);
if (array == null || array.length != 3) {
Logger.w(TAG, "Invalid GPS Timestamp array. array=" + Arrays.toString(array));
return null;
}
return String.format(Locale.US, "%02d:%02d:%02d",
(int) ((float) array[0].getNumerator() / array[0].getDenominator()),
(int) ((float) array[1].getNumerator() / array[1].getDenominator()),
(int) ((float) array[2].getNumerator() / array[2].getDenominator()));
}
try {
return Double.toString(attribute.getDoubleValue(mByteOrder));
} catch (NumberFormatException e) {
return null;
}
}
return null;
}
/**
* Returns the EXIF attribute of the specified tag or {@code null} if there is no such tag.
*
* @param tag the name of the tag.
*/
@SuppressWarnings("deprecation")
@Nullable
private ExifAttribute getExifAttribute(@NonNull String tag) {
// Maintain compatibility.
if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
if (DEBUG) {
Logger.d(TAG, "getExifAttribute: Replacing TAG_ISO_SPEED_RATINGS with "
+ "TAG_PHOTOGRAPHIC_SENSITIVITY.");
}
tag = TAG_PHOTOGRAPHIC_SENSITIVITY;
}
// Retrieves all tag groups. The value from primary image tag group has a higher priority
// than the value from the thumbnail tag group if there are more than one candidates.
for (int i = 0; i < EXIF_TAGS.length; ++i) {
ExifAttribute value = mAttributes.get(i).get(tag);
if (value != null) {
return value;
}
}
return null;
}
/**
* Generates an empty builder suitable for generating ExifData for JPEG from the current device.
*/
@NonNull
public static Builder builderForDevice() {
// Add PRIMARY defaults. EXIF and GPS defaults will be added in build()
return new Builder(ByteOrder.BIG_ENDIAN)
.setAttribute(TAG_ORIENTATION, String.valueOf(ORIENTATION_NORMAL))
.setAttribute(TAG_X_RESOLUTION, "72/1")
.setAttribute(TAG_Y_RESOLUTION, "72/1")
.setAttribute(TAG_RESOLUTION_UNIT, String.valueOf(RESOLUTION_UNIT_INCHES))
.setAttribute(TAG_Y_CB_CR_POSITIONING,
String.valueOf(Y_CB_CR_POSITIONING_CENTERED))
// Defaults derived from device
.setAttribute(TAG_MAKE, Build.MANUFACTURER)
.setAttribute(TAG_MODEL, Build.MODEL);
}
/**
* Builder for the {@link ExifData} class.
*/
public static final class Builder {
// Pattern to check gps timestamp
private static final Pattern GPS_TIMESTAMP_PATTERN =
Pattern.compile("^(\\d{2}):(\\d{2}):(\\d{2})$");
// Pattern to check date time primary format (e.g. 2020:01:01 00:00:00)
private static final Pattern DATETIME_PRIMARY_FORMAT_PATTERN =
Pattern.compile("^(\\d{4}):(\\d{2}):(\\d{2})\\s(\\d{2}):(\\d{2}):(\\d{2})$");
// Pattern to check date time secondary format (e.g. 2020-01-01 00:00:00)
private static final Pattern DATETIME_SECONDARY_FORMAT_PATTERN =
Pattern.compile("^(\\d{4})-(\\d{2})-(\\d{2})\\s(\\d{2}):(\\d{2}):(\\d{2})$");
private static final int DATETIME_VALUE_STRING_LENGTH = 19;
// Mappings from tag name to tag number and each item represents one IFD tag group.
static final List<HashMap<String, ExifTag>> sExifTagMapsForWriting =
Collections.list(new Enumeration<HashMap<String, ExifTag>>() {
int mIfdIndex = 0;
@Override
public boolean hasMoreElements() {
return mIfdIndex < EXIF_TAGS.length;
}
@Override
public HashMap<String, ExifTag> nextElement() {
// Build up the hash tables to look up Exif tags for writing Exif tags.
HashMap<String, ExifTag> map = new HashMap<>();
for (ExifTag tag : EXIF_TAGS[mIfdIndex]) {
map.put(tag.name, tag);
}
mIfdIndex++;
return map;
}
});
final List<Map<String, ExifAttribute>> mAttributes = Collections.list(
new Enumeration<Map<String, ExifAttribute>>() {
int mIfdIndex = 0;
@Override
public boolean hasMoreElements() {
return mIfdIndex < EXIF_TAGS.length;
}
@Override
public Map<String, ExifAttribute> nextElement() {
mIfdIndex++;
return new HashMap<>();
}
});
private final ByteOrder mByteOrder;
Builder(@NonNull ByteOrder byteOrder) {
mByteOrder = byteOrder;
}
/**
* Sets the width of the image.
*
* @param width the width of the image.
*/
@NonNull
public Builder setImageWidth(int width) {
return setAttribute(TAG_IMAGE_WIDTH, String.valueOf(width));
}
/**
* Sets the height of the image.
*
* @param height the height of the image.
*/
@NonNull
public Builder setImageHeight(int height) {
return setAttribute(TAG_IMAGE_LENGTH, String.valueOf(height));
}
/**
* Sets the orientation of the image in degrees.
*
* @param orientationDegrees the orientation in degrees. Can be one of (0, 90, 180, 270)
*/
@NonNull
public Builder setOrientationDegrees(int orientationDegrees) {
int orientationEnum;
switch (orientationDegrees) {
case 0:
orientationEnum = ExifInterface.ORIENTATION_NORMAL;
break;
case 90:
orientationEnum = ExifInterface.ORIENTATION_ROTATE_90;
break;
case 180:
orientationEnum = ExifInterface.ORIENTATION_ROTATE_180;
break;
case 270:
orientationEnum = ExifInterface.ORIENTATION_ROTATE_270;
break;
default:
Logger.w(TAG,
"Unexpected orientation value: " + orientationDegrees
+ ". Must be one of 0, 90, 180, 270.");
orientationEnum = ExifInterface.ORIENTATION_UNDEFINED;
break;
}
return setAttribute(TAG_ORIENTATION, String.valueOf(orientationEnum));
}
/**
* Sets the flash information from
* {@link androidx.camera.core.impl.CameraCaptureMetaData.FlashState}.
*
* @param flashState the state of the flash at capture time.
*/
@NonNull
public Builder setFlashState(@NonNull CameraCaptureMetaData.FlashState flashState) {
if (flashState == CameraCaptureMetaData.FlashState.UNKNOWN) {
// Cannot set flash state information
return this;
}
short value;
switch (flashState) {
case READY:
value = 0;
break;
case NONE:
value = FLAG_FLASH_NO_FLASH_FUNCTION;
break;
case FIRED:
value = FLAG_FLASH_FIRED;
break;
default:
Logger.w(TAG, "Unknown flash state: " + flashState);
return this;
}
if ((value & FLAG_FLASH_FIRED) == FLAG_FLASH_FIRED) {
// Set light source to flash
setAttribute(TAG_LIGHT_SOURCE, String.valueOf(LIGHT_SOURCE_FLASH));
}
return setAttribute(TAG_FLASH, String.valueOf(value));
}
/**
* Sets the amount of time the sensor was exposed for, in nanoseconds.
*
* @param exposureTimeNs The exposure time in nanoseconds.
*/
@NonNull
public Builder setExposureTimeNanos(long exposureTimeNs) {
return setAttribute(TAG_EXPOSURE_TIME,
String.valueOf(exposureTimeNs / (double) TimeUnit.SECONDS.toNanos(1)));
}
/**
* Sets the lens f-number.
*
* <p>The lens f-number has precision 1.xx, for example, 1.80.
*
* @param fNumber The f-number.
*/
@NonNull
public Builder setLensFNumber(float fNumber) {
return setAttribute(TAG_F_NUMBER, String.valueOf(fNumber));
}
/**
* Sets the ISO.
*
* @param iso the standard ISO sensitivity value, as defined in ISO 12232:2006.
*/
@NonNull
public Builder setIso(int iso) {
return setAttribute(TAG_SENSITIVITY_TYPE, String.valueOf(SENSITIVITY_TYPE_ISO_SPEED))
.setAttribute(TAG_PHOTOGRAPHIC_SENSITIVITY, String.valueOf(Math.min(65535,
iso)));
}
/**
* Sets lens focal length, in millimeters.
*
* @param focalLength The lens focal length in millimeters.
*/
@NonNull
public Builder setFocalLength(float focalLength) {
LongRational focalLengthRational =
new LongRational((long) (focalLength * MM_IN_MICRONS), MM_IN_MICRONS);
return setAttribute(TAG_FOCAL_LENGTH, focalLengthRational.toString());
}
/**
* Sets the white balance mode.
*
* @param whiteBalanceMode The white balance mode. One of {@link WhiteBalanceMode#AUTO}
* or {@link WhiteBalanceMode#MANUAL}.
*/
@NonNull
public Builder setWhiteBalanceMode(@NonNull WhiteBalanceMode whiteBalanceMode) {
String wbString = null;
switch (whiteBalanceMode) {
case AUTO:
wbString = String.valueOf(WHITE_BALANCE_AUTO);
break;
case MANUAL:
wbString = String.valueOf(WHITE_BALANCE_MANUAL);
break;
}
return setAttribute(TAG_WHITE_BALANCE, wbString);
}
/**
* Sets the value of the specified tag.
*
* @param tag the name of the tag.
* @param value the value of the tag.
*/
@NonNull
public Builder setAttribute(@NonNull String tag, @NonNull String value) {
setAttributeInternal(tag, value, mAttributes);
return this;
}
/**
* Removes the attribute with the given tag.
*
* @param tag the name of the tag.
*/
@NonNull
public Builder removeAttribute(@NonNull String tag) {
setAttributeInternal(tag, null, mAttributes);
return this;
}
private void setAttributeIfMissing(@NonNull String tag, @NonNull String value,
@NonNull List<Map<String, ExifAttribute>> attributes) {
for (Map<String, ExifAttribute> attrs : attributes) {
if (attrs.containsKey(tag)) {
// Attr already exists
return;
}
}
// Add missing attribute.
setAttributeInternal(tag, value, attributes);
}
@SuppressWarnings("deprecation")
// Allows null values to remove attributes
private void setAttributeInternal(@NonNull String tag, @Nullable String value,
@NonNull List<Map<String, ExifAttribute>> attributes) {
// Validate and convert if necessary.
if (TAG_DATETIME.equals(tag) || TAG_DATETIME_ORIGINAL.equals(tag)
|| TAG_DATETIME_DIGITIZED.equals(tag)) {
if (value != null) {
boolean isPrimaryFormat = DATETIME_PRIMARY_FORMAT_PATTERN.matcher(value).find();
boolean isSecondaryFormat = DATETIME_SECONDARY_FORMAT_PATTERN.matcher(
value).find();
// Validate
if (value.length() != DATETIME_VALUE_STRING_LENGTH
|| (!isPrimaryFormat && !isSecondaryFormat)) {
Logger.w(TAG, "Invalid value for " + tag + " : " + value);
return;
}
// If datetime value has secondary format (e.g. 2020-01-01 00:00:00), convert it
// to primary format (e.g. 2020:01:01 00:00:00) since it is the format in the
// official documentation.
// See JEITA CP-3451C Section 4.6.4. D. Other Tags, DateTime
if (isSecondaryFormat) {
// Replace "-" with ":" to match the primary format.
value = value.replaceAll("-", ":");
}
}
}
// Maintain compatibility.
if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
if (DEBUG) {
Logger.d(TAG, "setAttribute: Replacing TAG_ISO_SPEED_RATINGS with "
+ "TAG_PHOTOGRAPHIC_SENSITIVITY.");
}
tag = TAG_PHOTOGRAPHIC_SENSITIVITY;
}
// Convert the given value to rational values for backwards compatibility.
if (value != null && sTagSetForCompatibility.contains(tag)) {
if (tag.equals(TAG_GPS_TIMESTAMP)) {
Matcher m = GPS_TIMESTAMP_PATTERN.matcher(value);
if (!m.find()) {
Logger.w(TAG, "Invalid value for " + tag + " : " + value);
return;
}
value = Integer.parseInt(Preconditions.checkNotNull(m.group(1))) + "/1,"
+ Integer.parseInt(Preconditions.checkNotNull(m.group(2))) + "/1,"
+ Integer.parseInt(Preconditions.checkNotNull(m.group(3))) + "/1";
} else {
try {
double doubleValue = Double.parseDouble(value);
value = new LongRational(doubleValue).toString();
} catch (NumberFormatException e) {
Logger.w(TAG, "Invalid value for " + tag + " : " + value, e);
return;
}
}
}
for (int i = 0; i < EXIF_TAGS.length; ++i) {
final ExifTag exifTag = sExifTagMapsForWriting.get(i).get(tag);
if (exifTag != null) {
if (value == null) {
attributes.get(i).remove(tag);
continue;
}
Pair<Integer, Integer> guess = guessDataFormat(value);
int dataFormat;
if (exifTag.primaryFormat == guess.first
|| exifTag.primaryFormat == guess.second) {
dataFormat = exifTag.primaryFormat;
} else if (exifTag.secondaryFormat != -1 && (
exifTag.secondaryFormat == guess.first
|| exifTag.secondaryFormat == guess.second)) {
dataFormat = exifTag.secondaryFormat;
} else if (exifTag.primaryFormat == IFD_FORMAT_BYTE
|| exifTag.primaryFormat == IFD_FORMAT_UNDEFINED
|| exifTag.primaryFormat == IFD_FORMAT_STRING) {
dataFormat = exifTag.primaryFormat;
} else {
if (DEBUG) {
Logger.d(TAG, "Given tag (" + tag
+ ") value didn't match with one of expected "
+ "formats: " + IFD_FORMAT_NAMES[exifTag.primaryFormat]
+ (exifTag.secondaryFormat == -1 ? "" : ", "
+ IFD_FORMAT_NAMES[exifTag.secondaryFormat]) + " (guess: "
+ IFD_FORMAT_NAMES[guess.first] + (guess.second == -1 ? ""
: ", "
+ IFD_FORMAT_NAMES[guess.second]) + ")");
}
continue;
}
switch (dataFormat) {
case IFD_FORMAT_BYTE: {
attributes.get(i).put(tag, ExifAttribute.createByte(value));
break;
}
case IFD_FORMAT_UNDEFINED:
case IFD_FORMAT_STRING: {
attributes.get(i).put(tag, ExifAttribute.createString(value));
break;
}
case IFD_FORMAT_USHORT: {
final String[] values = value.split(",", -1);
final int[] intArray = new int[values.length];
for (int j = 0; j < values.length; ++j) {
intArray[j] = Integer.parseInt(values[j]);
}
attributes.get(i).put(tag,
ExifAttribute.createUShort(intArray, mByteOrder));
break;
}
case IFD_FORMAT_SLONG: {
final String[] values = value.split(",", -1);
final int[] intArray = new int[values.length];
for (int j = 0; j < values.length; ++j) {
intArray[j] = Integer.parseInt(values[j]);
}
attributes.get(i).put(tag,
ExifAttribute.createSLong(intArray, mByteOrder));
break;
}
case IFD_FORMAT_ULONG: {
final String[] values = value.split(",", -1);
final long[] longArray = new long[values.length];
for (int j = 0; j < values.length; ++j) {
longArray[j] = Long.parseLong(values[j]);
}
attributes.get(i).put(tag,
ExifAttribute.createULong(longArray, mByteOrder));
break;
}
case IFD_FORMAT_URATIONAL: {
final String[] values = value.split(",", -1);
final LongRational[] rationalArray = new LongRational[values.length];
for (int j = 0; j < values.length; ++j) {
final String[] numbers = values[j].split("/", -1);
rationalArray[j] = new LongRational(
(long) Double.parseDouble(numbers[0]),
(long) Double.parseDouble(numbers[1]));
}
attributes.get(i).put(tag,
ExifAttribute.createURational(rationalArray, mByteOrder));
break;
}
case IFD_FORMAT_SRATIONAL: {
final String[] values = value.split(",", -1);
final LongRational[] rationalArray = new LongRational[values.length];
for (int j = 0; j < values.length; ++j) {
final String[] numbers = values[j].split("/", -1);
rationalArray[j] = new LongRational(
(long) Double.parseDouble(numbers[0]),
(long) Double.parseDouble(numbers[1]));
}
attributes.get(i).put(tag,
ExifAttribute.createSRational(rationalArray, mByteOrder));
break;
}
case IFD_FORMAT_DOUBLE: {
final String[] values = value.split(",", -1);
final double[] doubleArray = new double[values.length];
for (int j = 0; j < values.length; ++j) {
doubleArray[j] = Double.parseDouble(values[j]);
}
attributes.get(i).put(tag,
ExifAttribute.createDouble(doubleArray, mByteOrder));
break;
}
default:
if (DEBUG) {
Logger.d(TAG,
"Data format isn't one of expected formats: " + dataFormat);
}
}
}
}
}
/**
* Builds an {@link ExifData} from the current state of the builder.
*/
@NonNull
public ExifData build() {
// Create a read-only copy of all attributes. This needs to be a deep copy since
// build() can be called multiple times. We'll remove null values as well.
List<Map<String, ExifAttribute>> attributes = Collections.list(
new Enumeration<Map<String, ExifAttribute>>() {
final Enumeration<Map<String, ExifAttribute>> mMapEnumeration =
Collections.enumeration(mAttributes);
@Override
public boolean hasMoreElements() {
return mMapEnumeration.hasMoreElements();
}
@Override
public Map<String, ExifAttribute> nextElement() {
return new HashMap<>(mMapEnumeration.nextElement());
}
});
// Add EXIF defaults if needed
if (!attributes.get(IFD_TYPE_EXIF).isEmpty()) {
setAttributeIfMissing(TAG_EXPOSURE_PROGRAM,
String.valueOf(EXPOSURE_PROGRAM_NOT_DEFINED), attributes);
setAttributeIfMissing(TAG_EXIF_VERSION, "0230", attributes);
// Default is for YCbCr components
setAttributeIfMissing(TAG_COMPONENTS_CONFIGURATION, "1,2,3,0", attributes);
setAttributeIfMissing(TAG_METERING_MODE, String.valueOf(METERING_MODE_UNKNOWN),
attributes);
setAttributeIfMissing(TAG_LIGHT_SOURCE, String.valueOf(LIGHT_SOURCE_UNKNOWN),
attributes);
setAttributeIfMissing(TAG_FLASHPIX_VERSION, "0100", attributes);
setAttributeIfMissing(TAG_FOCAL_PLANE_RESOLUTION_UNIT,
String.valueOf(RESOLUTION_UNIT_INCHES), attributes);
setAttributeIfMissing(TAG_FILE_SOURCE, String.valueOf(FILE_SOURCE_DSC), attributes);
setAttributeIfMissing(TAG_SCENE_TYPE,
String.valueOf(SCENE_TYPE_DIRECTLY_PHOTOGRAPHED), attributes);
setAttributeIfMissing(TAG_CUSTOM_RENDERED, String.valueOf(RENDERED_PROCESS_NORMAL),
attributes);
setAttributeIfMissing(TAG_SCENE_CAPTURE_TYPE,
String.valueOf(SCENE_CAPTURE_TYPE_STANDARD), attributes);
setAttributeIfMissing(TAG_CONTRAST, String.valueOf(CONTRAST_NORMAL), attributes);
setAttributeIfMissing(TAG_SATURATION, String.valueOf(SATURATION_NORMAL),
attributes);
setAttributeIfMissing(TAG_SHARPNESS, String.valueOf(SHARPNESS_NORMAL), attributes);
}
// Add GPS defaults if needed
if (!attributes.get(IFD_TYPE_GPS).isEmpty()) {
setAttributeIfMissing(TAG_GPS_VERSION_ID, "2300", attributes);
setAttributeIfMissing(TAG_GPS_SPEED_REF, GPS_SPEED_KILOMETERS_PER_HOUR, attributes);
setAttributeIfMissing(TAG_GPS_TRACK_REF, GPS_DIRECTION_TRUE, attributes);
setAttributeIfMissing(TAG_GPS_IMG_DIRECTION_REF, GPS_DIRECTION_TRUE, attributes);
setAttributeIfMissing(TAG_GPS_DEST_BEARING_REF, GPS_DIRECTION_TRUE, attributes);
setAttributeIfMissing(TAG_GPS_DEST_DISTANCE_REF, GPS_DISTANCE_KILOMETERS,
attributes);
}
return new ExifData(mByteOrder, attributes);
}
/**
* Determines the data format of EXIF entry value.
*
* @param entryValue The value to be determined.
* @return Returns two data formats guessed as a pair in integer. If there is no two
* candidate
* data formats for the given entry value, returns {@code -1} in the second of the pair.
*/
private static Pair<Integer, Integer> guessDataFormat(String entryValue) {
// See TIFF 6.0 Section 2, "Image File Directory".
// Take the first component if there are more than one component.
if (entryValue.contains(",")) {
String[] entryValues = entryValue.split(",", -1);
Pair<Integer, Integer> dataFormat = guessDataFormat(entryValues[0]);
if (dataFormat.first == IFD_FORMAT_STRING) {
return dataFormat;
}
for (int i = 1; i < entryValues.length; ++i) {
final Pair<Integer, Integer> guessDataFormat = guessDataFormat(entryValues[i]);
int first = -1, second = -1;
if (guessDataFormat.first.equals(dataFormat.first)
|| guessDataFormat.second.equals(dataFormat.first)) {
first = dataFormat.first;
}
if (dataFormat.second != -1 && (guessDataFormat.first.equals(dataFormat.second)
|| guessDataFormat.second.equals(dataFormat.second))) {
second = dataFormat.second;
}
if (first == -1 && second == -1) {
return new Pair<>(IFD_FORMAT_STRING, -1);
}
if (first == -1) {
dataFormat = new Pair<>(second, -1);
continue;
}
if (second == -1) {
dataFormat = new Pair<>(first, -1);
}
}
return dataFormat;
}
if (entryValue.contains("/")) {
String[] rationalNumber = entryValue.split("/", -1);
if (rationalNumber.length == 2) {
try {
long numerator = (long) Double.parseDouble(rationalNumber[0]);
long denominator = (long) Double.parseDouble(rationalNumber[1]);
if (numerator < 0L || denominator < 0L) {
return new Pair<>(IFD_FORMAT_SRATIONAL, -1);
}
if (numerator > Integer.MAX_VALUE || denominator > Integer.MAX_VALUE) {
return new Pair<>(IFD_FORMAT_URATIONAL, -1);
}
return new Pair<>(IFD_FORMAT_SRATIONAL, IFD_FORMAT_URATIONAL);
} catch (NumberFormatException e) {
// Ignored
}
}
return new Pair<>(IFD_FORMAT_STRING, -1);
}
try {
long longValue = Long.parseLong(entryValue);
if (longValue >= 0 && longValue <= 65535) {
return new Pair<>(IFD_FORMAT_USHORT, IFD_FORMAT_ULONG);
}
if (longValue < 0) {
return new Pair<>(IFD_FORMAT_SLONG, -1);
}
return new Pair<>(IFD_FORMAT_ULONG, -1);
} catch (NumberFormatException e) {
// Ignored
}
try {
Double.parseDouble(entryValue);
return new Pair<>(IFD_FORMAT_DOUBLE, -1);
} catch (NumberFormatException e) {
// Ignored
}
return new Pair<>(IFD_FORMAT_STRING, -1);
}
}
}