/*
* 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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.camera.core.Logger;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* A class for indicating EXIF attribute.
*
* This class was pulled from the {@link androidx.exifinterface.media.ExifInterface} class.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
final class ExifAttribute {
private static final String TAG = "ExifAttribute";
public static final long BYTES_OFFSET_UNKNOWN = -1;
// See JPEG File Interchange Format Version 1.02.
// The following values are defined for handling JPEG streams. In this implementation, we are
// not only getting information from EXIF but also from some JPEG special segments such as
// MARKER_COM for user comment and MARKER_SOFx for image width and height.
@SuppressWarnings("WeakerAccess") /* synthetic access */
static final Charset ASCII = StandardCharsets.US_ASCII;
// Formats for the value in IFD entry (See TIFF 6.0 Section 2, "Image File Directory".)
static final int IFD_FORMAT_BYTE = 1;
static final int IFD_FORMAT_STRING = 2;
static final int IFD_FORMAT_USHORT = 3;
static final int IFD_FORMAT_ULONG = 4;
static final int IFD_FORMAT_URATIONAL = 5;
static final int IFD_FORMAT_SBYTE = 6;
static final int IFD_FORMAT_UNDEFINED = 7;
static final int IFD_FORMAT_SSHORT = 8;
static final int IFD_FORMAT_SLONG = 9;
static final int IFD_FORMAT_SRATIONAL = 10;
static final int IFD_FORMAT_SINGLE = 11;
static final int IFD_FORMAT_DOUBLE = 12;
// 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"
};
// Sizes of the components of each IFD value format
static final int[] IFD_FORMAT_BYTES_PER_FORMAT = new int[] {
0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 1
};
@SuppressWarnings("WeakerAccess") /* synthetic access */
static final byte[] EXIF_ASCII_PREFIX = new byte[] {
0x41, 0x53, 0x43, 0x49, 0x49, 0x0, 0x0, 0x0
};
public final int format;
public final int numberOfComponents;
public final long bytesOffset;
public final byte[] bytes;
@SuppressWarnings("WeakerAccess") /* synthetic access */
ExifAttribute(int format, int numberOfComponents, byte[] bytes) {
this(format, numberOfComponents, BYTES_OFFSET_UNKNOWN, bytes);
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
ExifAttribute(int format, int numberOfComponents, long bytesOffset, byte[] bytes) {
this.format = format;
this.numberOfComponents = numberOfComponents;
this.bytesOffset = bytesOffset;
this.bytes = bytes;
}
@NonNull
public static ExifAttribute createUShort(@NonNull int[] values, @NonNull ByteOrder byteOrder) {
final ByteBuffer buffer = ByteBuffer.wrap(
new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_USHORT] * values.length]);
buffer.order(byteOrder);
for (int value : values) {
buffer.putShort((short) value);
}
return new ExifAttribute(IFD_FORMAT_USHORT, values.length, buffer.array());
}
@NonNull
public static ExifAttribute createUShort(int value, @NonNull ByteOrder byteOrder) {
return createUShort(new int[] {value}, byteOrder);
}
@NonNull
public static ExifAttribute createULong(@NonNull long[] values, @NonNull ByteOrder byteOrder) {
final ByteBuffer buffer = ByteBuffer.wrap(
new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_ULONG] * values.length]);
buffer.order(byteOrder);
for (long value : values) {
buffer.putInt((int) value);
}
return new ExifAttribute(IFD_FORMAT_ULONG, values.length, buffer.array());
}
@NonNull
public static ExifAttribute createULong(long value, @NonNull ByteOrder byteOrder) {
return createULong(new long[] {value}, byteOrder);
}
@NonNull
public static ExifAttribute createSLong(@NonNull int[] values, @NonNull ByteOrder byteOrder) {
final ByteBuffer buffer = ByteBuffer.wrap(
new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_SLONG] * values.length]);
buffer.order(byteOrder);
for (int value : values) {
buffer.putInt(value);
}
return new ExifAttribute(IFD_FORMAT_SLONG, values.length, buffer.array());
}
@NonNull
public static ExifAttribute createSLong(int value, @NonNull ByteOrder byteOrder) {
return createSLong(new int[] {value}, byteOrder);
}
@NonNull
public static ExifAttribute createByte(@NonNull String value) {
// Exception for GPSAltitudeRef tag
if (value.length() == 1 && value.charAt(0) >= '0' && value.charAt(0) <= '1') {
final byte[] bytes = new byte[] { (byte) (value.charAt(0) - '0') };
return new ExifAttribute(IFD_FORMAT_BYTE, bytes.length, bytes);
}
final byte[] ascii = value.getBytes(ASCII);
return new ExifAttribute(IFD_FORMAT_BYTE, ascii.length, ascii);
}
@NonNull
public static ExifAttribute createString(@NonNull String value) {
final byte[] ascii = (value + 'eb3b9ed0-f11d-0137-cfb9-0ebaa35b92c0').getBytes(ASCII);
return new ExifAttribute(IFD_FORMAT_STRING, ascii.length, ascii);
}
@NonNull
public static ExifAttribute createURational(@NonNull LongRational[] values,
@NonNull ByteOrder byteOrder) {
final ByteBuffer buffer = ByteBuffer.wrap(
new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_URATIONAL] * values.length]);
buffer.order(byteOrder);
for (LongRational value : values) {
buffer.putInt((int) value.getNumerator());
buffer.putInt((int) value.getDenominator());
}
return new ExifAttribute(IFD_FORMAT_URATIONAL, values.length, buffer.array());
}
@NonNull
public static ExifAttribute createURational(@NonNull LongRational value,
@NonNull ByteOrder byteOrder) {
return createURational(new LongRational[] {value}, byteOrder);
}
@NonNull
public static ExifAttribute createSRational(@NonNull LongRational[] values,
@NonNull ByteOrder byteOrder) {
final ByteBuffer buffer = ByteBuffer.wrap(
new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_SRATIONAL] * values.length]);
buffer.order(byteOrder);
for (LongRational value : values) {
buffer.putInt((int) value.getNumerator());
buffer.putInt((int) value.getDenominator());
}
return new ExifAttribute(IFD_FORMAT_SRATIONAL, values.length, buffer.array());
}
@NonNull
public static ExifAttribute createSRational(@NonNull LongRational value,
@NonNull ByteOrder byteOrder) {
return createSRational(new LongRational[] {value}, byteOrder);
}
@NonNull
public static ExifAttribute createDouble(@NonNull double[] values,
@NonNull ByteOrder byteOrder) {
final ByteBuffer buffer = ByteBuffer.wrap(
new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_DOUBLE] * values.length]);
buffer.order(byteOrder);
for (double value : values) {
buffer.putDouble(value);
}
return new ExifAttribute(IFD_FORMAT_DOUBLE, values.length, buffer.array());
}
@NonNull
public static ExifAttribute createDouble(double value, @NonNull ByteOrder byteOrder) {
return createDouble(new double[] {value}, byteOrder);
}
@Override
public String toString() {
return "(" + IFD_FORMAT_NAMES[format] + ", data length:" + bytes.length + ")";
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
Object getValue(ByteOrder byteOrder) {
ByteOrderedDataInputStream inputStream = null;
try {
inputStream = new ByteOrderedDataInputStream(bytes);
inputStream.setByteOrder(byteOrder);
switch (format) {
case IFD_FORMAT_BYTE:
case IFD_FORMAT_SBYTE: {
// Exception for GPSAltitudeRef tag
if (bytes.length == 1 && bytes[0] >= 0 && bytes[0] <= 1) {
return new String(new char[] { (char) (bytes[0] + '0') });
}
return new String(bytes, ASCII);
}
case IFD_FORMAT_UNDEFINED:
case IFD_FORMAT_STRING: {
int index = 0;
if (numberOfComponents >= EXIF_ASCII_PREFIX.length) {
boolean same = true;
for (int i = 0; i < EXIF_ASCII_PREFIX.length; ++i) {
if (bytes[i] != EXIF_ASCII_PREFIX[i]) {
same = false;
break;
}
}
if (same) {
index = EXIF_ASCII_PREFIX.length;
}
}
StringBuilder stringBuilder = new StringBuilder();
while (index < numberOfComponents) {
int ch = bytes[index];
if (ch == 0) {
break;
}
if (ch >= 32) {
stringBuilder.append((char) ch);
} else {
stringBuilder.append('?');
}
++index;
}
return stringBuilder.toString();
}
case IFD_FORMAT_USHORT: {
final int[] values = new int[numberOfComponents];
for (int i = 0; i < numberOfComponents; ++i) {
values[i] = inputStream.readUnsignedShort();
}
return values;
}
case IFD_FORMAT_ULONG: {
final long[] values = new long[numberOfComponents];
for (int i = 0; i < numberOfComponents; ++i) {
values[i] = inputStream.readUnsignedInt();
}
return values;
}
case IFD_FORMAT_URATIONAL: {
final LongRational[] values = new LongRational[numberOfComponents];
for (int i = 0; i < numberOfComponents; ++i) {
final long numerator = inputStream.readUnsignedInt();
final long denominator = inputStream.readUnsignedInt();
values[i] = new LongRational(numerator, denominator);
}
return values;
}
case IFD_FORMAT_SSHORT: {
final int[] values = new int[numberOfComponents];
for (int i = 0; i < numberOfComponents; ++i) {
values[i] = inputStream.readShort();
}
return values;
}
case IFD_FORMAT_SLONG: {
final int[] values = new int[numberOfComponents];
for (int i = 0; i < numberOfComponents; ++i) {
values[i] = inputStream.readInt();
}
return values;
}
case IFD_FORMAT_SRATIONAL: {
final LongRational[] values = new LongRational[numberOfComponents];
for (int i = 0; i < numberOfComponents; ++i) {
final long numerator = inputStream.readInt();
final long denominator = inputStream.readInt();
values[i] = new LongRational(numerator, denominator);
}
return values;
}
case IFD_FORMAT_SINGLE: {
final double[] values = new double[numberOfComponents];
for (int i = 0; i < numberOfComponents; ++i) {
values[i] = inputStream.readFloat();
}
return values;
}
case IFD_FORMAT_DOUBLE: {
final double[] values = new double[numberOfComponents];
for (int i = 0; i < numberOfComponents; ++i) {
values[i] = inputStream.readDouble();
}
return values;
}
default:
return null;
}
} catch (IOException e) {
Logger.w(TAG, "IOException occurred during reading a value", e);
return null;
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
Logger.e(TAG, "IOException occurred while closing InputStream", e);
}
}
}
}
public double getDoubleValue(@NonNull ByteOrder byteOrder) {
Object value = getValue(byteOrder);
if (value == null) {
throw new NumberFormatException("NULL can't be converted to a double value");
}
if (value instanceof String) {
return Double.parseDouble((String) value);
}
if (value instanceof long[]) {
long[] array = (long[]) value;
if (array.length == 1) {
return array[0];
}
throw new NumberFormatException("There are more than one component");
}
if (value instanceof int[]) {
int[] array = (int[]) value;
if (array.length == 1) {
return array[0];
}
throw new NumberFormatException("There are more than one component");
}
if (value instanceof double[]) {
double[] array = (double[]) value;
if (array.length == 1) {
return array[0];
}
throw new NumberFormatException("There are more than one component");
}
if (value instanceof LongRational[]) {
LongRational[] array = (LongRational[]) value;
if (array.length == 1) {
return array[0].toDouble();
}
throw new NumberFormatException("There are more than one component");
}
throw new NumberFormatException("Couldn't find a double value");
}
public int getIntValue(@NonNull ByteOrder byteOrder) {
Object value = getValue(byteOrder);
if (value == null) {
throw new NumberFormatException("NULL can't be converted to a integer value");
}
if (value instanceof String) {
return Integer.parseInt((String) value);
}
if (value instanceof long[]) {
long[] array = (long[]) value;
if (array.length == 1) {
return (int) array[0];
}
throw new NumberFormatException("There are more than one component");
}
if (value instanceof int[]) {
int[] array = (int[]) value;
if (array.length == 1) {
return array[0];
}
throw new NumberFormatException("There are more than one component");
}
throw new NumberFormatException("Couldn't find a integer value");
}
@Nullable
public String getStringValue(@NonNull ByteOrder byteOrder) {
Object value = getValue(byteOrder);
if (value == null) {
return null;
}
if (value instanceof String) {
return (String) value;
}
final StringBuilder stringBuilder = new StringBuilder();
if (value instanceof long[]) {
long[] array = (long[]) value;
for (int i = 0; i < array.length; ++i) {
stringBuilder.append(array[i]);
if (i + 1 != array.length) {
stringBuilder.append(",");
}
}
return stringBuilder.toString();
}
if (value instanceof int[]) {
int[] array = (int[]) value;
for (int i = 0; i < array.length; ++i) {
stringBuilder.append(array[i]);
if (i + 1 != array.length) {
stringBuilder.append(",");
}
}
return stringBuilder.toString();
}
if (value instanceof double[]) {
double[] array = (double[]) value;
for (int i = 0; i < array.length; ++i) {
stringBuilder.append(array[i]);
if (i + 1 != array.length) {
stringBuilder.append(",");
}
}
return stringBuilder.toString();
}
if (value instanceof LongRational[]) {
LongRational[] array = (LongRational[]) value;
for (int i = 0; i < array.length; ++i) {
stringBuilder.append(array[i].getNumerator());
stringBuilder.append('/');
stringBuilder.append(array[i].getDenominator());
if (i + 1 != array.length) {
stringBuilder.append(",");
}
}
return stringBuilder.toString();
}
return null;
}
public int size() {
return IFD_FORMAT_BYTES_PER_FORMAT[format] * numberOfComponents;
}
}