From ffe4efc3192d8831c8589aad6045466df7a41fd7 Mon Sep 17 00:00:00 2001 From: Jin Park Date: Tue, 5 Jul 2016 09:43:37 +0900 Subject: [PATCH] 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 --- media/java/android/media/ExifInterface.java | 200 ++++++++++++++++++++++++---- 1 file changed, 174 insertions(+), 26 deletions(-) diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java index a36758962b1b..2c98e98eb963 100644 --- a/media/java/android/media/ExifInterface.java +++ b/media/java/android/media/ExifInterface.java @@ -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); -- 2.11.0