Revert "Rewrite the GIF frame delay if it's below a threshold (coil-kt/coil#783) (#672)"

This reverts commit 0ebb55551c.
This commit is contained in:
tarsin committed 2023-01-16 11:18:06 +08:00
1 parent b1b10298b4
commit beb132d604
3 files changed
+3 -179

No files matched your search

@@ -1,66 +0,0 @@
@file:JvmName("GifDecodeUtils")
package coil.decode
import okio.BufferedSource
import okio.ByteString.Companion.encodeUtf8
import kotlin.experimental.and
// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
private val GIF_HEADER_87A = "GIF87a".encodeUtf8()
private val GIF_HEADER_89A = "GIF89a".encodeUtf8()
// https://developers.google.com/speed/webp/docs/riff_container
private val WEBP_HEADER_RIFF = "RIFF".encodeUtf8()
private val WEBP_HEADER_WEBP = "WEBP".encodeUtf8()
private val WEBP_HEADER_VPX8 = "VP8X".encodeUtf8()
// https://nokiatech.github.io/heif/technical.html
private val HEIF_HEADER_FTYP = "ftyp".encodeUtf8()
private val HEIF_HEADER_MSF1 = "msf1".encodeUtf8()
private val HEIF_HEADER_HEVC = "hevc".encodeUtf8()
private val HEIF_HEADER_HEVX = "hevx".encodeUtf8()
/**
* Return 'true' if the [source] contains a GIF image. The [source] is not consumed.
*/
fun DecodeUtils.isGif(source: BufferedSource): Boolean {
return source.rangeEquals(0, GIF_HEADER_89A) ||
source.rangeEquals(0, GIF_HEADER_87A)
}
/**
* Return 'true' if the [source] contains a WebP image. The [source] is not consumed.
*/
fun DecodeUtils.isWebP(source: BufferedSource): Boolean {
return source.rangeEquals(0, WEBP_HEADER_RIFF) &&
source.rangeEquals(8, WEBP_HEADER_WEBP)
}
/**
* Return 'true' if the [source] contains an animated WebP image. The [source] is not consumed.
*/
fun DecodeUtils.isAnimatedWebP(source: BufferedSource): Boolean {
return isWebP(source) &&
source.rangeEquals(12, WEBP_HEADER_VPX8) &&
source.request(17) &&
(source.buffer[16] and 0b00000010) > 0
}
/**
* Return 'true' if the [source] contains an HEIF image. The [source] is not consumed.
*/
fun DecodeUtils.isHeif(source: BufferedSource): Boolean {
return source.rangeEquals(4, HEIF_HEADER_FTYP)
}
/**
* Return 'true' if the [source] contains an animated HEIF image sequence. The [source] is not
* consumed.
*/
fun DecodeUtils.isAnimatedHeif(source: BufferedSource): Boolean {
return isHeif(source) &&
(source.rangeEquals(8, HEIF_HEADER_MSF1) ||
source.rangeEquals(8, HEIF_HEADER_HEVC) ||
source.rangeEquals(8, HEIF_HEADER_HEVX))
}
@@ -1,88 +0,0 @@
@file:Suppress("SameParameterValue")
package coil.decode
import okio.Buffer
import okio.ByteString
import okio.ByteString.Companion.decodeHex
import okio.ForwardingSource
import okio.Source
/**
* A [ForwardingSource] that rewrites the GIF frame delay in every graphics control block if it's
* below a threshold.
*/
internal class FrameDelayRewritingSource(delegate: Source) : ForwardingSource(delegate) {
// An intermediary buffer so we can read and alter the data before it's written to the destination.
private val buffer = Buffer()
override fun read(sink: Buffer, byteCount: Long): Long {
// Ensure our buffer has enough bytes to satisfy this read.
request(byteCount)
// Short circuit if there are no bytes in the buffer.
if (buffer.size == 0L) {
return if (byteCount == 0L) 0L else -1L
}
// Search through the buffer and rewrite any frame delays below the threshold.
var bytesWritten = 0L
while (true) {
val index = indexOf(FRAME_DELAY_START_MARKER)
if (index == -1L) break
// Write up until the end of the frame delay start marker.
bytesWritten += write(sink, index + FRAME_DELAY_START_MARKER_SIZE_BYTES)
// Check for the end of the graphics control extension block.
if (!request(5) || buffer[4] != 0.toByte()) continue
// Rewrite the frame delay if it is below the threshold.
// The frame delay is stored as two unsigned bytes in reverse order
// (i.e. the most significant bits are in the byte that's read second).
val frameDelay = (buffer[2].toUByte().toInt() shl 8) or buffer[1].toUByte().toInt()
if (frameDelay < MINIMUM_FRAME_DELAY) {
sink.writeByte(buffer[0].toInt())
sink.writeByte(DEFAULT_FRAME_DELAY)
sink.writeByte(0)
buffer.skip(3)
}
}
// Write anything left in the source.
if (bytesWritten < byteCount) {
bytesWritten += write(sink, byteCount - bytesWritten)
}
return if (bytesWritten == 0L) -1 else bytesWritten
}
private fun indexOf(bytes: ByteString): Long {
var index = -1L
while (true) {
index = buffer.indexOf(bytes[0], index + 1)
if (index == -1L) break
if (request(bytes.size.toLong()) && buffer.rangeEquals(index, bytes)) break
}
return index
}
private fun write(sink: Buffer, byteCount: Long): Long {
return buffer.read(sink, byteCount).coerceAtLeast(0)
}
private fun request(byteCount: Long): Boolean {
if (buffer.size >= byteCount) return true
val toRead = byteCount - buffer.size
return super.read(buffer, toRead) == toRead
}
private companion object {
// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
// See: "Graphics Control Extension"
private val FRAME_DELAY_START_MARKER = "0021F904".decodeHex()
private const val FRAME_DELAY_START_MARKER_SIZE_BYTES = 4
private const val MINIMUM_FRAME_DELAY = 2
private const val DEFAULT_FRAME_DELAY = 10
}
}
+3 -25
View File
@@ -25,15 +25,8 @@ import android.graphics.ImageDecoder.Source
import android.graphics.drawable.Animatable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import coil.decode.DecodeUtils
import coil.decode.FrameDelayRewritingSource
import coil.decode.isGif
import com.hippo.UriArchiveAccessor
import com.hippo.ehviewer.EhApplication
import okio.Buffer
import okio.BufferedSource
import okio.buffer
import okio.source
import java.io.FileInputStream
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
@@ -87,35 +80,20 @@ class Image private constructor(source: Source, private val byteBuffer: ByteBuff
@Throws(DecodeException::class)
@JvmStatic
fun decode(stream: FileInputStream): Image {
val source = stream.source().buffer()
val buffer = if (DecodeUtils.isGif(source)) {
rewriteSource(source)
} else {
val src = ImageDecoder.createSource(
stream.channel.map(
FileChannel.MapMode.READ_ONLY, 0,
stream.available().toLong()
)
}
val src = ImageDecoder.createSource(buffer)
)
return Image(src)
}
@Throws(DecodeException::class)
@JvmStatic
fun decode(buffer: ByteBuffer): Image {
val source = Buffer().apply { write(buffer) }
val byteBuffer = if (DecodeUtils.isGif(source)) {
rewriteSource(source)
} else {
buffer.position(0) as ByteBuffer
}
val src = ImageDecoder.createSource(byteBuffer)
val src = ImageDecoder.createSource(buffer)
return Image(src, buffer)
}
private fun rewriteSource(source: BufferedSource): ByteBuffer {
val bufferedSource = FrameDelayRewritingSource(source).buffer()
return ByteBuffer.wrap(bufferedSource.use { it.readByteArray() })
}
}
}