diff --git a/coil-base/src/androidTest/java/coil/decode/BitmapFactoryDecoderTest.kt b/coil-base/src/androidTest/java/coil/decode/BitmapFactoryDecoderTest.kt index da0f5b8cf2..6dbf87e3d6 100644 --- a/coil-base/src/androidTest/java/coil/decode/BitmapFactoryDecoderTest.kt +++ b/coil-base/src/androidTest/java/coil/decode/BitmapFactoryDecoderTest.kt @@ -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 @@ -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 @@ -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)) } @@ -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, diff --git a/coil-base/src/main/java/coil/decode/BitmapFactoryDecoder.kt b/coil-base/src/main/java/coil/decode/BitmapFactoryDecoder.kt index 1089f49f8d..77dcaf9dfb 100644 --- a/coil-base/src/main/java/coil/decode/BitmapFactoryDecoder.kt +++ b/coil-base/src/main/java/coil/decode/BitmapFactoryDecoder.kt @@ -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 { @@ -289,25 +290,26 @@ 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 { @@ -315,6 +317,7 @@ internal class BitmapFactoryDecoder(private val context: Context) : Decoder { 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 diff --git a/coil-test/src/main/assets/exif/basic.heic b/coil-test/src/main/assets/exif/basic.heic new file mode 100644 index 0000000000..5cc3bdaae2 Binary files /dev/null and b/coil-test/src/main/assets/exif/basic.heic differ