AnimatedWebPDecoder POC (#2426)
This commit is contained in:
1 parent
6edc8f06f3
commit
bd11a7fcc2
14 files changed
+373
-5
No files matched your search
Vendored
+4
@@ -11,6 +11,10 @@
|
||||
public static final ** CREATOR;
|
||||
}
|
||||
|
||||
-keepclassmembers,allowobfuscation class com.hippo.ehviewer.coil.AnimatedWebPDrawable {
|
||||
java.nio.ByteBuffer source;
|
||||
}
|
||||
|
||||
# Ktor logger
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
From 07931ec2f127ce2408690cc85399ca750ca12657 Mon Sep 17 00:00:00 2001
|
||||
From: FooIbar <118464521+FooIbar@users.noreply.github.com>
|
||||
Date: Thu, 19 Jun 2025 00:03:46 +0800
|
||||
Subject: [PATCH] Disable runtime detection of NEON support
|
||||
|
||||
---
|
||||
CMakeLists.txt | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index d7e8963f..9dda3bb1 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -181,7 +181,7 @@ endif()
|
||||
|
||||
# ##############################################################################
|
||||
# Android only.
|
||||
-if(ANDROID)
|
||||
+if(0)
|
||||
include_directories(${ANDROID_NDK}/sources/android/cpufeatures)
|
||||
add_library(cpufeatures-webp STATIC
|
||||
${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c)
|
||||
--
|
||||
2.45.2
|
||||
|
||||
@@ -109,6 +109,26 @@ FetchContent_Declare(
|
||||
FetchContent_MakeAvailable(libarchive)
|
||||
include_directories(${libarchive_SOURCE_DIR}/libarchive)
|
||||
|
||||
set(LIBWEBP_PATCH ${CMAKE_CURRENT_LIST_DIR}/0001-Disable-runtime-detection-of-NEON-support.patch)
|
||||
FetchContent_Declare(
|
||||
libwebp
|
||||
GIT_REPOSITORY https://chromium.googlesource.com/webm/libwebp
|
||||
GIT_TAG v1.5.0
|
||||
GIT_SHALLOW 1
|
||||
PATCH_COMMAND git apply --check -R ${LIBWEBP_PATCH} || git apply ${LIBWEBP_PATCH}
|
||||
)
|
||||
|
||||
option(WEBP_BUILD_ANIM_UTILS OFF)
|
||||
option(WEBP_BUILD_CWEBP OFF)
|
||||
option(WEBP_BUILD_DWEBP OFF)
|
||||
option(WEBP_BUILD_VWEBP OFF)
|
||||
option(WEBP_BUILD_WEBPINFO OFF)
|
||||
option(WEBP_BUILD_LIBWEBPMUX OFF)
|
||||
option(WEBP_BUILD_EXTRAS OFF)
|
||||
option(WEBP_ENABLE_SWAP_16BIT_CSP ON)
|
||||
|
||||
FetchContent_MakeAvailable(libwebp)
|
||||
|
||||
FetchContent_Declare(
|
||||
Corrosion
|
||||
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
|
||||
@@ -123,6 +143,7 @@ corrosion_import_crate(
|
||||
FEATURES $<IF:$<VERSION_GREATER_EQUAL:${ANDROID_NATIVE_API_LEVEL},26>,android-26,android>
|
||||
FLAGS $<$<NOT:$<CONFIG:Debug>>:-Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort>
|
||||
)
|
||||
corrosion_link_libraries(ehviewer_rust webp webpdemux)
|
||||
corrosion_add_target_local_rustflags(ehviewer_rust "-Clink-args=-Wl,-z,max-page-size=16384")
|
||||
|
||||
# Build and link our app's native lib
|
||||
|
||||
@@ -127,6 +127,7 @@ object Settings : DataStorePreferences(null) {
|
||||
val animateItems = boolPref("animate_items", true)
|
||||
val enableCronet = boolPref("enable_cronet", true)
|
||||
val enableQuic = boolPref("enable_quic", true)
|
||||
val webpDecoder = boolPref("webp_decoder", false)
|
||||
val desktopSite = boolPref("desktop_site", true)
|
||||
val displayName = stringOrNullPref("display_name", null)
|
||||
val avatar = stringOrNullPref("avatar", null)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import coil3.ImageLoader
|
||||
import coil3.asImage
|
||||
import coil3.decode.ByteBufferMetadata
|
||||
import coil3.decode.ContentMetadata
|
||||
import coil3.decode.DecodeResult
|
||||
import coil3.decode.DecodeUtils
|
||||
import coil3.decode.Decoder
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.gif.isAnimatedWebP
|
||||
import coil3.request.Options
|
||||
import java.io.FileInputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import okio.FileSystem
|
||||
|
||||
class AnimatedWebPDecoder(private val source: ByteBuffer) : Decoder {
|
||||
override suspend fun decode() = DecodeResult(AnimatedWebPDrawable(source).asImage(), false)
|
||||
|
||||
object Factory : Decoder.Factory {
|
||||
override fun create(
|
||||
result: SourceFetchResult,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader,
|
||||
) = if (DecodeUtils.isAnimatedWebP(result.source.source())) {
|
||||
result.source.toByteBufferOrNull()?.let { AnimatedWebPDecoder(it) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ImageSource.toByteBufferOrNull(): ByteBuffer? {
|
||||
if (fileSystem === FileSystem.SYSTEM) {
|
||||
val file = fileOrNull()
|
||||
if (file != null) {
|
||||
return file.toFile().inputStream().mapReadOnly()
|
||||
}
|
||||
}
|
||||
return when (val metadata = metadata) {
|
||||
is ContentMetadata -> metadata.assetFileDescriptor.createInputStream().mapReadOnly()
|
||||
is ByteBufferMetadata -> metadata.byteBuffer
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun FileInputStream.mapReadOnly(): ByteBuffer = channel.use { it.map(FileChannel.MapMode.READ_ONLY, 0, it.size()) }
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.core.graphics.createBitmap
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.time.TimeSource
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
// Hold a reference to the buffer as it's used by the decoder
|
||||
@Suppress("CanBeParameter")
|
||||
class AnimatedWebPDrawable(private val source: ByteBuffer) : Drawable(), Animatable {
|
||||
private val decodeScope = CoroutineScope(Dispatchers.IO.limitedParallelism(1))
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG or Paint.FILTER_BITMAP_FLAG)
|
||||
private val decoder = nativeCreateDecoder(source)
|
||||
private val width: Int
|
||||
private val height: Int
|
||||
private val loopCount: Int
|
||||
|
||||
private var decodeJob: Job? = null
|
||||
private var loopsCompleted = 0
|
||||
private var frameDuration: Int
|
||||
private var currentFrame: Frame
|
||||
private var nextFrame: Frame
|
||||
|
||||
init {
|
||||
check(decoder != 0L) { "Failed to create decoder" }
|
||||
val packed = nativeGetImageInfo(decoder)
|
||||
width = (packed shr 40).toInt()
|
||||
height = (packed shr 16 and 0xFFFFFF).toInt()
|
||||
loopCount = (packed and 0xFFFF).toInt()
|
||||
val bitmap = createBitmap(width, height)
|
||||
val timestamp = nativeDecodeNextFrame(decoder, false, bitmap)
|
||||
check(timestamp != 0) {
|
||||
nativeDestroyDecoder(decoder)
|
||||
"Failed to decode first frame"
|
||||
}
|
||||
frameDuration = timestamp
|
||||
currentFrame = Frame(bitmap, timestamp)
|
||||
nextFrame = Frame(createBitmap(width, height), 0)
|
||||
}
|
||||
|
||||
override fun getIntrinsicWidth() = width
|
||||
|
||||
override fun getIntrinsicHeight() = height
|
||||
|
||||
private fun decodeNextFrame(reset: Boolean) {
|
||||
nextFrame.timestamp = nativeDecodeNextFrame(decoder, reset, nextFrame.bitmap)
|
||||
if (nextFrame.timestamp == 0) {
|
||||
throw CancellationException("Failed to decode next frame")
|
||||
}
|
||||
nextFrame.bitmap.prepareToDraw()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
if (decodeJob?.isCompleted == true && nextFrame.timestamp != 0) {
|
||||
val timeMark = TimeSource.Monotonic.markNow()
|
||||
frameDuration = if (nextFrame.timestamp > currentFrame.timestamp) {
|
||||
nextFrame.timestamp - currentFrame.timestamp
|
||||
} else {
|
||||
loopsCompleted++
|
||||
nextFrame.timestamp
|
||||
}
|
||||
currentFrame = nextFrame.also { nextFrame = currentFrame }
|
||||
decodeJob = if (loopCount == 0 || loopsCompleted < loopCount) {
|
||||
decodeScope.launch {
|
||||
decodeNextFrame(false)
|
||||
delay(frameDuration - timeMark.elapsedNow().inWholeMilliseconds)
|
||||
invalidateSelf()
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
canvas.drawBitmap(currentFrame.bitmap, null, bounds, paint)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
if (alpha != paint.alpha) {
|
||||
paint.alpha = alpha
|
||||
invalidateSelf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAlpha(): Int = paint.alpha
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
if (colorFilter != paint.colorFilter) {
|
||||
paint.colorFilter = colorFilter
|
||||
invalidateSelf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getColorFilter(): ColorFilter? = paint.colorFilter
|
||||
|
||||
override fun isRunning() = decodeJob != null
|
||||
|
||||
override fun start() {
|
||||
if (decodeJob == null) {
|
||||
val timeMark = TimeSource.Monotonic.markNow()
|
||||
loopsCompleted = 0
|
||||
decodeJob = decodeScope.launch {
|
||||
val isFirstFrame = currentFrame.timestamp == frameDuration
|
||||
decodeNextFrame(!isFirstFrame)
|
||||
delay(frameDuration - timeMark.elapsedNow().inWholeMilliseconds)
|
||||
invalidateSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
decodeJob?.cancel()
|
||||
decodeJob = null
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
nativeDestroyDecoder(decoder)
|
||||
}
|
||||
}
|
||||
|
||||
private class Frame(val bitmap: Bitmap, var timestamp: Int)
|
||||
|
||||
private external fun nativeCreateDecoder(source: ByteBuffer): Long
|
||||
private external fun nativeGetImageInfo(decoder: Long): Long
|
||||
private external fun nativeDecodeNextFrame(decoder: Long, reset: Boolean, bitmap: Bitmap): Int
|
||||
private external fun nativeDestroyDecoder(decoder: Long)
|
||||
@@ -37,6 +37,8 @@ import coil3.size.Precision
|
||||
import coil3.size.Size
|
||||
import coil3.size.SizeResolver
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.coil.AnimatedWebPDecoder
|
||||
import com.hippo.ehviewer.coil.AnimatedWebPDrawable
|
||||
import com.hippo.ehviewer.coil.BitmapImageWithExtraInfo
|
||||
import com.hippo.ehviewer.coil.detectQrCode
|
||||
import com.hippo.ehviewer.coil.hardwareThreshold
|
||||
@@ -79,7 +81,10 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
|
||||
|
||||
private fun recycle() {
|
||||
when (val image = innerImage!!) {
|
||||
is DrawableImage -> src.close()
|
||||
is DrawableImage -> {
|
||||
(image.drawable as? AnimatedWebPDrawable)?.dispose()
|
||||
src.close()
|
||||
}
|
||||
is BitmapImage -> image.bitmap.recycle()
|
||||
}
|
||||
innerImage = null
|
||||
@@ -101,6 +106,9 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
|
||||
maybeCropBorder(Settings.cropBorder.value)
|
||||
detectQrCode(checkExtraneousAds)
|
||||
memoryCachePolicy(CachePolicy.DISABLED)
|
||||
if (Settings.webpDecoder.value) {
|
||||
decoderFactory(AnimatedWebPDecoder.Factory)
|
||||
}
|
||||
}
|
||||
}
|
||||
return when (val result = request.execute()) {
|
||||
|
||||
@@ -223,6 +223,12 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
|
||||
}
|
||||
}
|
||||
}
|
||||
var webpDecoder by Settings.webpDecoder.asMutableState()
|
||||
SwitchPref(
|
||||
checked = webpDecoder,
|
||||
onMutate = { webpDecoder = !webpDecoder },
|
||||
title = "Experimental animated WebP decoder",
|
||||
)
|
||||
if (isAtLeastO) {
|
||||
IntSliderPreference(
|
||||
maxValue = 16384,
|
||||
|
||||
Generated
+13
@@ -273,6 +273,7 @@ dependencies = [
|
||||
"image",
|
||||
"jni",
|
||||
"jni_fn",
|
||||
"libwebp-sys2",
|
||||
"log",
|
||||
"ndk",
|
||||
"quick-xml",
|
||||
@@ -877,6 +878,18 @@ version = "0.2.174"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
|
||||
[[package]]
|
||||
name = "libwebp-sys2"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/FooIbar/libwebp-sys2-rs.git#138c7114eb7cc9c3d0a6c5d074d1dda30550b765"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.9.4"
|
||||
|
||||
@@ -27,6 +27,7 @@ anyhow = "1.0"
|
||||
image = { default-features = false, version = "0.25" }
|
||||
rxing = { default-features = false, version = "0.7" }
|
||||
ndk = { version = "0.9", optional = true }
|
||||
libwebp-sys2 = { git = "https://github.com/FooIbar/libwebp-sys2-rs.git", features = ["1_5", "demux", "extern-types"] }
|
||||
|
||||
[dev-dependencies]
|
||||
reqwest = "0.12"
|
||||
|
||||
@@ -4,15 +4,16 @@ use super::jvm::jni_throwing;
|
||||
use crate::img::border::DetectBorder;
|
||||
use crate::img::core::{CustomPixel, ImageConsumer, Rgb565, Rgba8888, RgbaF16};
|
||||
use crate::img::qr_code::QrCode;
|
||||
use crate::img::webp::{decode_next_frame, get_image_info};
|
||||
use anyhow::{Ok, Result, anyhow};
|
||||
use image::ImageBuffer;
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::JClass;
|
||||
use jni::sys::jboolean;
|
||||
use jni::sys::{jintArray, jobject};
|
||||
use jni::sys::{jboolean, jint, jintArray, jlong, jobject};
|
||||
use jni_fn::jni_fn;
|
||||
use libwebp_sys::WebPAnimDecoder;
|
||||
use ndk::bitmap::{Bitmap, BitmapFormat};
|
||||
use std::ptr::slice_from_raw_parts;
|
||||
use std::ptr::{copy_nonoverlapping, slice_from_raw_parts};
|
||||
|
||||
#[jni_fn("com.hippo.ehviewer.image.ImageKt")]
|
||||
pub fn detectBorder(mut env: JNIEnv, _class: JClass, object: jobject) -> jintArray {
|
||||
@@ -31,6 +32,29 @@ pub fn hasQrCode(mut env: JNIEnv, _class: JClass, object: jobject) -> jboolean {
|
||||
})
|
||||
}
|
||||
|
||||
#[jni_fn("com.hippo.ehviewer.coil.AnimatedWebPDrawableKt")]
|
||||
pub fn nativeDecodeNextFrame(
|
||||
mut env: JNIEnv,
|
||||
_: JClass,
|
||||
decoder: jlong,
|
||||
reset: jboolean,
|
||||
bitmap: jobject,
|
||||
) -> jint {
|
||||
jni_throwing(&mut env, |env| {
|
||||
let dec = decoder as *mut WebPAnimDecoder;
|
||||
let (buf, timestamp) = unsafe { decode_next_frame(dec, reset != 0) };
|
||||
if !buf.is_null() {
|
||||
let info = unsafe { get_image_info(dec) };
|
||||
let handle = unsafe { Bitmap::from_jni(env.get_raw(), bitmap) };
|
||||
let dst = handle.lock_pixels()? as *mut u8;
|
||||
let size = info.canvas_width * info.canvas_height * 4;
|
||||
unsafe { copy_nonoverlapping(buf, dst, size as usize) };
|
||||
handle.unlock_pixels()?;
|
||||
}
|
||||
Ok(timestamp)
|
||||
})
|
||||
}
|
||||
|
||||
fn ptr_as_image<'local, P: CustomPixel>(
|
||||
ptr: *const !,
|
||||
w: u32,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![cfg(feature = "jvm")]
|
||||
|
||||
use crate::EhError;
|
||||
use crate::img::webp::{create_decoder, get_image_info, pack_image_info};
|
||||
use crate::parser::api::parse_vote_tag;
|
||||
use crate::parser::archive::{parse_archive_url, parse_archives, parse_archives_with_funds};
|
||||
use crate::parser::config::parse_fav_cat;
|
||||
@@ -14,9 +15,10 @@ use crate::parser::torrent::parse_torrent_list;
|
||||
use android_logger::Config;
|
||||
use anyhow::{Context, Result, ensure};
|
||||
use jni::objects::{JByteBuffer, JClass};
|
||||
use jni::sys::{JNI_TRUE, JNI_VERSION_1_6, jboolean, jint, jobject};
|
||||
use jni::sys::{JNI_TRUE, JNI_VERSION_1_6, jboolean, jint, jlong, jobject};
|
||||
use jni::{JNIEnv, JavaVM};
|
||||
use jni_fn::jni_fn;
|
||||
use libwebp_sys::{WebPAnimDecoder, WebPAnimDecoderDelete};
|
||||
use log::LevelFilter;
|
||||
use serde::Serialize;
|
||||
use std::ffi::c_void;
|
||||
@@ -137,6 +139,32 @@ pub fn parseProfile(mut env: JNIEnv, _class: JClass, buffer: JByteBuffer, limit:
|
||||
})
|
||||
}
|
||||
|
||||
#[jni_fn("com.hippo.ehviewer.coil.AnimatedWebPDrawableKt")]
|
||||
pub fn nativeCreateDecoder(mut env: JNIEnv, _: JClass, buffer: JByteBuffer) -> jlong {
|
||||
jni_throwing(&mut env, |env| {
|
||||
let buffer = deref_mut_direct_bytebuffer(env, buffer)?;
|
||||
Ok(create_decoder(buffer) as i64)
|
||||
})
|
||||
}
|
||||
|
||||
#[jni_fn("com.hippo.ehviewer.coil.AnimatedWebPDrawableKt")]
|
||||
pub fn nativeGetImageInfo(mut env: JNIEnv, _: JClass, decoder: jlong) -> jlong {
|
||||
jni_throwing(&mut env, |_| {
|
||||
let dec = decoder as *const WebPAnimDecoder;
|
||||
let info = unsafe { get_image_info(dec) };
|
||||
Ok(pack_image_info(info) as i64)
|
||||
})
|
||||
}
|
||||
|
||||
#[jni_fn("com.hippo.ehviewer.coil.AnimatedWebPDrawableKt")]
|
||||
pub fn nativeDestroyDecoder(mut env: JNIEnv, _: JClass, decoder: jlong) {
|
||||
jni_throwing(&mut env, |_| {
|
||||
let dec = decoder as *mut WebPAnimDecoder;
|
||||
unsafe { WebPAnimDecoderDelete(dec) };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn deref_mut_direct_bytebuffer<'local>(
|
||||
env: &JNIEnv,
|
||||
buffer: JByteBuffer<'local>,
|
||||
@@ -170,6 +198,12 @@ impl ThrowingHasDefault for i32 {
|
||||
}
|
||||
}
|
||||
|
||||
impl ThrowingHasDefault for i64 {
|
||||
fn default() -> Self {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl ThrowingHasDefault for () {
|
||||
fn default() -> Self {}
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ pub mod border;
|
||||
pub mod copy_region;
|
||||
pub mod core;
|
||||
pub mod qr_code;
|
||||
pub mod webp;
|
||||
@@ -0,0 +1,42 @@
|
||||
use libwebp_sys::{
|
||||
MODE_rgbA, WebPAnimDecoder, WebPAnimDecoderGetInfo, WebPAnimDecoderGetNext,
|
||||
WebPAnimDecoderHasMoreFrames, WebPAnimDecoderNew, WebPAnimDecoderOptions, WebPAnimDecoderReset,
|
||||
WebPAnimInfo, WebPData,
|
||||
};
|
||||
use std::mem::zeroed;
|
||||
use std::ptr::null_mut;
|
||||
|
||||
pub fn create_decoder(data: &[u8]) -> *mut WebPAnimDecoder {
|
||||
let webp_data = WebPData {
|
||||
bytes: data.as_ptr(),
|
||||
size: data.len(),
|
||||
};
|
||||
let dec_options = WebPAnimDecoderOptions {
|
||||
color_mode: MODE_rgbA,
|
||||
use_threads: 1,
|
||||
padding: [0; 7],
|
||||
};
|
||||
unsafe { WebPAnimDecoderNew(&webp_data, &dec_options) }
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn get_image_info(dec: *const WebPAnimDecoder) -> WebPAnimInfo {
|
||||
let mut info = unsafe { zeroed() };
|
||||
let _ = unsafe { WebPAnimDecoderGetInfo(dec, &mut info) };
|
||||
info
|
||||
}
|
||||
|
||||
pub fn pack_image_info(info: WebPAnimInfo) -> u64 {
|
||||
(info.canvas_width as u64) << 40 | (info.canvas_height as u64) << 16 | (info.loop_count as u64)
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn decode_next_frame(dec: *mut WebPAnimDecoder, reset: bool) -> (*const u8, i32) {
|
||||
let mut buf = null_mut();
|
||||
let mut timestamp = 0;
|
||||
unsafe {
|
||||
if reset || WebPAnimDecoderHasMoreFrames(dec) == 0 {
|
||||
WebPAnimDecoderReset(dec);
|
||||
}
|
||||
let _ = WebPAnimDecoderGetNext(dec, &mut buf, &mut timestamp);
|
||||
}
|
||||
(buf, timestamp)
|
||||
}
|
||||
Reference in new issue
Block a user