Skip to content

Commit

Permalink
Removed gif parsing and changed encoded loop count to be available on…
Browse files Browse the repository at this point in the history
…ly on SDK 28 or higher
  • Loading branch information
tgyuuAn committed Nov 9, 2024
1 parent 9b8b120 commit 5b302b6
Show file tree
Hide file tree
Showing 5 changed files with 30 additions and 95 deletions.
27 changes: 9 additions & 18 deletions coil-gif/src/main/java/coil3/gif/AnimatedImageDecoder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
}
Expand All @@ -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
Expand Down
10 changes: 2 additions & 8 deletions coil-gif/src/main/java/coil3/gif/GifDecoder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()) }

Expand All @@ -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) {
Expand Down
19 changes: 12 additions & 7 deletions coil-gif/src/main/java/coil3/gif/MovieDrawable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
60 changes: 0 additions & 60 deletions coil-gif/src/main/java/coil3/gif/decodeUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
9 changes: 7 additions & 2 deletions coil-gif/src/main/java/coil3/gif/imageRequests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package coil3.gif
import android.graphics.ImageDecoder
import android.graphics.drawable.AnimatedImageDrawable
import android.graphics.drawable.Drawable
import android.os.Build.VERSION.SDK_INT
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
Expand All @@ -18,7 +19,11 @@ import coil3.request.Options
* @see AnimatedImageDrawable.setRepeatCount
*/
fun ImageRequest.Builder.repeatCount(repeatCount: Int) = apply {
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" }
}
extras[repeatCountKey] = repeatCount
}

Expand Down

0 comments on commit 5b302b6

Please sign in to comment.