diff --git a/dotlottie-rs/src/dotlottie_player.rs b/dotlottie-rs/src/dotlottie_player.rs index 55d4d664..1cdda14b 100644 --- a/dotlottie-rs/src/dotlottie_player.rs +++ b/dotlottie-rs/src/dotlottie_player.rs @@ -1,6 +1,6 @@ use crate::time::{Duration, Instant}; use std::sync::RwLock; -use std::{fs, rc::Rc, sync::Arc}; +use std::{fs, rc::Rc, sync::Arc, ffi::c_void}; use crate::actions::open_url_policy::OpenUrlPolicy; use crate::state_machine_engine::events::Event; @@ -839,10 +839,39 @@ impl DotLottieRuntime { ) }; + let manager_ptr = manager as *const DotLottieManager as *mut c_void; + + unsafe extern "C" fn asset_resolver_callback( + src: *const i8, + user_data: *mut c_void + ) -> *mut Vec { + let src_str = match std::ffi::CStr::from_ptr(src).to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + + if user_data.is_null() { + return std::ptr::null_mut(); + } + + let manager = &*(user_data as *const DotLottieManager); + match manager.resolve_asset(src_str) { + Ok(asset_data) => { + Box::into_raw(Box::new(asset_data)) + } + Err(_) => { + std::ptr::null_mut() + } + } + } + if let Ok(animation_data) = active_animation { self.markers = extract_markers(animation_data.as_str()); let animation_loaded = self.load_animation_common( - |renderer, w, h| renderer.load_data(&animation_data, w, h), + |renderer, w, h| { + renderer.set_asset_resolver(Some(asset_resolver_callback), manager_ptr)?; + renderer.load_data(&animation_data, w, h) + }, width, height, ); diff --git a/dotlottie-rs/src/fms/mod.rs b/dotlottie-rs/src/fms/mod.rs index 2dd3ba7a..847fae5c 100644 --- a/dotlottie-rs/src/fms/mod.rs +++ b/dotlottie-rs/src/fms/mod.rs @@ -9,13 +9,7 @@ use std::cell::RefCell; use std::io::{self, Read}; use zip::ZipArchive; -const BASE64_CHARS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - const DATA_IMAGE_PREFIX: &str = "data:image/"; -const DATA_FONT_PREFIX: &str = "data:font/"; -const BASE64_PREFIX: &str = ";base64,"; -const DEFAULT_EXT: &str = "png"; -const DEFAULT_FONT_EXT: &str = "ttf"; pub struct DotLottieManager { active_animation_id: Box, @@ -91,95 +85,44 @@ impl DotLottieManager { .get_mut("assets") .and_then(|v| v.as_array_mut()) { - let image_prefix = if self.version == 2 { "i/" } else { "images/" }; - let mut asset_path = String::with_capacity(128); // Larger initial capacity - let embedded_flag = Value::Number(1.into()); - let empty_u = Value::String(String::new()); for asset in assets.iter_mut() { if let Some(asset_obj) = asset.as_object_mut() { if let Some(p_str) = asset_obj.get("p").and_then(|v| v.as_str()) { if p_str.starts_with(DATA_IMAGE_PREFIX) { asset_obj.insert("e".to_string(), embedded_flag.clone()); - } else { - asset_path.clear(); - asset_path.push_str(image_prefix); - asset_path.push_str(p_str.trim_matches('"')); - - if let Ok(mut result) = archive.by_name(&asset_path) { - let mut content = Vec::with_capacity(result.size() as usize); - if result.read_to_end(&mut content).is_ok() { - let image_ext = p_str - .rfind('.') - .map(|i| &p_str[i + 1..]) - .unwrap_or(DEFAULT_EXT); - let image_data_base64 = Self::encode_base64(&content); - - let data_url = format!( - "{DATA_IMAGE_PREFIX}{image_ext}{BASE64_PREFIX}{image_data_base64}" - ); - - asset_obj.insert("u".to_string(), empty_u.clone()); - asset_obj.insert("p".to_string(), Value::String(data_url)); - asset_obj.insert("e".to_string(), embedded_flag.clone()); - } - } } } } } } - if self.version == 2 { - if let Some(fonts) = lottie_animation - .get_mut("fonts") - .and_then(|v| v.as_object_mut()) - { - if let Some(font_list) = fonts.get_mut("list").and_then(|v| v.as_array_mut()) { - let mut font_path = String::with_capacity(128); - - for font in font_list.iter_mut() { - if let Some(font_obj) = font.as_object_mut() { - if let Some(f_path_str) = font_obj.get("fPath").and_then(|v| v.as_str()) - { - // only process fonts with /f/ prefix (package-internal fonts) - if f_path_str.starts_with("/f/") { - font_path.clear(); - font_path.push_str("f/"); - let path_without_prefix = - f_path_str.strip_prefix("/f/").unwrap_or(f_path_str); - font_path.push_str(path_without_prefix); - - if let Ok(mut result) = archive.by_name(&font_path) { - let mut content = - Vec::with_capacity(result.size() as usize); - if result.read_to_end(&mut content).is_ok() { - let font_ext = path_without_prefix - .rfind('.') - .map(|i| &path_without_prefix[i + 1..]) - .unwrap_or(DEFAULT_FONT_EXT); - let font_data_base64 = Self::encode_base64(&content); - - let data_url = format!( - "{DATA_FONT_PREFIX}{font_ext}{BASE64_PREFIX}{font_data_base64}" - ); - - font_obj.insert( - "fPath".to_string(), - Value::String(data_url), - ); - } - } - } - } - } - } - } + serde_json::to_string(&lottie_animation).map_err(|_| DotLottieError::ReadContentError) + } + + pub fn resolve_asset(&self, asset_path: &str) -> Result, DotLottieError> { + let mut archive = self.archive.borrow_mut(); + + let mut asset_path = asset_path.to_string(); + if asset_path.starts_with("/f/") { + // font path handling + asset_path = format!("f/{asset_path}"); + } else { + // image path handling + let image_prefix = if self.version == 2 { "i/" } else { "images/" }; + let asset_name = asset_path.split('/').next_back().unwrap_or(""); + asset_path = format!("{image_prefix}{asset_name}"); + } + + if let Ok(mut result) = archive.by_name(&asset_path) { + let mut content = Vec::with_capacity(result.size() as usize); + if result.read_to_end(&mut content).is_ok() { + return Ok(content); } } - serde_json::to_string(&lottie_animation).map_err(|_| DotLottieError::ReadContentError) + Err(DotLottieError::FileFindError) } #[inline] @@ -208,48 +151,6 @@ impl DotLottieManager { String::from_utf8(content).map_err(|_| DotLottieError::InvalidUtf8Error) } - #[inline] - fn encode_base64(input: &[u8]) -> String { - if input.is_empty() { - return String::new(); - } - - let output_len = input.len().div_ceil(3) * 4; - let mut result = Vec::with_capacity(output_len); - - let mut i = 0; - while i + 2 < input.len() { - let b0 = input[i] as u32; - let b1 = input[i + 1] as u32; - let b2 = input[i + 2] as u32; - let n = (b0 << 16) | (b1 << 8) | b2; - - result.push(BASE64_CHARS[((n >> 18) & 63) as usize]); - result.push(BASE64_CHARS[((n >> 12) & 63) as usize]); - result.push(BASE64_CHARS[((n >> 6) & 63) as usize]); - result.push(BASE64_CHARS[(n & 63) as usize]); - i += 3; - } - - if i < input.len() { - let b0 = input[i] as u32; - let b1 = input.get(i + 1).copied().unwrap_or(0) as u32; - let n = (b0 << 16) | (b1 << 8); - - result.push(BASE64_CHARS[((n >> 18) & 63) as usize]); - result.push(BASE64_CHARS[((n >> 12) & 63) as usize]); - result.push(if i + 1 < input.len() { - BASE64_CHARS[((n >> 6) & 63) as usize] - } else { - b'=' - }); - result.push(b'='); - } - - // safe conversion from Vec to String since we only used valid ASCII - unsafe { String::from_utf8_unchecked(result) } - } - #[inline] fn read_zip_file( archive: &mut ZipArchive, @@ -294,15 +195,4 @@ mod tests { } } } - - #[test] - fn test_base64_encoding() { - let input = b"Hello, World!"; - let result = DotLottieManager::encode_base64(input); - assert_eq!(result, "SGVsbG8sIFdvcmxkIQ=="); - - let empty_input = b""; - let empty_result = DotLottieManager::encode_base64(empty_input); - assert_eq!(empty_result, ""); - } } diff --git a/dotlottie-rs/src/lottie_renderer/mod.rs b/dotlottie-rs/src/lottie_renderer/mod.rs index bcdbcfd6..99f4bb5b 100644 --- a/dotlottie-rs/src/lottie_renderer/mod.rs +++ b/dotlottie-rs/src/lottie_renderer/mod.rs @@ -12,7 +12,7 @@ pub use renderer::{Animation, ColorSpace, Drawable, Renderer, Shape}; #[cfg(feature = "tvg")] pub use thorvg::{TvgAnimation, TvgEngine, TvgError, TvgRenderer, TvgShape}; -use std::{error::Error, fmt}; +use std::{error::Error, fmt, ffi::c_void}; #[derive(Debug)] pub enum LottieRendererError { @@ -36,6 +36,8 @@ fn into_lottie(_err: R::Error) -> LottieRendererError { LottieRendererError::RendererError } +pub type AssetResolverFn = unsafe extern "C" fn(*const i8, *mut c_void) -> *mut Vec; + pub trait LottieRenderer { fn load_data(&mut self, data: &str, width: u32, height: u32) -> Result<(), LottieRendererError>; @@ -106,6 +108,12 @@ pub trait LottieRenderer { font_name: &str, font_data: &[u8], ) -> Result<(), LottieRendererError>; + + fn set_asset_resolver( + &mut self, + resolver: Option, + user_data: *mut c_void, + ) -> Result<(), LottieRendererError>; } impl dyn LottieRenderer { @@ -124,6 +132,8 @@ impl dyn LottieRenderer { buffer: vec![], layout: Layout::default(), user_transform: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], + asset_resolver: None, + asset_resolver_user_data: std::ptr::null_mut(), }) } } @@ -143,6 +153,8 @@ struct LottieRendererImpl { buffer: Vec, layout: Layout, user_transform: [f32; 9], + asset_resolver: Option, + asset_resolver_user_data: *mut c_void, } impl LottieRendererImpl { @@ -197,6 +209,10 @@ impl LottieRendererImpl { fn load_animation(&mut self, data: &str) -> Result { let mut animation = R::Animation::default(); + if let Some(resolver) = self.asset_resolver { + animation.set_asset_resolver(Some(resolver), self.asset_resolver_user_data).map_err(into_lottie::)?; + } + animation .load_data(data, "lottie+json") .map_err(into_lottie::)?; @@ -593,6 +609,16 @@ impl LottieRenderer for LottieRendererImpl { Ok(()) } + + fn set_asset_resolver( + &mut self, + resolver: Option, + user_data: *mut c_void, + ) -> Result<(), LottieRendererError> { + self.asset_resolver = resolver; + self.asset_resolver_user_data = user_data; + Ok(()) + } } #[inline] diff --git a/dotlottie-rs/src/lottie_renderer/renderer.rs b/dotlottie-rs/src/lottie_renderer/renderer.rs index bacbc713..70a436ab 100644 --- a/dotlottie-rs/src/lottie_renderer/renderer.rs +++ b/dotlottie-rs/src/lottie_renderer/renderer.rs @@ -1,4 +1,5 @@ use core::error; +use std::ffi::c_void; pub enum ColorSpace { ABGR8888, @@ -59,6 +60,12 @@ pub trait Animation: Default { fn set_quality(&mut self, quality: u8) -> Result<(), Self::Error>; + fn set_asset_resolver( + &mut self, + resolver: Option, + user_data: *mut c_void, + ) -> Result<(), Self::Error>; + fn tween( &mut self, to: f32, diff --git a/dotlottie-rs/src/lottie_renderer/thorvg.rs b/dotlottie-rs/src/lottie_renderer/thorvg.rs index 2c86ab02..7fa8b824 100644 --- a/dotlottie-rs/src/lottie_renderer/thorvg.rs +++ b/dotlottie-rs/src/lottie_renderer/thorvg.rs @@ -1,16 +1,15 @@ use crate::time::Instant; use std::{ - error::Error, - ffi::{c_char, CString}, - fmt, ptr, - result::Result, + error::Error, ffi::{CString, c_char, c_void}, fmt, ptr, result::Result }; #[cfg(feature = "tvg-ttf")] use crate::lottie_renderer::fallback_font; -use super::{Animation, ColorSpace, Drawable, Renderer, Shape}; +use super::{Animation, ColorSpace, Drawable, Renderer, Shape, AssetResolverFn}; + +const DEFAULT_EXT: &str = "png"; #[expect(non_upper_case_globals)] #[allow(non_snake_case)] @@ -431,6 +430,102 @@ impl Animation for TvgAnimation { unsafe { tvg::tvg_lottie_animation_set_quality(self.raw_animation, quality).into_result() } } + fn set_asset_resolver( + &mut self, + resolver: Option, + user_data: *mut c_void, + ) -> Result<(), TvgError> { + if let Some(asset_resolver_fn) = resolver { + // Create a struct to hold both the resolver function and user_data + let resolver_data = Box::new((asset_resolver_fn, user_data)); + let resolver_data_ptr = Box::into_raw(resolver_data) as *mut c_void; + + unsafe extern "C" fn thorvg_asset_resolver_wrapper( + paint: tvg::Tvg_Paint, + src: *const i8, + user_data: *mut c_void + ) -> bool { + if user_data.is_null() { + return false; + } + + // Extract the resolver function and original user_data + let (resolver_fn, original_user_data) = *(user_data as *const (AssetResolverFn, *mut c_void)); + + let asset_data_ptr = resolver_fn(src, original_user_data); + if asset_data_ptr.is_null() { + return false; + } + + let src_str = match std::ffi::CStr::from_ptr(src).to_str() { + Ok(s) => s, + Err(_) => return false, + }; + + let mut paint_type: tvg::Tvg_Type = 0; + unsafe { _ = tvg::tvg_paint_get_type(paint, &mut paint_type).into_result() }; + let asset_data = unsafe { Box::from_raw(asset_data_ptr) }; + + // text load + if paint_type == tvg::Tvg_Type_TVG_TYPE_TEXT { + let asset_name = src_str.split('/').next_back().unwrap_or(""); + let mut result = tvg::tvg_font_load_data( + asset_name.as_ptr() as *const i8, + asset_data.as_ptr() as *const i8, + asset_data.len() as u32, + CString::new("ttf").unwrap().as_ptr(), + false, + ).into_result(); + + if result.is_ok() { + result = tvg::tvg_text_set_font( + paint, + asset_name.as_ptr() as *const i8, + ).into_result(); + } + + drop(asset_data); + return result.is_ok(); + } else if paint_type == tvg::Tvg_Type_TVG_TYPE_PICTURE { + // image load + let asset_ext = src_str + .rfind('.') + .map(|i| &src_str[i + 1..]) + .unwrap_or(DEFAULT_EXT); + + let result = TvgAnimation::tvg_load_data_dispatch( + paint, + asset_data.as_ptr() as *const i8, + asset_data.len() as u32, + CString::new(asset_ext).unwrap().as_ptr(), + ); + + drop(asset_data); + return result.is_ok(); + } + + // not supported paint type + false + } + + unsafe { + tvg::tvg_picture_set_asset_resolver( + self.raw_paint, + Some(thorvg_asset_resolver_wrapper), + resolver_data_ptr, + ).into_result() + } + } else { + unsafe { + tvg::tvg_picture_set_asset_resolver( + self.raw_paint, + None, + std::ptr::null_mut(), + ).into_result() + } + } + } + fn tween( &mut self, _to: f32,