diff --git a/zoomable-image/coil3/src/androidTest/assets/p3_image.jpg b/zoomable-image/coil3/src/androidTest/assets/p3_image.jpg new file mode 100644 index 00000000..831667c7 --- /dev/null +++ b/zoomable-image/coil3/src/androidTest/assets/p3_image.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:289d4dd8e1598071f30aedaedf4f6671564672c79683c78556238840b70b4fb7 +size 87536 diff --git a/zoomable-image/coil3/src/androidTest/kotlin/me/saket/telephoto/zoomable/coil3/Coil3ImageSourceTest.kt b/zoomable-image/coil3/src/androidTest/kotlin/me/saket/telephoto/zoomable/coil3/Coil3ImageSourceTest.kt index bfaff8e2..67e118f8 100644 --- a/zoomable-image/coil3/src/androidTest/kotlin/me/saket/telephoto/zoomable/coil3/Coil3ImageSourceTest.kt +++ b/zoomable-image/coil3/src/androidTest/kotlin/me/saket/telephoto/zoomable/coil3/Coil3ImageSourceTest.kt @@ -7,7 +7,6 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.drawable.ColorDrawable import android.net.Uri -import android.os.Environment import android.provider.MediaStore import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size @@ -632,6 +631,37 @@ class Coil3ImageSourceTest { assertThat(loadCount).isEqualTo(1) } + // Regression test for https://github.com/saket/telephoto/issues/129. + @Test fun do_not_crash_if_the_color_space_cannot_be_parsed_by_compose_ui() = runTest { + serverRule.server.dispatcher = object : Dispatcher() { + override fun dispatch(request: RecordedRequest) = assetAsResponse("p3_image.jpg") + } + val imageUrl = withContext(Dispatchers.IO) { + serverRule.server.url("ignored").toString() + } + + lateinit var imageState: ZoomableImageState + rule.setContent { + ZoomableAsyncImage( + state = rememberZoomableImageState().also { imageState = it }, + modifier = Modifier.fillMaxSize(), + model = ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .allowHardware(false) // Unsupported by Screenshot.capture() + .listener(onError = { _, res -> + res.throwable.printStackTrace() + }) + .build(), + contentDescription = null + ) + } + + rule.waitUntil { imageState.isImageDisplayed } + rule.runOnIdle { + dropshots.assertSnapshot(rule.activity) + } + } + context(TestScope) private fun resolve( canvasSize: Size = Size(1080f, 1920f), diff --git a/zoomable-image/coil3/src/androidTest/screenshots/do_not_crash_if_the_color_space_cannot_be_parsed_by_compose_ui.png b/zoomable-image/coil3/src/androidTest/screenshots/do_not_crash_if_the_color_space_cannot_be_parsed_by_compose_ui.png new file mode 100644 index 00000000..ccc24b57 --- /dev/null +++ b/zoomable-image/coil3/src/androidTest/screenshots/do_not_crash_if_the_color_space_cannot_be_parsed_by_compose_ui.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec55541118da2df239b647c072de25335cfb8e26cc27541c726c1ecd5e910a55 +size 736917 diff --git a/zoomable-image/sub-sampling-image/src/androidMain/kotlin/me/saket/telephoto/subsamplingimage/ImageBitmapOptions.kt b/zoomable-image/sub-sampling-image/src/androidMain/kotlin/me/saket/telephoto/subsamplingimage/ImageBitmapOptions.kt index a2a219fe..f3f34c43 100644 --- a/zoomable-image/sub-sampling-image/src/androidMain/kotlin/me/saket/telephoto/subsamplingimage/ImageBitmapOptions.kt +++ b/zoomable-image/sub-sampling-image/src/androidMain/kotlin/me/saket/telephoto/subsamplingimage/ImageBitmapOptions.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.colorspace.ColorSpace import androidx.compose.ui.graphics.toAndroidColorSpace import androidx.compose.ui.graphics.toComposeColorSpace import dev.drewhamilton.poko.Poko +import kotlin.LazyThreadSafetyMode.NONE import android.graphics.ColorSpace as AndroidColorSpace import androidx.compose.ui.graphics.colorspace.ColorSpace as ComposeColorSpace @@ -19,16 +20,21 @@ import androidx.compose.ui.graphics.colorspace.ColorSpace as ComposeColorSpace @Immutable class ImageBitmapOptions internal constructor( val config: ImageBitmapConfig = ImageBitmapConfig.Argb8888, - val colorSpace: ColorSpace? = null, internal val androidColorSpace: AndroidColorSpace? = null, + colorSpace: () -> ColorSpace?, ) { + // The compose color space is computed lazily to prevent crashes by bad Android <> Compose UI + // conversions. Example: https://issuetracker.google.com/issues/377021410#comment3 + @Suppress("unused") + val colorSpace: ColorSpace? by lazy(NONE) { colorSpace() } + // TODO: remove when https://issuetracker.google.com/issues/377021410 is resolved. constructor( config: ImageBitmapConfig = ImageBitmapConfig.Argb8888, colorSpace: ComposeColorSpace? = null, ) : this( config = config, - colorSpace = colorSpace, + colorSpace = { colorSpace }, androidColorSpace = if (SDK_INT >= 26) colorSpace?.toAndroidColorSpace() else null, ) @@ -44,7 +50,7 @@ fun ImageBitmapOptions(from: Bitmap): ImageBitmapOptions { val androidColorSpace = if (SDK_INT >= 26) from.colorSpace else null return ImageBitmapOptions( config = from.config!!.toComposeConfig(), - colorSpace = if (SDK_INT >= 26) androidColorSpace?.toComposeColorSpace() else null, + colorSpace = { if (SDK_INT >= 26) androidColorSpace?.toComposeColorSpace() else null }, androidColorSpace = androidColorSpace, ) }