diff --git a/coil-core/api/android/coil-core.api b/coil-core/api/android/coil-core.api index 6c0aeb1015..e45ada8340 100644 --- a/coil-core/api/android/coil-core.api +++ b/coil-core/api/android/coil-core.api @@ -335,19 +335,6 @@ public final class coil3/decode/ResourceMetadata : coil3/decode/ImageSource$Meta public final fun getResId ()I } -public final class coil3/decode/StaticImageDecoderDecoder : coil3/decode/Decoder { - public fun (Landroid/graphics/ImageDecoder$Source;Lcoil3/decode/ImageSource;Lcoil3/request/Options;Lkotlinx/coroutines/sync/Semaphore;)V - public synthetic fun (Landroid/graphics/ImageDecoder$Source;Lcoil3/decode/ImageSource;Lcoil3/request/Options;Lkotlinx/coroutines/sync/Semaphore;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun decode (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class coil3/decode/StaticImageDecoderDecoder$Factory : coil3/decode/Decoder$Factory { - public fun ()V - public fun (I)V - public synthetic fun (IILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun create (Lcoil3/fetch/SourceFetchResult;Lcoil3/request/Options;Lcoil3/ImageLoader;)Lcoil3/decode/Decoder; -} - public abstract interface class coil3/disk/DiskCache { public abstract fun clear ()V public abstract fun getDirectory ()Lokio/Path; diff --git a/coil-core/src/androidMain/kotlin/coil3/decode/StaticImageDecoderDecoder.kt b/coil-core/src/androidMain/kotlin/coil3/decode/StaticImageDecoderDecoder.kt index 64c7d8f7ba..af205f182e 100644 --- a/coil-core/src/androidMain/kotlin/coil3/decode/StaticImageDecoderDecoder.kt +++ b/coil-core/src/androidMain/kotlin/coil3/decode/StaticImageDecoderDecoder.kt @@ -6,7 +6,6 @@ import androidx.core.graphics.decodeBitmap import androidx.core.util.component1 import androidx.core.util.component2 import coil3.ImageLoader -import coil3.annotation.ExperimentalCoilApi import coil3.asCoilImage import coil3.decode.BitmapFactoryDecoder.Companion.DEFAULT_MAX_PARALLELISM import coil3.fetch.SourceFetchResult @@ -22,61 +21,60 @@ import coil3.util.widthPx import kotlin.math.roundToInt import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit +import okio.Closeable +import okio.FileSystem -@ExperimentalCoilApi @RequiresApi(28) -class StaticImageDecoderDecoder( +internal class StaticImageDecoderDecoder( private val source: ImageDecoder.Source, - private val toClose: ImageSource, + private val closeable: Closeable, private val options: Options, private val parallelismLock: Semaphore = Semaphore(Int.MAX_VALUE), ) : Decoder { override suspend fun decode() = parallelismLock.withPermit { var isSampled = false - val image = run { - var imageDecoder: ImageDecoder? = null - try { - source.decodeBitmap { info, _ -> - // Capture the image decoder to manually close it later. - imageDecoder = this + var imageDecoder: ImageDecoder? = null + try { + val bitmap = source.decodeBitmap { info, _ -> + // Capture the image decoder to manually close it later. + imageDecoder = this - // Configure the output image's size. - val (srcWidth, srcHeight) = info.size - val dstWidth = options.size.widthPx(options.scale) { srcWidth } - val dstHeight = options.size.heightPx(options.scale) { srcHeight } - if (srcWidth > 0 && srcHeight > 0 && - (srcWidth != dstWidth || srcHeight != dstHeight)) { - val multiplier = DecodeUtils.computeSizeMultiplier( - srcWidth = srcWidth, - srcHeight = srcHeight, - dstWidth = dstWidth, - dstHeight = dstHeight, - scale = options.scale, - ) + // Configure the output image's size. + val (srcWidth, srcHeight) = info.size + val dstWidth = options.size.widthPx(options.scale) { srcWidth } + val dstHeight = options.size.heightPx(options.scale) { srcHeight } + if (srcWidth > 0 && srcHeight > 0 && + (srcWidth != dstWidth || srcHeight != dstHeight)) { + val multiplier = DecodeUtils.computeSizeMultiplier( + srcWidth = srcWidth, + srcHeight = srcHeight, + dstWidth = dstWidth, + dstHeight = dstHeight, + scale = options.scale, + ) - // Set the target size if the image is larger than the requested dimensions - // or the request requires exact dimensions. - isSampled = multiplier < 1 - if (isSampled || !options.allowInexactSize) { - val targetWidth = (multiplier * srcWidth).roundToInt() - val targetHeight = (multiplier * srcHeight).roundToInt() - setTargetSize(targetWidth, targetHeight) - } + // Set the target size if the image is larger than the requested dimensions + // or the request requires exact dimensions. + isSampled = multiplier < 1 + if (isSampled || !options.allowInexactSize) { + val targetWidth = (multiplier * srcWidth).roundToInt() + val targetHeight = (multiplier * srcHeight).roundToInt() + setTargetSize(targetWidth, targetHeight) } - - // Configure any other attributes. - configureImageDecoderProperties() } - } finally { - imageDecoder?.close() - toClose.close() + + // Configure any other attributes. + configureImageDecoderProperties() } + DecodeResult( + image = bitmap.asCoilImage(), + isSampled = isSampled, + ) + } finally { + imageDecoder?.close() + closeable.close() } - DecodeResult( - image = image.asCoilImage(), - isSampled = isSampled, - ) } private fun ImageDecoder.configureImageDecoderProperties() { @@ -114,9 +112,11 @@ class StaticImageDecoderDecoder( @RequiresApi(28) private fun ImageSource.imageDecoderSourceOrNull(options: Options): ImageDecoder.Source? { - val file = fileOrNull() - if (file != null) { - return ImageDecoder.createSource(file.toFile()) + if (fileSystem == FileSystem.SYSTEM) { + val file = fileOrNull() + if (file != null && fileSystem == FileSystem.SYSTEM) { + return ImageDecoder.createSource(file.toFile()) + } } val metadata = metadata diff --git a/coil-gif/src/main/java/coil3/decode/AnimatedImageDecoderDecoder.kt b/coil-gif/src/main/java/coil3/decode/AnimatedImageDecoderDecoder.kt index 0720dffa11..deb584effd 100644 --- a/coil-gif/src/main/java/coil3/decode/AnimatedImageDecoderDecoder.kt +++ b/coil-gif/src/main/java/coil3/decode/AnimatedImageDecoderDecoder.kt @@ -33,7 +33,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext import okio.BufferedSource -import okio.buffer +import okio.FileSystem /** * A [Decoder] that uses [ImageDecoder] to decode GIFs, animated WebPs, and animated HEIFs. @@ -54,9 +54,9 @@ class AnimatedImageDecoderDecoder @JvmOverloads constructor( var isSampled = false val drawable = runInterruptible { var imageDecoder: ImageDecoder? = null - val wrappedSource = wrapImageSource(source) + val source = maybeWrapImageSourceToRewriteFrameDelay(source, enforceMinimumFrameDelay) try { - wrappedSource.toImageDecoderSource().decodeDrawable { info, _ -> + source.toImageDecoderSource().decodeDrawable { info, _ -> // Capture the image decoder to manually close it later. imageDecoder = this @@ -89,7 +89,7 @@ class AnimatedImageDecoderDecoder @JvmOverloads constructor( } } finally { imageDecoder?.close() - wrappedSource.close() + source.close() } } return DecodeResult( @@ -98,22 +98,12 @@ class AnimatedImageDecoderDecoder @JvmOverloads constructor( ) } - private fun wrapImageSource(source: ImageSource): ImageSource { - return if (NeedRewriteGifSource && enforceMinimumFrameDelay && DecodeUtils.isGif(source.source())) { - // Wrap the source to rewrite its frame delay as it's read. - ImageSource( - source = FrameDelayRewritingSource(source.source()).buffer(), - fileSystem = options.fileSystem, - ) - } else { - source - } - } - private fun ImageSource.toImageDecoderSource(): ImageDecoder.Source { - val file = fileOrNull() - if (file != null) { - return ImageDecoder.createSource(file.toFile()) + if (fileSystem == FileSystem.SYSTEM) { + val file = fileOrNull() + if (file != null && fileSystem == FileSystem.SYSTEM) { + return ImageDecoder.createSource(file.toFile()) + } } val metadata = metadata @@ -194,6 +184,3 @@ class AnimatedImageDecoderDecoder @JvmOverloads constructor( } } } - -// https://android.googlesource.com/platform/frameworks/base/+/2be87bb707e2c6d75f668c4aff6697b85fbf5b15 -private val NeedRewriteGifSource = SDK_INT < 34 diff --git a/coil-gif/src/main/java/coil3/decode/FrameDelayRewritingSource.kt b/coil-gif/src/main/java/coil3/decode/FrameDelayRewritingSource.kt index 9c66e02149..f2852a8e53 100644 --- a/coil-gif/src/main/java/coil3/decode/FrameDelayRewritingSource.kt +++ b/coil-gif/src/main/java/coil3/decode/FrameDelayRewritingSource.kt @@ -2,11 +2,13 @@ package coil3.decode +import android.os.Build.VERSION.SDK_INT import okio.Buffer import okio.ByteString import okio.ByteString.Companion.decodeHex import okio.ForwardingSource import okio.Source +import okio.buffer /** * A [ForwardingSource] that rewrites the GIF frame delay in every graphics control block if it's @@ -86,3 +88,20 @@ internal class FrameDelayRewritingSource(delegate: Source) : ForwardingSource(de private const val DEFAULT_FRAME_DELAY = 10 } } + +internal fun maybeWrapImageSourceToRewriteFrameDelay( + source: ImageSource, + enforceMinimumFrameDelay: Boolean, +): ImageSource { + // https://android.googlesource.com/platform/frameworks/base/+/2be87bb707e2c6d75f668c4aff6697b85fbf5b15 + if (enforceMinimumFrameDelay && SDK_INT < 34 && DecodeUtils.isGif(source.source())) { + // Wrap the source to rewrite its frame delay as it's read. + return ImageSource( + source = FrameDelayRewritingSource(source.source()).buffer(), + fileSystem = source.fileSystem, + // Intentionally don't copy any metadata. + ) + } else { + return source + } +} diff --git a/coil-gif/src/main/java/coil3/decode/GifDecoder.kt b/coil-gif/src/main/java/coil3/decode/GifDecoder.kt index 49772895d8..49789a585e 100644 --- a/coil-gif/src/main/java/coil3/decode/GifDecoder.kt +++ b/coil-gif/src/main/java/coil3/decode/GifDecoder.kt @@ -18,7 +18,6 @@ import coil3.request.repeatCount import coil3.util.animatable2CompatCallbackOf import coil3.util.isHardware import kotlinx.coroutines.runInterruptible -import okio.buffer /** * A [Decoder] that uses [Movie] to decode GIFs. @@ -31,16 +30,12 @@ import okio.buffer class GifDecoder @JvmOverloads constructor( private val source: ImageSource, private val options: Options, - private val enforceMinimumFrameDelay: Boolean = true + private val enforceMinimumFrameDelay: Boolean = true, ) : Decoder { override suspend fun decode() = runInterruptible { - val bufferedSource = if (enforceMinimumFrameDelay) { - FrameDelayRewritingSource(source.source()).buffer() - } else { - source.source() - } - val movie: Movie? = bufferedSource.use { Movie.decodeStream(it.inputStream()) } + val source = maybeWrapImageSourceToRewriteFrameDelay(source, enforceMinimumFrameDelay) + val movie: Movie? = source.use { Movie.decodeStream(it.source().inputStream()) } check(movie != null && movie.width() > 0 && movie.height() > 0) { "Failed to decode GIF." } @@ -51,7 +46,7 @@ class GifDecoder @JvmOverloads constructor( options.bitmapConfig.isHardware -> Bitmap.Config.ARGB_8888 else -> options.bitmapConfig }, - scale = options.scale + scale = options.scale, ) drawable.setRepeatCount(options.repeatCount) @@ -68,7 +63,7 @@ class GifDecoder @JvmOverloads constructor( DecodeResult( image = drawable.asCoilImage(), - isSampled = false + isSampled = false, ) }