diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java index 59597af55085b..bbeaa4b9a1f90 100644 --- a/media/java/android/media/ExifInterface.java +++ b/media/java/android/media/ExifInterface.java @@ -442,6 +442,10 @@ public class ExifInterface { private static final int RAF_INFO_SIZE = 160; private static final int RAF_JPEG_LENGTH_VALUE_SIZE = 4; + private static final byte[] HEIF_TYPE_FTYP = new byte[] {'f', 't', 'y', 'p'}; + private static final byte[] HEIF_BRAND_MIF1 = new byte[] {'m', 'i', 'f', '1'}; + private static final byte[] HEIF_BRAND_HEIC = new byte[] {'h', 'e', 'i', 'c'}; + // See http://fileformats.archiveteam.org/wiki/Olympus_ORF private static final short ORF_SIGNATURE_1 = 0x4f52; private static final short ORF_SIGNATURE_2 = 0x5352; @@ -1264,6 +1268,7 @@ public class ExifInterface { private static final int IMAGE_TYPE_RAF = 9; private static final int IMAGE_TYPE_RW2 = 10; private static final int IMAGE_TYPE_SRW = 11; + private static final int IMAGE_TYPE_HEIF = 12; static { sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss"); @@ -1691,6 +1696,10 @@ public class ExifInterface { getRafAttributes(inputStream); break; } + case IMAGE_TYPE_HEIF: { + getHeifAttributes(inputStream); + break; + } case IMAGE_TYPE_ORF: { getOrfAttributes(inputStream); break; @@ -2107,6 +2116,8 @@ public class ExifInterface { return IMAGE_TYPE_JPEG; } else if (isRafFormat(signatureCheckBytes)) { return IMAGE_TYPE_RAF; + } else if (isHeifFormat(signatureCheckBytes)) { + return IMAGE_TYPE_HEIF; } else if (isOrfFormat(signatureCheckBytes)) { return IMAGE_TYPE_ORF; } else if (isRw2Format(signatureCheckBytes)) { @@ -2145,6 +2156,78 @@ public class ExifInterface { return true; } + private boolean isHeifFormat(byte[] signatureCheckBytes) throws IOException { + ByteOrderedDataInputStream signatureInputStream = null; + try { + signatureInputStream = new ByteOrderedDataInputStream(signatureCheckBytes); + signatureInputStream.setByteOrder(ByteOrder.BIG_ENDIAN); + + long chunkSize = signatureInputStream.readInt(); + byte[] chunkType = new byte[4]; + signatureInputStream.read(chunkType); + + if (!Arrays.equals(chunkType, HEIF_TYPE_FTYP)) { + return false; + } + + long chunkDataOffset = 8; + if (chunkSize == 1) { + // This indicates that the next 8 bytes represent the chunk size, + // and chunk data comes after that. + chunkSize = signatureInputStream.readLong(); + if (chunkSize < 16) { + // The smallest valid chunk is 16 bytes long in this case. + return false; + } + chunkDataOffset += 8; + } + + // only sniff up to signatureCheckBytes.length + if (chunkSize > signatureCheckBytes.length) { + chunkSize = signatureCheckBytes.length; + } + + long chunkDataSize = chunkSize - chunkDataOffset; + + // It should at least have major brand (4-byte) and minor version (4-byte). + // The rest of the chunk (if any) is a list of (4-byte) compatible brands. + if (chunkDataSize < 8) { + return false; + } + + byte[] brand = new byte[4]; + boolean isMif1 = false; + boolean isHeic = false; + for (long i = 0; i < chunkDataSize / 4; ++i) { + if (signatureInputStream.read(brand) != brand.length) { + return false; + } + if (i == 1) { + // Skip this index, it refers to the minorVersion, not a brand. + continue; + } + if (Arrays.equals(brand, HEIF_BRAND_MIF1)) { + isMif1 = true; + } else if (Arrays.equals(brand, HEIF_BRAND_HEIC)) { + isHeic = true; + } + if (isMif1 && isHeic) { + return true; + } + } + } catch (Exception e) { + if (DEBUG) { + Log.d(TAG, "Exception parsing HEIF file type box.", e); + } + } finally { + if (signatureInputStream != null) { + signatureInputStream.close(); + signatureInputStream = null; + } + } + return false; + } + /** * ORF has a similar structure to TIFF but it contains a different signature at the TIFF Header. * This method looks at the 2 bytes following the Byte Order bytes to determine if this file is @@ -2437,6 +2520,101 @@ public class ExifInterface { } } + private void getHeifAttributes(ByteOrderedDataInputStream in) throws IOException { + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + try { + if (mSeekableFileDescriptor != null) { + retriever.setDataSource(mSeekableFileDescriptor); + } else { + retriever.setDataSource(new MediaDataSource() { + long mPosition; + + @Override + public void close() throws IOException {} + + @Override + public int readAt(long position, byte[] buffer, int offset, int size) + throws IOException { + if (size == 0) { + return 0; + } + if (position < 0) { + return -1; + } + if (mPosition != position) { + in.seek(position); + mPosition = position; + } + + int bytesRead = in.read(buffer, offset, size); + if (bytesRead < 0) { + mPosition = -1; // need to seek on next read + return -1; + } + + mPosition += bytesRead; + return bytesRead; + } + + @Override + public long getSize() throws IOException { + return -1; + } + }); + } + + String hasVideo = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO); + + final String METADATA_HAS_VIDEO_VALUE_YES = "yes"; + if (METADATA_HAS_VIDEO_VALUE_YES.equals(hasVideo)) { + String width = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); + String height = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); + + if (width != null) { + mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, + ExifAttribute.createUShort(Integer.parseInt(width), mExifByteOrder)); + } + + if (height != null) { + mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, + ExifAttribute.createUShort(Integer.parseInt(height), mExifByteOrder)); + } + + // Note that the rotation angle from MediaMetadataRetriever for heif images + // are CCW, while rotation in ExifInterface orientations are CW. + String rotation = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); + if (rotation != null) { + int orientation = ExifInterface.ORIENTATION_NORMAL; + + switch (Integer.parseInt(rotation)) { + case 90: + orientation = ExifInterface.ORIENTATION_ROTATE_270; + break; + case 180: + orientation = ExifInterface.ORIENTATION_ROTATE_180; + break; + case 270: + orientation = ExifInterface.ORIENTATION_ROTATE_90; + break; + } + + mAttributes[IFD_TYPE_PRIMARY].put(TAG_ORIENTATION, + ExifAttribute.createUShort(orientation, mExifByteOrder)); + } + + if (DEBUG) { + Log.d(TAG, "Heif meta: " + width + "x" + height + ", rotation " + rotation); + } + } + } finally { + retriever.release(); + } + } + /** * ORF files contains a primary image data and a MakerNote data that contains preview/thumbnail * images. Both data takes the form of IFDs and can therefore be read with the @@ -2677,7 +2855,7 @@ public class ExifInterface { } if (getAttribute(TAG_ORIENTATION) == null) { mAttributes[IFD_TYPE_PRIMARY].put(TAG_ORIENTATION, - ExifAttribute.createULong(0, mExifByteOrder)); + ExifAttribute.createUShort(0, mExifByteOrder)); } if (getAttribute(TAG_LIGHT_SOURCE) == null) { mAttributes[IFD_TYPE_EXIF].put(TAG_LIGHT_SOURCE,