/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.imageutils;
import android.media.ExifInterface;
import java.io.IOException;
import java.io.InputStream;
import com.facebook.common.logging.FLog;
/**
* Util for getting exif orientation from a jpeg stored as a byte array.
*/
class TiffUtil {
private static final Class<?> TAG = TiffUtil.class;
public static final int TIFF_BYTE_ORDER_BIG_END = 0x4D4D002A;
public static final int TIFF_BYTE_ORDER_LITTLE_END = 0x49492A00;
public static final int TIFF_TAG_ORIENTATION = 0x0112;
public static final int TIFF_TYPE_SHORT = 3;
/**
* Determines auto-rotate angle based on orientation information.
* @param orientation orientation information read from APP1 EXIF (TIFF) block.
* @return orientation: 1/3/6/8 -> 0/180/90/270.
*/
public static int getAutoRotateAngleFromOrientation(int orientation) {
switch (orientation) {
case ExifInterface.ORIENTATION_NORMAL:
case ExifInterface.ORIENTATION_UNDEFINED:
return 0;
case ExifInterface.ORIENTATION_ROTATE_180:
return 180;
case ExifInterface.ORIENTATION_ROTATE_90:
return 90;
case ExifInterface.ORIENTATION_ROTATE_270:
return 270;
}
FLog.i(TAG, "Unsupported orientation");
return 0;
}
/**
* Reads orientation information from TIFF data.
* @param is the input stream of TIFF data
* @param length length of the TIFF data
* @return orientation information (1/3/6/8 on success, 0 if not found)
*/
public static int readOrientationFromTIFF(InputStream is, int length) throws IOException {
// read tiff header
TiffHeader tiffHeader = new TiffHeader();
length = readTiffHeader(is, length, tiffHeader);
// move to the first IFD
// offset is relative to the beginning of the TIFF data
// and we already consumed the first 8 bytes of header
int toSkip = tiffHeader.firstIfdOffset - 8;
if (length == 0 || toSkip > length) {
return 0;
}
is.skip(toSkip);
length -= toSkip;
// move to the entry with orientation tag
length = moveToTiffEntryWithTag(is, length, tiffHeader.isLittleEndian, TIFF_TAG_ORIENTATION);
// read orientation
return getOrientationFromTiffEntry(is, length, tiffHeader.isLittleEndian);
}
/**
* Structure that holds TIFF header.
*/
private static class TiffHeader {
boolean isLittleEndian;
int byteOrder;
int firstIfdOffset;
}
/**
* Reads the TIFF header to the provided structure.
* @param is the input stream of TIFF data
* @param length length of the TIFF data
* @return remaining length of the data on success, 0 on failure
* @throws IOException
*/
private static int readTiffHeader(InputStream is, int length, TiffHeader tiffHeader)
throws IOException {
if (length <= 8) {
return 0;
}
// read the byte order
tiffHeader.byteOrder = StreamProcessor.readPackedInt(is, 4, false);
length -= 4;
if (tiffHeader.byteOrder != TIFF_BYTE_ORDER_LITTLE_END &&
tiffHeader.byteOrder != TIFF_BYTE_ORDER_BIG_END) {
FLog.e(TAG, "Invalid TIFF header");
return 0;
}
tiffHeader.isLittleEndian = (tiffHeader.byteOrder == TIFF_BYTE_ORDER_LITTLE_END);
// read the offset of the first IFD and check if it is reasonable
tiffHeader.firstIfdOffset = StreamProcessor.readPackedInt(is, 4, tiffHeader.isLittleEndian);
length -= 4;
if (tiffHeader.firstIfdOffset < 8 || tiffHeader.firstIfdOffset - 8 > length) {
FLog.e(TAG, "Invalid offset");
return 0;
}
return length;
}
/**
* Positions the given input stream to the entry that has a specified tag. Tag will be consumed.
* @param is the input stream of TIFF data positioned to the beginning of an IFD.
* @param length length of the available data in the given input stream.
* @param isLittleEndian whether the TIFF data is stored in little or big endian format
* @param tagToFind tag to find
* @return remaining length of the data on success, 0 on failure
*/
private static int moveToTiffEntryWithTag(
InputStream is,
int length,
boolean isLittleEndian,
int tagToFind)
throws IOException {
if (length < 14) {
return 0;
}
// read the number of entries and go through all of them
// each IFD entry has length of 12 bytes and is composed of
// {TAG [2], TYPE [2], COUNT [4], VALUE/OFFSET [4]}
int numEntries = StreamProcessor.readPackedInt(is, 2, isLittleEndian);
length -= 2;
while (numEntries-- > 0 && length >= 12) {
int tag = StreamProcessor.readPackedInt(is, 2, isLittleEndian);
length -= 2;
if (tag == tagToFind) {
return length;
}
is.skip(10);
length -= 10;
}
return 0;
}
/**
* Reads the orientation information from the TIFF entry.
* It is assumed that the entry has a TIFF orientation tag and that tag has already been consumed.
* @param is the input stream positioned at the TIFF entry with tag already being consumed
* @param isLittleEndian whether the TIFF data is stored in little or big endian format
* @return Orientation value in TIFF IFD entry.
*/
private static int getOrientationFromTiffEntry(InputStream is, int length, boolean isLittleEndian)
throws IOException {
if (length < 10) {
return 0;
}
// orientation entry has type = short
int type = StreamProcessor.readPackedInt(is, 2, isLittleEndian);
if (type != TIFF_TYPE_SHORT) {
return 0;
}
// orientation entry has count = 1
int count = StreamProcessor.readPackedInt(is, 4, isLittleEndian);
if (count != 1) {
return 0;
}
int value = StreamProcessor.readPackedInt(is, 2, isLittleEndian);
int padding = StreamProcessor.readPackedInt(is, 2, isLittleEndian);
return value;
}
}