Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ffe4efc3 authored by Jin Park's avatar Jin Park Committed by Jin Seok Park
Browse files

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
parent bc0f0f43
Loading
Loading
Loading
Loading
+174 −26
Original line number Diff line number Diff line
@@ -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)) {
                switch (mMimeType) {
                    case IMAGE_TYPE_JPEG: {
                        getJpegAttributes(in, IFD_TIFF_HINT);
                        break;
                    }
                    case IMAGE_TYPE_RAF: {
                        getRafAttributes(in);
                        break;
                    }
                    default: {
                        getRawAttributes(in);
                    return;
                        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);
            }
        } 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);