Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix parsing EXIF data for HEIF/HEIC files. #664

Merged
merged 2 commits into from
Feb 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import coil.size.OriginalSize
import coil.size.PixelSize
import coil.size.Scale
import coil.size.Size
import coil.util.decodeBitmapAsset
import coil.util.isSimilarTo
import coil.util.size
import kotlinx.coroutines.runBlocking
Expand All @@ -30,13 +31,13 @@ class BitmapFactoryDecoderTest {

private lateinit var context: Context
private lateinit var pool: BitmapPool
private lateinit var service: BitmapFactoryDecoder
private lateinit var decoder: BitmapFactoryDecoder

@Before
fun before() {
context = ApplicationProvider.getApplicationContext()
pool = BitmapPool(Int.MAX_VALUE)
service = BitmapFactoryDecoder(context)
decoder = BitmapFactoryDecoder(context)
}

@Test
Expand Down Expand Up @@ -95,8 +96,20 @@ class BitmapFactoryDecoderTest {
@Test
fun largeExifMetadata() {
val size = PixelSize(500, 500)
val normal = decodeBitmap("exif/large_metadata_normalized.jpg", size)
val actual = decodeBitmap("exif/large_metadata_normalized.jpg", size)
val expected = decodeBitmap("exif/large_metadata_normalized.jpg", size)
val actual = decodeBitmap("exif/large_metadata.jpg", size)
assertTrue(expected.isSimilarTo(actual))
}

/** Regression test: https://github.com/coil-kt/coil/issues/619 */
@Test
fun heicExifMetadata() {
// HEIC files are not supported before API 30.
assumeTrue(SDK_INT >= 30)

// Ensure this completes and doesn't end up in an infinite loop.
val normal = context.decodeBitmapAsset("exif/basic.heic")
val actual = decodeBitmap("exif/basic.heic", OriginalSize)
assertTrue(normal.isSimilarTo(actual))
}

Expand Down Expand Up @@ -314,7 +327,7 @@ class BitmapFactoryDecoderTest {
options: Options = Options(context, scale = Scale.FILL)
): DecodeResult = runBlocking {
val source = context.assets.open(assetName).source().buffer()
val result = service.decode(
val result = decoder.decode(
pool = pool,
source = source,
size = size,
Expand Down
25 changes: 14 additions & 11 deletions coil-base/src/main/java/coil/decode/BitmapFactoryDecoder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ internal class BitmapFactoryDecoder(private val context: Context) : Decoder {
val rotationDegrees: Int
if (shouldReadExifData(outMimeType)) {
val exifInterface = ExifInterface(ExifInterfaceInputStream(safeBufferedSource.peek().inputStream()))
safeSource.exception?.let { throw it }
isFlipped = exifInterface.isFlipped
rotationDegrees = exifInterface.rotationDegrees
} else {
Expand Down Expand Up @@ -289,32 +290,34 @@ internal class BitmapFactoryDecoder(private val context: Context) : Decoder {
/** Wrap [delegate] so that it works with [ExifInterface]. */
private class ExifInterfaceInputStream(private val delegate: InputStream) : InputStream() {

override fun read() = delegate.read()
// Ensure that this value is always larger than the size of the image
// so ExifInterface won't stop reading the stream prematurely.
@Volatile private var availableBytes = GIGABYTE_IN_BYTES

override fun read(b: ByteArray) = delegate.read(b)
override fun read() = interceptBytesRead(delegate.read())

override fun read(b: ByteArray, off: Int, len: Int) = delegate.read(b, off, len)
override fun read(b: ByteArray) = interceptBytesRead(delegate.read(b))

override fun read(b: ByteArray, off: Int, len: Int) = interceptBytesRead(delegate.read(b, off, len))

override fun skip(n: Long) = delegate.skip(n)

// Ensure that this value is always larger than the size of the image
// so ExifInterface won't stop reading the stream prematurely.
override fun available() = 1024 * 1024 * 1024
override fun available() = availableBytes

override fun close() = delegate.close()

override fun mark(readlimit: Int) = delegate.mark(readlimit)

override fun reset() = delegate.reset()

override fun markSupported() = delegate.markSupported()
private fun interceptBytesRead(bytesRead: Int): Int {
if (bytesRead == -1) availableBytes = 0
return bytesRead
}
}

companion object {
private const val MIME_TYPE_JPEG = "image/jpeg"
private const val MIME_TYPE_WEBP = "image/webp"
private const val MIME_TYPE_HEIC = "image/heic"
private const val MIME_TYPE_HEIF = "image/heif"
private const val GIGABYTE_IN_BYTES = 1024 * 1024 * 1024

// NOTE: We don't support PNG EXIF data as it's very rarely used and requires buffering
// the entire file into memory. All of the supported formats short circuit when the EXIF
Expand Down
Binary file added coil-test/src/main/assets/exif/basic.heic
Binary file not shown.