ExifInterface: Add RAF file parse support

A RAF format file has a unique way of storing its data. This CL adds
code that checks whether a file is a RAF file format and parses the
data according to specifications.

Bug: 29409358
Change-Id: If37d4ba8de47cdbacd524a07148ba6c14f873259
This commit is contained in:
Jin Park
2016-07-05 09:43:37 +09:00
committed by Jin Seok Park
parent bc0f0f4351
commit ffe4efc319

View File

@@ -42,6 +42,7 @@ import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Arrays;
@@ -384,8 +385,16 @@ public class ExifInterface {
public static final int WHITEBALANCE_AUTO = 0;
public static final int WHITEBALANCE_MANUAL = 1;
// Maximum size for checking file type signature (see image_type_recognition_lite.cc)
private static final int SIGNATURE_CHECK_SIZE = 5000;
private static final byte[] JPEG_SIGNATURE = new byte[] {(byte) 0xff, (byte) 0xd8, (byte) 0xff};
private static final int JPEG_SIGNATURE_SIZE = 3;
private static final String RAF_SIGNATURE = "FUJIFILMCCD-RAW";
private static final int RAF_SIGNATURE_SIZE = 15;
private static final int RAF_OFFSET_TO_JPEG_IMAGE_OFFSET = 84;
private static final int RAF_INFO_SIZE = 160;
private int mRafJpegOffset, mRafJpegLength, mRafCfaHeaderOffset, mRafCfaHeaderLength;
private static SimpleDateFormat sFormatter;
@@ -1034,6 +1043,9 @@ public class ExifInterface {
new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG)
};
// RAF file tag (See piex.cc line 372)
private static final ExifTag TAG_RAF_IMAGE_SIZE =
new ExifTag(TAG_STRIP_OFFSETS, 273, IFD_FORMAT_USHORT);
// See JEITA CP-3451C Section 4.6.3: Exif-specific IFD.
// The following values are used for indicating pointers to the other Image File Directories.
@@ -1106,6 +1118,20 @@ public class ExifInterface {
private static final byte MARKER_COM = (byte) 0xfe;
private static final byte MARKER_EOI = (byte) 0xd9;
// Supported Image File Types
private static final int IMAGE_TYPE_UNKNOWN = 0;
private static final int IMAGE_TYPE_ARW = 1;
private static final int IMAGE_TYPE_CR2 = 2;
private static final int IMAGE_TYPE_DNG = 3;
private static final int IMAGE_TYPE_JPEG = 4;
private static final int IMAGE_TYPE_NEF = 5;
private static final int IMAGE_TYPE_NRW = 6;
private static final int IMAGE_TYPE_ORF = 7;
private static final int IMAGE_TYPE_PEF = 8;
private static final int IMAGE_TYPE_RAF = 9;
private static final int IMAGE_TYPE_RW2 = 10;
private static final int IMAGE_TYPE_SRW = 11;
static {
System.loadLibrary("media_jni");
nativeInitRaw();
@@ -1128,7 +1154,7 @@ public class ExifInterface {
private final AssetManager.AssetInputStream mAssetInputStream;
private final boolean mIsInputStream;
private boolean mIsRaw;
private int mRawType;
private int mMimeType;
private final HashMap[] mAttributes = new HashMap[EXIF_TAGS.length];
private ByteOrder mExifByteOrder = ByteOrder.BIG_ENDIAN;
private boolean mHasThumbnail;
@@ -1504,13 +1530,24 @@ public class ExifInterface {
mAttributes[i] = new HashMap();
}
// Process RAW input stream
if (HANDLE_RAW) {
in = new BufferedInputStream(in, JPEG_SIGNATURE_SIZE);
// Check file type
in = new BufferedInputStream(in, SIGNATURE_CHECK_SIZE);
mMimeType = getMimeType((BufferedInputStream) in);
if (!isJpegInputStream((BufferedInputStream) in)) {
getRawAttributes(in);
return;
switch (mMimeType) {
case IMAGE_TYPE_JPEG: {
getJpegAttributes(in, IFD_TIFF_HINT);
break;
}
case IMAGE_TYPE_RAF: {
getRafAttributes(in);
break;
}
default: {
getRawAttributes(in);
break;
}
}
} else {
if (mAssetInputStream != null) {
@@ -1524,16 +1561,20 @@ public class ExifInterface {
return;
}
} else {
in = new BufferedInputStream(in, JPEG_SIGNATURE_SIZE);
if (!isJpegInputStream((BufferedInputStream) in) && handleRawResult(
in.mark(JPEG_SIGNATURE_SIZE);
byte[] signatureBytes = new byte[JPEG_SIGNATURE_SIZE];
if (in.read(signatureBytes) != JPEG_SIGNATURE_SIZE) {
throw new EOFException();
}
in.reset();
if (!isJpegInputStream(signatureBytes) && handleRawResult(
nativeGetRawAttributesFromInputStream(in))) {
return;
}
}
// Process JPEG input stream
getJpegAttributes(in, IFD_TIFF_HINT);
}
// Process JPEG input stream
getJpegAttributes(in, IFD_TIFF_HINT);
} catch (IOException e) {
// Ignore exceptions in order to keep the compatibility with the old versions of
// ExifInterface.
@@ -1549,15 +1590,13 @@ public class ExifInterface {
}
}
private static boolean isJpegInputStream(BufferedInputStream in) throws IOException {
in.mark(JPEG_SIGNATURE_SIZE);
byte[] signatureBytes = new byte[JPEG_SIGNATURE_SIZE];
if (in.read(signatureBytes) != JPEG_SIGNATURE_SIZE) {
throw new EOFException();
private static boolean isJpegInputStream(byte[] signatureBytes) throws IOException {
for (int i = 0; i < JPEG_SIGNATURE_SIZE; i++) {
if (signatureBytes[i] != JPEG_SIGNATURE[i]) {
return false;
}
}
boolean isJpeg = Arrays.equals(JPEG_SIGNATURE, signatureBytes);
in.reset();
return isJpeg;
return true;
}
private boolean handleRawResult(HashMap map) {
@@ -1879,6 +1918,22 @@ public class ExifInterface {
}
}
// Checks the type of image file
private int getMimeType(BufferedInputStream in) throws IOException {
in.mark(SIGNATURE_CHECK_SIZE);
byte[] signatureBytes = new byte[SIGNATURE_CHECK_SIZE];
if (in.read(signatureBytes) != SIGNATURE_CHECK_SIZE) {
throw new EOFException();
}
in.reset();
if (isJpegInputStream(signatureBytes)) {
return IMAGE_TYPE_JPEG;
} else if (isRafInputStream(signatureBytes)) {
return IMAGE_TYPE_RAF;
}
return IMAGE_TYPE_UNKNOWN;
}
/**
* Loads EXIF attributes from a JPEG input stream.
*
@@ -1959,7 +2014,7 @@ public class ExifInterface {
if (dataInputStream.read(bytes) != length) {
throw new IOException("Invalid exif");
}
readExifSegment(bytes, bytesRead);
readExifSegment(bytes, bytesRead, imageType);
bytesRead += length;
length = 0;
break;
@@ -2059,10 +2114,99 @@ public class ExifInterface {
mAttributes[IFD_PREVIEW_HINT] = new HashMap();
}
}
// Process thumbnail.
processThumbnail(dataInputStream, bytesRead, exifBytes.length);
processThumbnail(dataInputStream, 0, exifBytes.length);
}
/**
* RAF files contains a JPEG and a CFA data.
* The JPEG contains two images, a preview and a thumbnail, while the CFA contains a RAW image.
* This method looks at the first 160 bytes to determine if this file is a RAF file.
* There is no official specification for RAF files from Fuji, but there is an online archive of
* image file specifications:
* http://fileformats.archiveteam.org/wiki/Fujifilm_RAF
*/
private boolean isRafInputStream(byte[] signatureBytes) throws IOException {
byte[] rafSignatureBytes = RAF_SIGNATURE.getBytes();
for (int i = 0; i < RAF_SIGNATURE_SIZE; i++) {
if (signatureBytes[i] != rafSignatureBytes[i]) {
return false;
}
}
return true;
}
/**
* This method looks at the first 160 bytes of a RAF file to retrieve the offset and length
* values for the JPEG and CFA data.
* Using that data, it parses the JPEG data to retrieve the preview and thumbnail image data,
* then parses the CFA metadata to retrieve the primary image length/width values.
* For data format details, see http://fileformats.archiveteam.org/wiki/Fujifilm_RAF
*/
private void getRafAttributes(InputStream in) throws IOException {
// Retrieve offset & length values
in.mark(RAF_INFO_SIZE);
in.skip(RAF_OFFSET_TO_JPEG_IMAGE_OFFSET);
byte[] jpegOffsetBytes = new byte[4];
byte[] jpegLengthBytes = new byte[4];
byte[] cfaHeaderOffsetBytes = new byte[4];
byte[] cfaHeaderLengthBytes = new byte[4];
in.read(jpegOffsetBytes);
in.read(jpegLengthBytes);
in.read(cfaHeaderOffsetBytes);
in.read(cfaHeaderLengthBytes);
mRafJpegOffset = ByteBuffer.wrap(jpegOffsetBytes).getInt();
mRafJpegLength = ByteBuffer.wrap(jpegLengthBytes).getInt();
mRafCfaHeaderOffset = ByteBuffer.wrap(cfaHeaderOffsetBytes).getInt();
mRafCfaHeaderLength = ByteBuffer.wrap(cfaHeaderLengthBytes).getInt();
in.reset();
// Retrieve JPEG image metadata
in.mark(mRafJpegOffset + mRafJpegLength);
in.skip(mRafJpegOffset);
getJpegAttributes(in, IFD_PREVIEW_HINT);
in.reset();
// Skip to CFA header offset.
// A while loop is used because the skip method may not be able to skip the requested amount
// at once because the size of the buffer may be restricted.
int totalSkip = mRafCfaHeaderOffset;
while (totalSkip > 0) {
long skipped = in.skip(totalSkip);
totalSkip -= skipped;
}
// Retrieve primary image length/width values, if TAG_RAF_IMAGE_SIZE exists
byte[] exifBytes = new byte[mRafCfaHeaderLength];
int read = in.read(exifBytes);
ByteOrderAwarenessDataInputStream dataInputStream =
new ByteOrderAwarenessDataInputStream(exifBytes);
int numberOfDirectoryEntry = dataInputStream.readInt();
if (DEBUG) {
Log.d(TAG, "numberOfDirectoryEntry: " + numberOfDirectoryEntry);
}
// CFA stores some metadata about the RAW image. Since CFA uses proprietary tags, can only
// find and retrieve image size information tags, while skipping others.
// See piex.cc RafGetDimension()
for (int i = 0; i < numberOfDirectoryEntry; ++i) {
int tagNumber = dataInputStream.readUnsignedShort();
int numberOfBytes = dataInputStream.readUnsignedShort();
if (tagNumber == TAG_RAF_IMAGE_SIZE.number) {
int imageLength = dataInputStream.readShort();
int imageWidth = dataInputStream.readShort();
ExifAttribute imageLengthAttribute =
ExifAttribute.createUShort(imageLength, mExifByteOrder);
ExifAttribute imageWidthAttribute =
ExifAttribute.createUShort(imageWidth, mExifByteOrder);
mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_LENGTH, imageLengthAttribute);
mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_WIDTH, imageWidthAttribute);
if (DEBUG) {
Log.d(TAG, "Updated to length: " + imageLength + ", width: " + imageWidth);
}
return;
}
dataInputStream.skip(numberOfBytes);
}
}
// Stores a new JPEG image with EXIF attributes into a given output stream.
@@ -2164,7 +2308,8 @@ public class ExifInterface {
}
// Reads the given EXIF byte area and save its tag data into attributes.
private void readExifSegment(byte[] exifBytes, int exifOffsetFromBeginning) throws IOException {
private void readExifSegment(byte[] exifBytes, int exifOffsetFromBeginning, int imageType)
throws IOException {
ByteOrderAwarenessDataInputStream dataInputStream =
new ByteOrderAwarenessDataInputStream(exifBytes);
@@ -2172,7 +2317,7 @@ public class ExifInterface {
parseTiffHeaders(dataInputStream, exifBytes.length);
// Read TIFF image file directories. See JEITA CP-3451C Section 4.5.2. Figure 6.
readImageFileDirectory(dataInputStream, IFD_TIFF_HINT);
readImageFileDirectory(dataInputStream, imageType);
// Process thumbnail.
processThumbnail(dataInputStream, exifOffsetFromBeginning, exifBytes.length);
@@ -2482,18 +2627,20 @@ public class ExifInterface {
}
// The following code limits the size of thumbnail size not to overflow EXIF data area.
thumbnailLength = Math.min(thumbnailLength, exifBytesLength - thumbnailOffset);
// The following code changes the offset value for RAF files.
if (mMimeType == IMAGE_TYPE_RAF) {
exifOffsetFromBeginning += mRafJpegOffset;
}
if (thumbnailOffset > 0 && thumbnailLength > 0) {
mHasThumbnail = true;
mThumbnailOffset = exifOffsetFromBeginning + thumbnailOffset;
mThumbnailLength = thumbnailLength;
if (mFilename == null && mAssetInputStream == null && mSeekableFileDescriptor == null) {
// Save the thumbnail in memory if the input doesn't support reading again.
byte[] thumbnailBytes = new byte[thumbnailLength];
dataInputStream.seek(thumbnailOffset);
dataInputStream.readFully(thumbnailBytes);
mThumbnailBytes = thumbnailBytes;
if (DEBUG) {
Bitmap bitmap = BitmapFactory.decodeByteArray(
thumbnailBytes, 0, thumbnailBytes.length);
@@ -2511,6 +2658,7 @@ public class ExifInterface {
private boolean isThumbnail(HashMap map) throws IOException {
ExifAttribute imageLengthAttribute = (ExifAttribute) map.get(TAG_IMAGE_LENGTH);
ExifAttribute imageWidthAttribute = (ExifAttribute) map.get(TAG_IMAGE_WIDTH);
if (imageLengthAttribute != null && imageWidthAttribute != null) {
int imageLengthValue = imageLengthAttribute.getIntValue(mExifByteOrder);
int imageWidthValue = imageWidthAttribute.getIntValue(mExifByteOrder);