diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index ac453646e..42cb3e2ef 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -45,6 +45,6 @@ SET(LIBARCHIVE_CUSTOM_LIBS "liblzma" "nettle") add_subdirectory(libarchive) # Build and link our app's native lib -add_library(${PROJECT_NAME} SHARED archive.c fdutils.c gifUtils.c) +add_library(${PROJECT_NAME} SHARED archive.c fdutils.c) target_include_directories(${PROJECT_NAME} PUBLIC libarchive/libarchive) target_link_libraries(${PROJECT_NAME} archive_static log -Wl,--gc-sections) diff --git a/app/src/main/cpp/gifUtils.c b/app/src/main/cpp/gifUtils.c deleted file mode 100644 index 5e5899648..000000000 --- a/app/src/main/cpp/gifUtils.c +++ /dev/null @@ -1,11 +0,0 @@ -#include -#include - -#define GIF_HEADER_87A "GIF87a" -#define GIF_HEADER_89A "GIF89a" - -JNIEXPORT jboolean JNICALL -Java_com_hippo_Native_isGif(JNIEnv *env, jobject thiz, jobject buffer) { - void *addr = (*env)->GetDirectBufferAddress(env, buffer); - return !memcmp(addr, GIF_HEADER_87A, 6) || !memcmp(addr, GIF_HEADER_89A, 6); -} diff --git a/app/src/main/java/coil/decode/DecodeUtils.kt b/app/src/main/java/coil/decode/DecodeUtils.kt new file mode 100644 index 000000000..0d530af66 --- /dev/null +++ b/app/src/main/java/coil/decode/DecodeUtils.kt @@ -0,0 +1,66 @@ +@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)) +} diff --git a/app/src/main/java/com/hippo/Native.kt b/app/src/main/java/com/hippo/Native.kt index cf2e35d72..66c98888d 100644 --- a/app/src/main/java/com/hippo/Native.kt +++ b/app/src/main/java/com/hippo/Native.kt @@ -18,7 +18,6 @@ package com.hippo import java.io.FileDescriptor -import java.nio.ByteBuffer object Native { fun initialize() { @@ -27,6 +26,4 @@ object Native { @JvmStatic external fun getFd(fd: FileDescriptor?): Int - - external fun isGif(buffer: ByteBuffer): Boolean } \ No newline at end of file diff --git a/app/src/main/java/com/hippo/image/Image.kt b/app/src/main/java/com/hippo/image/Image.kt index 36e52b750..f2fb26eff 100644 --- a/app/src/main/java/com/hippo/image/Image.kt +++ b/app/src/main/java/com/hippo/image/Image.kt @@ -25,8 +25,9 @@ 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 com.hippo.Native +import coil.decode.isGif import com.hippo.UriArchiveAccessor import com.hippo.ehviewer.EhApplication import okio.Buffer @@ -86,41 +87,35 @@ class Image private constructor(source: Source, private val byteBuffer: ByteBuff @Throws(DecodeException::class) @JvmStatic fun decode(stream: FileInputStream): Image { - val buffer = stream.channel.map( - FileChannel.MapMode.READ_ONLY, 0, - stream.available().toLong() - ) - val source = if (checkIsGif(buffer)) { - rewriteSource(stream.source().buffer()) + val source = stream.source().buffer() + val buffer = if (DecodeUtils.isGif(source)) { + rewriteSource(source) } else { - buffer + stream.channel.map( + FileChannel.MapMode.READ_ONLY, 0, + stream.available().toLong() + ) } - val src = ImageDecoder.createSource(source) + val src = ImageDecoder.createSource(buffer) return Image(src) } @Throws(DecodeException::class) @JvmStatic fun decode(buffer: ByteBuffer): Image { - return if (checkIsGif(buffer)) { - val rewritten = rewriteSource(Buffer().apply { - write(buffer) - UriArchiveAccessor.releaseByteBuffer(buffer) - }) - Image(ImageDecoder.createSource(rewritten)) + val source = Buffer().apply { write(buffer) } + val byteBuffer = if (DecodeUtils.isGif(source)) { + rewriteSource(source) } else { - Image(ImageDecoder.createSource(buffer), buffer) + buffer.position(0) as ByteBuffer } + val src = ImageDecoder.createSource(byteBuffer) + return Image(src, buffer) } private fun rewriteSource(source: BufferedSource): ByteBuffer { val bufferedSource = FrameDelayRewritingSource(source).buffer() return ByteBuffer.wrap(bufferedSource.use { it.readByteArray() }) } - - private fun checkIsGif(buffer: ByteBuffer): Boolean { - check(buffer.isDirect) - return Native.isGif(buffer) - } } } \ No newline at end of file