Skip to content

Commit

Permalink
Prevent sub-sampling of single-frame GIFs
Browse files Browse the repository at this point in the history
Fixes #83
  • Loading branch information
saket committed Apr 26, 2024
1 parent f086e95 commit 483bf05
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,35 @@ import okio.ByteString.Companion.encodeUtf8
private val SVG_TAG: ByteString = "<svg".encodeUtf8()
private val LEFT_ANGLE_BRACKET: ByteString = "<".encodeUtf8()

// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
private val GIF_HEADER_87A = "GIF87a".encodeUtf8()
private val GIF_HEADER_89A = "GIF89a".encodeUtf8()

/**
* Copied from coil-svg.
* TODO: remove this if https://github.com/coil-kt/coil/issues/1811 is accepted.
*/
*
* Return 'true' if the [source] contains an SVG image. The [source] is not consumed.
*
* NOTE: There's no guaranteed method to determine if a byte stream is an SVG without attempting
* to decode it. This method uses heuristics.
*/
@Suppress("UnusedReceiverParameter")
internal fun DecodeUtils.isSvg(source: BufferedSource): Boolean {
return source.rangeEquals(0, LEFT_ANGLE_BRACKET) &&
source.indexOf(SVG_TAG, 0, 1024) != -1L
}

/**
* Copied from coil-gif.
*
* Return 'true' if the [source] contains a GIF image. The [source] is not consumed.
*/
@Suppress("UnusedReceiverParameter")
internal fun DecodeUtils.isGif(source: BufferedSource): Boolean {
return source.rangeEquals(0, GIF_HEADER_89A) ||
source.rangeEquals(0, GIF_HEADER_87A)
}

/** Copied from coil-svg. */
internal fun BufferedSource.indexOf(bytes: ByteString, fromIndex: Long, toIndex: Long): Long {
require(bytes.size > 0) { "bytes is empty" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,41 +22,33 @@ context(Resolver)
internal suspend fun SubSamplingImageSource.canBeSubSampled(): Boolean {
val preventSubSampling = when (this) {
is ResourceImageSource -> isVectorDrawable()
is AssetImageSource -> isSvgDecoderPresent() && isSvg()
is UriImageSource -> isSvgDecoderPresent() && isSvg()
is FileImageSource -> isSvgDecoderPresent() && isSvg(FileSystem.SYSTEM.source(path))
is RawImageSource -> isSvgDecoderPresent() && isSvg(source.invoke())
is AssetImageSource -> canBeSubSampled()
is UriImageSource -> canBeSubSampled()
is FileImageSource -> canBeSubSampled(FileSystem.SYSTEM.source(path))
is RawImageSource -> canBeSubSampled(source.invoke())
}
return !preventSubSampling
}

context(Resolver)
private fun isSvgDecoderPresent(): Boolean {
// Searching for coil's SvgDecoder by name isn't the best idea,
// but it'll prevent opening of bitmap sources and inspecting
// them for SVGs for projects that don't need SVGs.
return imageLoader.components.decoderFactories.any {
it::class.qualifiedName?.contains("svg", ignoreCase = true) == true
}
}

context(Resolver)
private fun ResourceImageSource.isVectorDrawable(): Boolean =
TypedValue().apply {
request.context.resources.getValue(id, this, /* resolveRefs = */ true)
}.string.endsWith(".xml")

context(Resolver)
private suspend fun AssetImageSource.isSvg(): Boolean =
isSvg(peek(request.context).source())
private suspend fun AssetImageSource.canBeSubSampled(): Boolean =
canBeSubSampled(peek(request.context).source())

context(Resolver)
@SuppressLint("Recycle")
private suspend fun UriImageSource.isSvg(): Boolean =
isSvg(peek(request.context).source())
private suspend fun UriImageSource.canBeSubSampled(): Boolean =
canBeSubSampled(peek(request.context).source())

private suspend fun isSvg(source: Source?): Boolean {
private suspend fun canBeSubSampled(source: Source): Boolean {
return withContext(Dispatchers.IO) {
source?.buffer()?.use(DecodeUtils::isSvg) == true
source.buffer().use {
DecodeUtils.isSvg(it) || DecodeUtils.isGif(it)
}
}
}

0 comments on commit 483bf05

Please sign in to comment.