From 7fdda43e27ff3fb5304df5f377b632a8335b59a9 Mon Sep 17 00:00:00 2001 From: Baptiste Candellier Date: Fri, 4 Oct 2024 19:04:13 +0200 Subject: [PATCH 1/4] feat(coil): enable animated images on iOS --- .../utils/coil/AnimatedSkiaImageDecoder.kt | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/AnimatedSkiaImageDecoder.kt diff --git a/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/AnimatedSkiaImageDecoder.kt b/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/AnimatedSkiaImageDecoder.kt new file mode 100644 index 000000000..c57b87521 --- /dev/null +++ b/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/AnimatedSkiaImageDecoder.kt @@ -0,0 +1,147 @@ +package fr.outadoc.justchatting.utils.coil + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.setValue +import coil3.Canvas +import coil3.Image +import coil3.ImageLoader +import coil3.decode.DecodeResult +import coil3.decode.Decoder +import coil3.decode.ImageSource +import coil3.fetch.SourceFetchResult +import coil3.request.Options +import kotlin.time.TimeSource +import okio.use +import org.jetbrains.skia.AnimationFrameInfo +import org.jetbrains.skia.Bitmap +import org.jetbrains.skia.Codec +import org.jetbrains.skia.ColorAlphaType +import org.jetbrains.skia.ColorInfo +import org.jetbrains.skia.ColorSpace +import org.jetbrains.skia.ColorType +import org.jetbrains.skia.Data +import org.jetbrains.skia.ImageInfo +import org.jetbrains.skia.Image as SkiaImage + +internal class AnimatedSkiaImageDecoder( + private val source: ImageSource, + private val options: Options, + private val prerenderFrames: Boolean = true, +) : Decoder { + + override suspend fun decode(): DecodeResult? { + val bytes = source.source().use { it.readByteArray() } + val codec = Codec.makeFromData(Data.makeFromBytes(bytes)) + return DecodeResult( + image = AnimatedSkiaImage(codec, prerenderFrames), + isSampled = false, + ) + } + + class Factory( + private val prerenderFrames: Boolean = false, + ) : Decoder.Factory { + override fun create( + result: SourceFetchResult, + options: Options, + imageLoader: ImageLoader, + ): Decoder? = AnimatedSkiaImageDecoder(result.source, options, prerenderFrames) + } +} + +private class AnimatedSkiaImage( + private val codec: Codec, + prerenderFrames: Boolean, +) : Image { + private val imageInfo = ImageInfo( + colorInfo = ColorInfo( + colorType = ColorType.BGRA_8888, + alphaType = ColorAlphaType.UNPREMUL, + colorSpace = ColorSpace.sRGB, + ), + width = codec.width, + height = codec.height, + ) + private val bitmap = Bitmap().apply { allocPixels(codec.imageInfo) } + private val frames = Array(codec.frameCount) { index -> + if (prerenderFrames) decodeFrame(index) else null + } + + private var invalidateTick by mutableIntStateOf(0) + private var startTime: TimeSource.Monotonic.ValueTimeMark? = null + private var lastFrameIndex = 0 + private var isDone = false + + override val size: Long + get() { + var size = codec.imageInfo.computeMinByteSize().toLong() + if (size <= 0L) { + // Estimate 4 bytes per pixel. + size = 4L * codec.width * codec.height + } + return size.coerceAtLeast(0) + } + + override val width: Int + get() = codec.width + + override val height: Int + get() = codec.height + + override val shareable: Boolean + get() = false + + override fun draw(canvas: Canvas) { + val totalFrames = codec.framesInfo.size + if (totalFrames == 0) { + return + } + + if (totalFrames == 1) { + canvas.drawFrame(0) + return + } + + if (isDone) { + canvas.drawFrame(lastFrameIndex) + return + } + + val startTime = startTime ?: TimeSource.Monotonic.markNow().also { startTime = it } + val elapsedTime = startTime.elapsedNow().inWholeMilliseconds + var durationMillis = 0 + var frameIndex = totalFrames - 1 + for ((index, frame) in codec.framesInfo.withIndex()) { + if (durationMillis > elapsedTime) { + frameIndex = (index - 1).coerceAtLeast(0) + break + } + durationMillis += frame.safeFrameDuration + } + lastFrameIndex = frameIndex + isDone = frameIndex == (totalFrames - 1) + + canvas.drawFrame(frameIndex) + + if (!isDone) { + // Increment this value to force the image to be redrawn. + invalidateTick++ + } + } + + private fun decodeFrame(frameIndex: Int): ByteArray { + codec.readPixels(bitmap, frameIndex) + return bitmap.readPixels(imageInfo, imageInfo.minRowBytes)!! + } + + private fun Canvas.drawFrame(frameIndex: Int) { + val frame = frames[frameIndex] ?: decodeFrame(frameIndex).also { frames[frameIndex] = it } + drawImage(SkiaImage.makeRaster(imageInfo, frame, imageInfo.minRowBytes), 0f, 0f) + } +} + +private val AnimationFrameInfo.safeFrameDuration: Int + get() = duration.let { if (it <= 0) DEFAULT_FRAME_DURATION else it } + +private const val DEFAULT_FRAME_DURATION = 100 From 305dbc4732199d95fbef3f29bfd7b35df056b0fc Mon Sep 17 00:00:00 2001 From: Baptiste Candellier Date: Fri, 4 Oct 2024 19:31:09 +0200 Subject: [PATCH 2/4] Setup shared image loader --- .../outadoc/justchatting/MainApplication.kt | 46 +--------------- shared/build.gradle.kts | 1 + .../utils/coil/ImageLoaderFactory.android.kt | 40 ++++++++++++++ .../feature/shared/presentation/mobile/App.kt | 7 +++ .../utils/coil/CoilCustomLogger.kt | 37 +++++++++++++ .../utils/coil/ImageLoaderFactory.kt | 5 ++ .../justchatting/di/PlatformModule.ios.kt | 33 ++++++------ .../utils/coil/AnimatedSkiaImageDecoder.kt | 2 +- .../utils/coil/ImageLoaderFactory.ios.kt | 53 +++++++++++++++++++ 9 files changed, 162 insertions(+), 62 deletions(-) create mode 100644 shared/src/androidMain/kotlin/fr/outadoc/justchatting/utils/coil/ImageLoaderFactory.android.kt create mode 100644 shared/src/commonMain/kotlin/fr/outadoc/justchatting/utils/coil/CoilCustomLogger.kt create mode 100644 shared/src/commonMain/kotlin/fr/outadoc/justchatting/utils/coil/ImageLoaderFactory.kt create mode 100644 shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/ImageLoaderFactory.ios.kt diff --git a/app-android/src/androidMain/kotlin/fr/outadoc/justchatting/MainApplication.kt b/app-android/src/androidMain/kotlin/fr/outadoc/justchatting/MainApplication.kt index 59f7a7dab..5ac3447bb 100644 --- a/app-android/src/androidMain/kotlin/fr/outadoc/justchatting/MainApplication.kt +++ b/app-android/src/androidMain/kotlin/fr/outadoc/justchatting/MainApplication.kt @@ -1,26 +1,11 @@ package fr.outadoc.justchatting import android.app.Application -import android.os.Build -import coil3.ImageLoader -import coil3.PlatformContext -import coil3.SingletonImageLoader -import coil3.disk.DiskCache -import coil3.gif.AnimatedImageDecoder -import coil3.gif.GifDecoder -import coil3.memory.MemoryCache -import coil3.request.crossfade -import coil3.request.transitionFactory -import coil3.transition.Transition -import coil3.util.DebugLogger import com.google.android.material.color.DynamicColors import fr.outadoc.justchatting.utils.logging.AndroidLogStrategy import fr.outadoc.justchatting.utils.logging.Logger -import okio.Path.Companion.toOkioPath -class MainApplication : - Application(), - SingletonImageLoader.Factory { +class MainApplication : Application() { override fun onCreate() { super.onCreate() @@ -31,33 +16,4 @@ class MainApplication : DynamicColors.applyToActivitiesIfAvailable(this) } - - override fun newImageLoader(context: PlatformContext): ImageLoader = - ImageLoader.Builder(context) - .crossfade(true) - .memoryCache { - MemoryCache.Builder() - .maxSizePercent(applicationContext, percent = 0.25) - .build() - } - .diskCache { - DiskCache.Builder() - .directory(applicationContext.cacheDir.toOkioPath().resolve("image_cache")) - .maxSizePercent(0.02) - .build() - } - .transitionFactory(Transition.Factory.NONE) - .components { - if (Build.VERSION.SDK_INT >= 28) { - add(AnimatedImageDecoder.Factory(enforceMinimumFrameDelay = true)) - } else { - add(GifDecoder.Factory()) - } - } - .apply { - if (BuildConfig.ENABLE_LOGGING) { - logger(DebugLogger()) - } - } - .build() } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 780ceb374..61075a2f8 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -113,6 +113,7 @@ kotlin { implementation(libs.androidx.palette) implementation(libs.androidx.paging.runtime.android) implementation(libs.androidx.splashscreen) + implementation(libs.coil.gif) implementation(libs.koin.android) implementation(libs.ktor.client.okhttp) implementation(libs.material.core) diff --git a/shared/src/androidMain/kotlin/fr/outadoc/justchatting/utils/coil/ImageLoaderFactory.android.kt b/shared/src/androidMain/kotlin/fr/outadoc/justchatting/utils/coil/ImageLoaderFactory.android.kt new file mode 100644 index 000000000..40b8b348d --- /dev/null +++ b/shared/src/androidMain/kotlin/fr/outadoc/justchatting/utils/coil/ImageLoaderFactory.android.kt @@ -0,0 +1,40 @@ +package fr.outadoc.justchatting.utils.coil + +import android.os.Build +import coil3.ImageLoader +import coil3.PlatformContext +import coil3.SingletonImageLoader +import coil3.disk.DiskCache +import coil3.gif.AnimatedImageDecoder +import coil3.gif.GifDecoder +import coil3.memory.MemoryCache +import coil3.request.crossfade +import okio.Path.Companion.toOkioPath + +internal actual object ImageLoaderFactory : SingletonImageLoader.Factory { + + override fun newImageLoader(context: PlatformContext): ImageLoader { + return ImageLoader.Builder(context) + .crossfade(true) + .memoryCache { + MemoryCache.Builder() + .maxSizePercent(context, percent = 0.25) + .build() + } + .diskCache { + DiskCache.Builder() + .directory(context.cacheDir.toOkioPath().resolve("image_cache")) + .maxSizePercent(0.02) + .build() + } + .components { + if (Build.VERSION.SDK_INT >= 28) { + add(AnimatedImageDecoder.Factory(enforceMinimumFrameDelay = true)) + } else { + add(GifDecoder.Factory()) + } + } + .logger(CoilCustomLogger()) + .build() + } +} diff --git a/shared/src/commonMain/kotlin/fr/outadoc/justchatting/feature/shared/presentation/mobile/App.kt b/shared/src/commonMain/kotlin/fr/outadoc/justchatting/feature/shared/presentation/mobile/App.kt index 4702c33a4..313158633 100644 --- a/shared/src/commonMain/kotlin/fr/outadoc/justchatting/feature/shared/presentation/mobile/App.kt +++ b/shared/src/commonMain/kotlin/fr/outadoc/justchatting/feature/shared/presentation/mobile/App.kt @@ -15,9 +15,11 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.navigation.compose.rememberNavController +import coil3.SingletonImageLoader import com.eygraber.uri.Uri import fr.outadoc.justchatting.feature.onboarding.presentation.mobile.OnboardingScreen import fr.outadoc.justchatting.feature.shared.presentation.MainRouterViewModel +import fr.outadoc.justchatting.utils.coil.ImageLoaderFactory import fr.outadoc.justchatting.utils.presentation.AppTheme import fr.outadoc.justchatting.utils.presentation.OnLifecycleEvent import org.koin.compose.koinInject @@ -37,6 +39,11 @@ internal fun App( val navController = rememberNavController() val navigator = rememberListDetailPaneScaffoldNavigator() + LaunchedEffect(Unit) { + // Initialize Coil + SingletonImageLoader.setSafe(ImageLoaderFactory) + } + val onChannelClick = { userId: String -> navigator.navigateTo( pane = ListDetailPaneScaffoldRole.Detail, diff --git a/shared/src/commonMain/kotlin/fr/outadoc/justchatting/utils/coil/CoilCustomLogger.kt b/shared/src/commonMain/kotlin/fr/outadoc/justchatting/utils/coil/CoilCustomLogger.kt new file mode 100644 index 000000000..1d7bdb9c3 --- /dev/null +++ b/shared/src/commonMain/kotlin/fr/outadoc/justchatting/utils/coil/CoilCustomLogger.kt @@ -0,0 +1,37 @@ +package fr.outadoc.justchatting.utils.coil + +import fr.outadoc.justchatting.utils.logging.Logger + +internal class CoilCustomLogger( + override var minLevel: coil3.util.Logger.Level = coil3.util.Logger.Level.Debug, +) : coil3.util.Logger { + + override fun log( + tag: String, + level: coil3.util.Logger.Level, + message: String?, + throwable: Throwable?, + ) { + Logger.println( + level = when (level) { + coil3.util.Logger.Level.Error -> Logger.Level.Error + coil3.util.Logger.Level.Warn -> Logger.Level.Warning + coil3.util.Logger.Level.Info -> Logger.Level.Info + coil3.util.Logger.Level.Debug -> Logger.Level.Debug + coil3.util.Logger.Level.Verbose -> Logger.Level.Verbose + }, + tag = tag, + content = { + buildString { + if (message != null) { + appendLine(message) + } + + if (throwable != null) { + appendLine(throwable.stackTraceToString()) + } + } + }, + ) + } +} diff --git a/shared/src/commonMain/kotlin/fr/outadoc/justchatting/utils/coil/ImageLoaderFactory.kt b/shared/src/commonMain/kotlin/fr/outadoc/justchatting/utils/coil/ImageLoaderFactory.kt new file mode 100644 index 000000000..10ab078f3 --- /dev/null +++ b/shared/src/commonMain/kotlin/fr/outadoc/justchatting/utils/coil/ImageLoaderFactory.kt @@ -0,0 +1,5 @@ +package fr.outadoc.justchatting.utils.coil + +import coil3.SingletonImageLoader + +internal expect object ImageLoaderFactory : SingletonImageLoader.Factory diff --git a/shared/src/iosMain/kotlin/fr/outadoc/justchatting/di/PlatformModule.ios.kt b/shared/src/iosMain/kotlin/fr/outadoc/justchatting/di/PlatformModule.ios.kt index adfe71744..0f53449f1 100644 --- a/shared/src/iosMain/kotlin/fr/outadoc/justchatting/di/PlatformModule.ios.kt +++ b/shared/src/iosMain/kotlin/fr/outadoc/justchatting/di/PlatformModule.ios.kt @@ -30,6 +30,7 @@ import platform.Foundation.NSDocumentDirectory import platform.Foundation.NSFileManager import platform.Foundation.NSUserDomainMask +@OptIn(ExperimentalForeignApi::class) internal actual val platformModule: Module get() = module { single { @@ -49,25 +50,10 @@ internal actual val platformModule: Module ) } - @OptIn(ExperimentalForeignApi::class) single> { PreferenceDataStoreFactory.createWithPath( produceFile = { - val documentDirectory: Path = - NSFileManager.defaultManager - .URLForDirectory( - directory = NSDocumentDirectory, - inDomain = NSUserDomainMask, - appropriateForURL = null, - create = false, - error = null, - ) - ?.path - ?.toPath() - ?: error("Could not get document directory") - - documentDirectory - .resolve("fr.outadoc.justchatting.preferences_pb") + getDocumentsDirectory().resolve("fr.outadoc.justchatting.preferences_pb") }, ) } @@ -79,3 +65,18 @@ internal actual val platformModule: Module single { AppleAppVersionNameProvider() } single { AppleReadExternalDependenciesList() } } + +@OptIn(ExperimentalForeignApi::class) +private fun getDocumentsDirectory(): Path { + return NSFileManager.defaultManager + .URLForDirectory( + directory = NSDocumentDirectory, + inDomain = NSUserDomainMask, + appropriateForURL = null, + create = false, + error = null, + ) + ?.path + ?.toPath() + ?: error("Could not get document directory") +} diff --git a/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/AnimatedSkiaImageDecoder.kt b/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/AnimatedSkiaImageDecoder.kt index c57b87521..c5e15d454 100644 --- a/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/AnimatedSkiaImageDecoder.kt +++ b/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/AnimatedSkiaImageDecoder.kt @@ -11,7 +11,6 @@ import coil3.decode.Decoder import coil3.decode.ImageSource import coil3.fetch.SourceFetchResult import coil3.request.Options -import kotlin.time.TimeSource import okio.use import org.jetbrains.skia.AnimationFrameInfo import org.jetbrains.skia.Bitmap @@ -22,6 +21,7 @@ import org.jetbrains.skia.ColorSpace import org.jetbrains.skia.ColorType import org.jetbrains.skia.Data import org.jetbrains.skia.ImageInfo +import kotlin.time.TimeSource import org.jetbrains.skia.Image as SkiaImage internal class AnimatedSkiaImageDecoder( diff --git a/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/ImageLoaderFactory.ios.kt b/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/ImageLoaderFactory.ios.kt new file mode 100644 index 000000000..44b582639 --- /dev/null +++ b/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/ImageLoaderFactory.ios.kt @@ -0,0 +1,53 @@ +package fr.outadoc.justchatting.utils.coil + +import coil3.ImageLoader +import coil3.PlatformContext +import coil3.SingletonImageLoader +import coil3.disk.DiskCache +import coil3.memory.MemoryCache +import coil3.request.crossfade +import kotlinx.cinterop.ExperimentalForeignApi +import okio.Path +import okio.Path.Companion.toPath +import platform.Foundation.NSCachesDirectory +import platform.Foundation.NSFileManager +import platform.Foundation.NSUserDomainMask + +internal actual object ImageLoaderFactory : SingletonImageLoader.Factory { + + override fun newImageLoader(context: PlatformContext): ImageLoader { + return ImageLoader.Builder(context) + .crossfade(true) + .memoryCache { + MemoryCache.Builder() + .maxSizePercent(context, percent = 0.25) + .build() + } + .diskCache { + DiskCache.Builder() + .directory(getCachesDirectory().resolve("image_cache")) + .maxSizePercent(0.02) + .build() + } + .components { + add(AnimatedSkiaImageDecoder.Factory(prerenderFrames = true)) + } + .logger(CoilCustomLogger()) + .build() + } + + @OptIn(ExperimentalForeignApi::class) + private fun getCachesDirectory(): Path { + return NSFileManager.defaultManager + .URLForDirectory( + directory = NSCachesDirectory, + inDomain = NSUserDomainMask, + appropriateForURL = null, + create = false, + error = null, + ) + ?.path + ?.toPath() + ?: error("Could not get caches directory") + } +} From 23e50d4c183dd0d826c65b43a641b70fb3d4a1cb Mon Sep 17 00:00:00 2001 From: Baptiste Candellier Date: Fri, 4 Oct 2024 19:58:34 +0200 Subject: [PATCH 3/4] Fix issue with decoding non-gif images --- .../utils/coil/ImageLoaderFactory.android.kt | 2 +- .../utils/coil/AnimatedSkiaImageDecoder.kt | 24 ++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/shared/src/androidMain/kotlin/fr/outadoc/justchatting/utils/coil/ImageLoaderFactory.android.kt b/shared/src/androidMain/kotlin/fr/outadoc/justchatting/utils/coil/ImageLoaderFactory.android.kt index 40b8b348d..615c17a20 100644 --- a/shared/src/androidMain/kotlin/fr/outadoc/justchatting/utils/coil/ImageLoaderFactory.android.kt +++ b/shared/src/androidMain/kotlin/fr/outadoc/justchatting/utils/coil/ImageLoaderFactory.android.kt @@ -29,7 +29,7 @@ internal actual object ImageLoaderFactory : SingletonImageLoader.Factory { } .components { if (Build.VERSION.SDK_INT >= 28) { - add(AnimatedImageDecoder.Factory(enforceMinimumFrameDelay = true)) + add(AnimatedImageDecoder.Factory()) } else { add(GifDecoder.Factory()) } diff --git a/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/AnimatedSkiaImageDecoder.kt b/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/AnimatedSkiaImageDecoder.kt index c5e15d454..a1391cb43 100644 --- a/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/AnimatedSkiaImageDecoder.kt +++ b/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/AnimatedSkiaImageDecoder.kt @@ -7,10 +7,13 @@ import coil3.Canvas import coil3.Image import coil3.ImageLoader import coil3.decode.DecodeResult +import coil3.decode.DecodeUtils import coil3.decode.Decoder import coil3.decode.ImageSource import coil3.fetch.SourceFetchResult import coil3.request.Options +import okio.BufferedSource +import okio.ByteString.Companion.encodeUtf8 import okio.use import org.jetbrains.skia.AnimationFrameInfo import org.jetbrains.skia.Bitmap @@ -24,6 +27,7 @@ import org.jetbrains.skia.ImageInfo import kotlin.time.TimeSource import org.jetbrains.skia.Image as SkiaImage +@Deprecated("Replace with proper coil3 implementation once available") internal class AnimatedSkiaImageDecoder( private val source: ImageSource, private val options: Options, @@ -42,11 +46,15 @@ internal class AnimatedSkiaImageDecoder( class Factory( private val prerenderFrames: Boolean = false, ) : Decoder.Factory { + override fun create( result: SourceFetchResult, options: Options, imageLoader: ImageLoader, - ): Decoder? = AnimatedSkiaImageDecoder(result.source, options, prerenderFrames) + ): Decoder? { + if (!DecodeUtils.isGif(result.source.source())) return null + return AnimatedSkiaImageDecoder(result.source, options, prerenderFrames) + } } } @@ -145,3 +153,17 @@ private val AnimationFrameInfo.safeFrameDuration: Int get() = duration.let { if (it <= 0) DEFAULT_FRAME_DURATION else it } private const val DEFAULT_FRAME_DURATION = 100 + +// Copied from coil3.gif + +// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp +private val GIF_HEADER_87A = "GIF87a".encodeUtf8() +private val GIF_HEADER_89A = "GIF89a".encodeUtf8() + +/** + * Return 'true' if the [source] contains a GIF image. The [source] is not consumed. + */ +private fun DecodeUtils.isGif(source: BufferedSource): Boolean { + return source.rangeEquals(0, GIF_HEADER_89A) || + source.rangeEquals(0, GIF_HEADER_87A) +} From 9f7c38dac8cb47c90e78cb0ac055a836e479c4b3 Mon Sep 17 00:00:00 2001 From: Baptiste Candellier Date: Fri, 4 Oct 2024 20:02:04 +0200 Subject: [PATCH 4/4] Run spotless --- .../outadoc/justchatting/utils/coil/AnimatedSkiaImageDecoder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/AnimatedSkiaImageDecoder.kt b/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/AnimatedSkiaImageDecoder.kt index a1391cb43..45ddb44da 100644 --- a/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/AnimatedSkiaImageDecoder.kt +++ b/shared/src/iosMain/kotlin/fr/outadoc/justchatting/utils/coil/AnimatedSkiaImageDecoder.kt @@ -165,5 +165,5 @@ private val GIF_HEADER_89A = "GIF89a".encodeUtf8() */ private fun DecodeUtils.isGif(source: BufferedSource): Boolean { return source.rangeEquals(0, GIF_HEADER_89A) || - source.rangeEquals(0, GIF_HEADER_87A) + source.rangeEquals(0, GIF_HEADER_87A) }