From 836c5fa21cc359615ce1bdd297fd1c322b698944 Mon Sep 17 00:00:00 2001 From: tgyuuAn Date: Fri, 8 Nov 2024 17:09:42 +0900 Subject: [PATCH] Removed gif parsing and changed encoded loop count to be available only on SDK 28 or higher --- .../java/coil3/gif/AnimatedImageDecoder.kt | 27 +++------ .../src/main/java/coil3/gif/GifDecoder.kt | 10 +--- .../src/main/java/coil3/gif/MovieDrawable.kt | 19 +++--- .../src/main/java/coil3/gif/decodeUtils.kt | 60 ------------------- .../src/main/java/coil3/gif/imageRequests.kt | 4 +- 5 files changed, 25 insertions(+), 95 deletions(-) diff --git a/coil-gif/src/main/java/coil3/gif/AnimatedImageDecoder.kt b/coil-gif/src/main/java/coil3/gif/AnimatedImageDecoder.kt index ed3f76840a..c5a64c163a 100644 --- a/coil-gif/src/main/java/coil3/gif/AnimatedImageDecoder.kt +++ b/coil-gif/src/main/java/coil3/gif/AnimatedImageDecoder.kt @@ -16,8 +16,7 @@ import coil3.decode.Decoder import coil3.decode.ImageSource import coil3.decode.toImageDecoderSourceOrNull import coil3.fetch.SourceFetchResult -import coil3.gif.MovieDrawable.Companion.GIF_LOOP_COUNT -import coil3.gif.MovieDrawable.Companion.REPEAT_INFINITE +import coil3.gif.MovieDrawable.Companion.ENCODED_LOOP_COUNT import coil3.gif.internal.animatable2CallbackOf import coil3.gif.internal.asPostProcessor import coil3.gif.internal.maybeWrapImageSourceToRewriteFrameDelay @@ -57,21 +56,10 @@ class AnimatedImageDecoder( override suspend fun decode(): DecodeResult { var isSampled = false - val loopCount = if (options.repeatCount == GIF_LOOP_COUNT) { - extractLoopCountFromGif(source.source().peek()) ?: REPEAT_INFINITE - } else { - options.repeatCount - } - val drawable = runInterruptible { - maybeWrapImageSourceToRewriteFrameDelay( - source, - enforceMinimumFrameDelay, - ).use { source -> + maybeWrapImageSourceToRewriteFrameDelay(source, enforceMinimumFrameDelay).use { source -> val imageSource = source.toImageDecoderSourceOrNull(options, animated = true) - ?: ImageDecoder.createSource( - source.source().use { it.squashToDirectByteBuffer() }, - ) + ?: ImageDecoder.createSource(source.source().use { it.squashToDirectByteBuffer() }) imageSource.decodeDrawable { info, _ -> // Configure the output image's size. val (srcWidth, srcHeight) = info.size @@ -102,13 +90,14 @@ class AnimatedImageDecoder( setTargetSize(targetWidth, targetHeight) } } + // Configure any other attributes. configureImageDecoderProperties() } } } return DecodeResult( - image = wrapDrawable(drawable, loopCount).asImage(), + image = wrapDrawable(drawable).asImage(), isSampled = isSampled, ) } @@ -130,12 +119,14 @@ class AnimatedImageDecoder( postProcessor = options.animatedTransformation?.asPostProcessor() } - private suspend fun wrapDrawable(baseDrawable: Drawable, loopCount: Int): Drawable { + private suspend fun wrapDrawable(baseDrawable: Drawable): Drawable { if (baseDrawable !is AnimatedImageDrawable) { return baseDrawable } - baseDrawable.repeatCount = loopCount + if (options.repeatCount != ENCODED_LOOP_COUNT) { + baseDrawable.repeatCount = options.repeatCount + } // Set the start and end animation callbacks if any one is supplied through the request. val onStart = options.animationStartCallback diff --git a/coil-gif/src/main/java/coil3/gif/GifDecoder.kt b/coil-gif/src/main/java/coil3/gif/GifDecoder.kt index 4292df1e00..eb4704173d 100644 --- a/coil-gif/src/main/java/coil3/gif/GifDecoder.kt +++ b/coil-gif/src/main/java/coil3/gif/GifDecoder.kt @@ -34,13 +34,6 @@ class GifDecoder( ) : Decoder { override suspend fun decode() = runInterruptible { - val loopCount = if (options.repeatCount == MovieDrawable.GIF_LOOP_COUNT) { - source.source().peek().let { extractLoopCountFromGif(it) } - ?: MovieDrawable.REPEAT_INFINITE - } else { - options.repeatCount - } - val source = maybeWrapImageSourceToRewriteFrameDelay(source, enforceMinimumFrameDelay) val movie: Movie? = source.use { Movie.decodeStream(it.source().inputStream()) } @@ -56,8 +49,9 @@ class GifDecoder( scale = options.scale, ) - drawable.setRepeatCount(loopCount) + drawable.setRepeatCount(options.repeatCount) + // Set the start and end animation callbacks if any one is supplied through the request. val onStart = options.animationStartCallback val onEnd = options.animationEndCallback if (onStart != null || onEnd != null) { diff --git a/coil-gif/src/main/java/coil3/gif/MovieDrawable.kt b/coil-gif/src/main/java/coil3/gif/MovieDrawable.kt index 857ee7bbe5..b2b716782e 100644 --- a/coil-gif/src/main/java/coil3/gif/MovieDrawable.kt +++ b/coil-gif/src/main/java/coil3/gif/MovieDrawable.kt @@ -14,7 +14,9 @@ import android.graphics.PorterDuff import android.graphics.Rect import android.graphics.drawable.AnimatedImageDrawable import android.graphics.drawable.Drawable +import android.os.Build.VERSION.SDK_INT import android.os.SystemClock +import androidx.annotation.RequiresApi import androidx.core.graphics.createBitmap import androidx.core.graphics.withSave import androidx.vectordrawable.graphics.drawable.Animatable2Compat @@ -151,7 +153,11 @@ class MovieDrawable @JvmOverloads constructor( * Default: [REPEAT_INFINITE] */ fun setRepeatCount(repeatCount: Int) { - require(repeatCount >= GIF_LOOP_COUNT) { "Invalid repeatCount: $repeatCount" } + if (SDK_INT >= 28) { + require(repeatCount >= ENCODED_LOOP_COUNT) { "Invalid repeatCount: $repeatCount" } + } else { + require(repeatCount >= REPEAT_INFINITE) { "Invalid repeatCount: $repeatCount" } + } this.repeatCount = repeatCount } @@ -193,8 +199,7 @@ class MovieDrawable @JvmOverloads constructor( @Suppress("DEPRECATION") override fun getOpacity(): Int { return if (paint.alpha == 255 && - (pixelOpacity == OPAQUE || (pixelOpacity == UNCHANGED && movie.isOpaque)) - ) { + (pixelOpacity == OPAQUE || (pixelOpacity == UNCHANGED && movie.isOpaque))) { PixelFormat.OPAQUE } else { PixelFormat.TRANSLUCENT @@ -287,10 +292,10 @@ class MovieDrawable @JvmOverloads constructor( const val REPEAT_INFINITE = -1 /** - * Pass this to [setRepeatCount] to repeat according to GIF's LoopCount metadata. - * - * If the metadata is not available, it will default to infinite repetition. + * Pass this to [setRepeatCount] to repeat according to encoded LoopCount metadata. + * This only applies when using [AnimatedImageDecoder]. */ - const val GIF_LOOP_COUNT = -2 + @RequiresApi(28) + const val ENCODED_LOOP_COUNT = -2 } } diff --git a/coil-gif/src/main/java/coil3/gif/decodeUtils.kt b/coil-gif/src/main/java/coil3/gif/decodeUtils.kt index 2c72822ed3..575ed1598f 100644 --- a/coil-gif/src/main/java/coil3/gif/decodeUtils.kt +++ b/coil-gif/src/main/java/coil3/gif/decodeUtils.kt @@ -63,63 +63,3 @@ fun DecodeUtils.isAnimatedHeif(source: BufferedSource): Boolean { source.rangeEquals(8, HEIF_HEADER_HEVC) || source.rangeEquals(8, HEIF_HEADER_HEVX)) } - -internal fun extractLoopCountFromGif(source: BufferedSource): Int? { - return try { - parseLoopCount(source) - } catch (e: Exception) { - null - } -} - -private fun parseLoopCount(source: BufferedSource): Int? { - val headerBytes = ByteArray(6) - if (source.read(headerBytes) != 6) return null - - val screenDescriptorBytes = ByteArray(7) - if (source.read(screenDescriptorBytes) != 7) return null - - if ((screenDescriptorBytes[4] and 0b10000000.toByte()) != 0.toByte()) { - val colorTableSize = 3 * (1 shl ((screenDescriptorBytes[4].toInt() and 0b00000111) + 1)) - source.skip(colorTableSize.toLong()) - } - - // Handle Application Extension Block - while (!source.exhausted()) { - val blockType = source.readByte().toInt() and 0xFF - if (blockType == 0x21) { // Extension Introducer - val label = source.readByte().toInt() and 0xFF - if (label == 0xFF) { // Application Extension - val blockSize = source.readByte().toInt() and 0xFF - val appIdentifier = source.readUtf8(8) - val appAuthCode = source.readUtf8(3) - - if (appIdentifier == "NETSCAPE" && appAuthCode == "2.0") { - val dataBlockSize = source.readByte().toInt() and 0xFF - val loopCountIndicator = source.readByte().toInt() and 0xFF - - // Read low and high bytes for loop count - val loopCountLow = source.readByte().toInt() and 0xFF - val loopCountHigh = source.readByte().toInt() and 0xFF - val loopCount = (loopCountHigh shl 8) or loopCountLow - val blockTerminator = source.readByte().toInt() and 0xFF - return if (loopCount == 0) null else loopCount - } else { - skipExtensionBlock(source) // Skip if not NETSCAPE - } - } else { - skipExtensionBlock(source) // Skip other extension blocks - } - } - } - return null -} - -// Extension Blocks always terminate with a zero -private fun skipExtensionBlock(source: BufferedSource) { - while (true) { - val size = source.readByte().toInt() and 0xFF - if (size == 0) break - source.skip(size.toLong()) - } -} diff --git a/coil-gif/src/main/java/coil3/gif/imageRequests.kt b/coil-gif/src/main/java/coil3/gif/imageRequests.kt index cae0251812..53145256ad 100644 --- a/coil-gif/src/main/java/coil3/gif/imageRequests.kt +++ b/coil-gif/src/main/java/coil3/gif/imageRequests.kt @@ -6,7 +6,7 @@ import android.graphics.drawable.Drawable import coil3.Extras import coil3.annotation.ExperimentalCoilApi import coil3.getExtra -import coil3.gif.MovieDrawable.Companion.GIF_LOOP_COUNT +import coil3.gif.MovieDrawable.Companion.ENCODED_LOOP_COUNT import coil3.gif.MovieDrawable.Companion.REPEAT_INFINITE import coil3.request.ImageRequest import coil3.request.Options @@ -18,7 +18,7 @@ import coil3.request.Options * @see AnimatedImageDrawable.setRepeatCount */ fun ImageRequest.Builder.repeatCount(repeatCount: Int) = apply { - require(repeatCount >= GIF_LOOP_COUNT) { "Invalid repeatCount: $repeatCount" } + require(repeatCount >= ENCODED_LOOP_COUNT) { "Invalid repeatCount: $repeatCount" } extras[repeatCountKey] = repeatCount }