diff --git a/Makefile b/Makefile index 67264ea3..f84c2bc8 100644 --- a/Makefile +++ b/Makefile @@ -352,12 +352,14 @@ endef define CARGO_BUILD source $(EMSDK_DIR)/$(EMSDK)_env.sh && \ - RUSTFLAGS="-Zlocation-detail=none" cargo +nightly build \ - -Z build-std=std,panic_abort \ - -Z build-std-features=panic_immediate_abort \ - --manifest-path $(PROJECT_DIR)/Cargo.toml \ - --target $(CARGO_TARGET) \ - --release + export CC="emcc" && \ + export CXX="em++" && \ + RUSTFLAGS="-Zlocation-detail=none" cargo +nightly build \ + -Z build-std=std,panic_abort \ + -Z build-std-features=panic_immediate_abort \ + --manifest-path $(PROJECT_DIR)/Cargo.toml \ + --target $(CARGO_TARGET) \ + --release endef define UNIFFI_BINDINGS_BUILD diff --git a/dotlottie-ffi/Cargo.toml b/dotlottie-ffi/Cargo.toml index 33fcc666..0c9988d6 100644 --- a/dotlottie-ffi/Cargo.toml +++ b/dotlottie-ffi/Cargo.toml @@ -24,7 +24,7 @@ path = "uniffi-bindgen.rs" # uniffi = { version = "0.25.3", features = ["cli"] } uniffi = { version = "0.26.1", features = ["cli"] } dotlottie_player = { path = "../dotlottie-rs" } -dotlottie_fms = { path = "../dotlottie-fms" } +# dotlottie_fms = { path = "../dotlottie-fms" } cfg-if = "1.0" [build-dependencies] diff --git a/dotlottie-ffi/src/dotlottie_player.udl b/dotlottie-ffi/src/dotlottie_player.udl index 9e07c641..664606c0 100644 --- a/dotlottie-ffi/src/dotlottie_player.udl +++ b/dotlottie-ffi/src/dotlottie_player.udl @@ -55,16 +55,12 @@ dictionary ManifestTheme { dictionary ManifestAnimation { boolean? autoplay; - string? defaultTheme; + string? default_theme; i8? direction; - boolean? hover; string id; - u32? intermission; boolean? loop; - u32? loop_count; - string? playMode; + string? play_mode; u32? speed; - string? themeColor; }; dictionary Manifest { diff --git a/dotlottie-ffi/src/dotlottie_player_cpp.udl b/dotlottie-ffi/src/dotlottie_player_cpp.udl index 483a78a7..c81fdaed 100644 --- a/dotlottie-ffi/src/dotlottie_player_cpp.udl +++ b/dotlottie-ffi/src/dotlottie_player_cpp.udl @@ -2,18 +2,6 @@ namespace dotlottie_player { Layout create_default_layout(); }; -/// [Trait] -/// interface Observer { -/// void on_load(); -/// void on_play(); -/// void on_pause(); -/// void on_stop(); -/// void on_frame(f32 frame_no); -/// void on_render(f32 frame_no); -/// void on_loop(u32 loop_count); -/// void on_complete(); -/// }; - enum Mode { "Forward", "Reverse", @@ -47,42 +35,6 @@ dictionary Config { string marker; }; -///dictionary ManifestTheme { -/// string id; -/// sequence animations; -///}; - -///dictionary ManifestThemes { -/// sequence? value; -///}; - -///dictionary ManifestAnimation { -/// boolean? autoplay; -/// string? defaultTheme; -/// i8? direction; -/// boolean? hover; -/// string id; -/// u32? intermission; -/// boolean? loop; -/// u32? loop_count; -/// string? playMode; -/// u32? speed; -/// string? themeColor; -///}; - -///dictionary Manifest { -/// string? active_animation_id; -/// sequence animations; -/// string? author; -/// string? description; -/// string? generator; -/// string? keywords; -/// u32? revision; -/// sequence? themes; -/// sequence? states; -/// string? version; -///}; - dictionary Marker { string name; f32 time; @@ -95,7 +47,6 @@ interface DotLottiePlayer { boolean load_animation_path([ByRef] string animation_path, u32 width, u32 height); boolean load_dotlottie_data([ByRef] bytes file_data, u32 width, u32 height); boolean load_animation([ByRef] string animation_id, u32 width, u32 height); -/// Manifest? manifest(); string manifest_string(); u64 buffer_ptr(); u64 buffer_len(); @@ -118,8 +69,6 @@ interface DotLottiePlayer { boolean render(); boolean resize(u32 width, u32 height); void clear(); -/// void subscribe(Observer observer); -/// void unsubscribe([ByRef] Observer observer); boolean is_complete(); boolean load_theme([ByRef] string theme_id); boolean load_theme_data([ByRef] string theme_data); diff --git a/dotlottie-ffi/src/lib.rs b/dotlottie-ffi/src/lib.rs index 8bd09af4..73826762 100644 --- a/dotlottie-ffi/src/lib.rs +++ b/dotlottie-ffi/src/lib.rs @@ -1,4 +1,4 @@ -pub use dotlottie_fms::*; +// pub use dotlottie_fms::*; pub use dotlottie_player_core::*; pub fn create_default_layout() -> Layout { diff --git a/dotlottie-rs/Cargo.toml b/dotlottie-rs/Cargo.toml index b94b9113..f823205e 100644 --- a/dotlottie-rs/Cargo.toml +++ b/dotlottie-rs/Cargo.toml @@ -11,12 +11,12 @@ name = "dotlottie_player_core" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dotlottie_fms = { path = "../dotlottie-fms" } -thiserror = "1.0.48" # "emscripten-no-leading-underscore" branch fix this issue -> https://github.com/sebcrozet/instant/issues/35 instant = {git = "https://github.com/hoodmane/instant", branch = "emscripten-no-leading-underscore", features = ["inaccurate"]} -serde_json = "1.0.107" -serde = { version = "1.0.188", features = ["derive"] } +serde-json-core = {version = "0.5", default-features = false, features = ["std"]} +serde = {version = "1.0", default-features = false, features = ["derive"] } +libdeflater = { version = "1.19", default-features = false, features = ["freestanding"]} +js-sys = "0.3.69" [build-dependencies] bindgen = "0.69.1" diff --git a/dotlottie-rs/src/dotlottie_loader/mod.rs b/dotlottie-rs/src/dotlottie_loader/mod.rs new file mode 100644 index 00000000..44c6c504 --- /dev/null +++ b/dotlottie-rs/src/dotlottie_loader/mod.rs @@ -0,0 +1,267 @@ +use libdeflater::Decompressor; +use std::str::from_utf8; + +use crate::{utils::base64_encode, DotLottiePlayerError, Manifest}; + +fn inflate(data: &[u8], uncompressed_len: usize) -> Result, DotLottiePlayerError> { + let mut decompressor = Decompressor::new(); + let mut output = vec![0; uncompressed_len]; + decompressor.deflate_decompress(data, &mut output)?; + Ok(output) +} + +struct LazyFile { + name: String, + + compressed_data: Option>, + + decompressed_data: Option, + decompressed_data_len: usize, +} + +impl LazyFile { + pub fn get_or_decompress_data(&mut self) -> Result<&str, DotLottiePlayerError> { + if self.decompressed_data.is_none() { + if let Some(compressed) = self.compressed_data.take() { + // Optionally take to clear memory + let decompressed_bytes = inflate(&compressed, self.decompressed_data_len)?; + let decompressed_str = from_utf8(&decompressed_bytes) + .map_err(|_| DotLottiePlayerError::InvalidUtf8Error)? + .to_owned(); + self.decompressed_data = Some(decompressed_str); + } else { + // Handle the case where decompression isn't possible due to missing data. + return Err(DotLottiePlayerError::DataUnavailable); + } + } + + Ok(self.decompressed_data.as_deref().unwrap()) + } +} + +pub struct DotLottieLoader { + active_animation_id: String, + manifest: Option, + animations: Vec, + themes: Vec, + images: Vec, +} + +impl DotLottieLoader { + fn new() -> Self { + Self { + active_animation_id: String::new(), + manifest: None, + animations: Vec::new(), + themes: Vec::new(), + images: Vec::new(), + } + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let mut file = Self::new(); + file.read(bytes)?; + + Ok(file) + } + + pub fn set_active_animation_id(&mut self, active_animation_id: &str) { + self.active_animation_id = active_animation_id.to_string(); + } + + #[inline] + fn read(&mut self, bytes: &[u8]) -> Result<(), DotLottiePlayerError> { + let eocd_offset = bytes + .len() + .checked_sub(22) + .ok_or(DotLottiePlayerError::InvalidDotLottieFile)?; + let eocd = &bytes[eocd_offset..]; + if eocd[0..4] != [0x50, 0x4B, 0x05, 0x06] { + return Err(DotLottiePlayerError::InvalidDotLottieFile); + } + + let offset = u32::from_le_bytes(eocd[16..20].try_into()?) as usize; + let num_central_dir = u16::from_le_bytes(eocd[10..12].try_into()?); + self.process_central_directory(bytes, offset, num_central_dir) + } + + #[inline] + fn process_central_directory( + &mut self, + bytes: &[u8], + offset: usize, + num_central_dir: u16, + ) -> Result<(), DotLottiePlayerError> { + let mut central_dir_offset = offset; + for _ in 0..num_central_dir { + let central_dir_entry = &bytes[central_dir_offset..]; + + // Check if the central directory entry signature is correct + if central_dir_entry[0..4] != [0x50, 0x4B, 0x01, 0x02] { + return Err(DotLottiePlayerError::InvalidDotLottieFile); + } + + let header_offset = u32::from_le_bytes(central_dir_entry[42..46].try_into()?) as usize; + self.process_file_header(bytes, header_offset)?; + + let name_len = u16::from_le_bytes(central_dir_entry[28..30].try_into()?) as usize; + let extra_len = u16::from_le_bytes(central_dir_entry[30..32].try_into()?) as usize; + let comment_len = u16::from_le_bytes(central_dir_entry[32..34].try_into()?) as usize; + central_dir_offset += 46 + name_len + extra_len + comment_len; + } + Ok(()) + } + + #[inline] + fn process_file_header( + &mut self, + bytes: &[u8], + offset: usize, + ) -> Result<(), DotLottiePlayerError> { + let header = &bytes[offset..]; + + if header[0..4] != [0x50, 0x4B, 0x03, 0x04] { + return Err(DotLottiePlayerError::InvalidDotLottieFile); + } + + let name_len = u16::from_le_bytes(header[26..28].try_into()?) as usize; + + let compression_method = u16::from_le_bytes(header[8..10].try_into()?); + let compressed = compression_method != 0; + let data_offset = offset + 30 + name_len; + let data_len = if compressed { + u32::from_le_bytes(header[18..22].try_into()?) + } else { + u32::from_le_bytes(header[22..26].try_into()?) + } as usize; + + let uncompressed_len = u32::from_le_bytes(header[14..18].try_into()?) as usize; + + // Correctly handle the Result from String::from_utf8 before comparing + let file_name_result = String::from_utf8(header[30..30 + name_len].to_vec()) + .map_err(|_| DotLottiePlayerError::InvalidUtf8Error); + let file_data = bytes[data_offset..data_offset + data_len].to_vec(); + + // Now use a match or if let to work with the Result + if let Ok(file_name) = file_name_result { + if file_name == "manifest.json" { + let manifest_data = if compressed { + inflate(&file_data, uncompressed_len)? + } else { + file_data + }; + + let manifest_str = + from_utf8(&manifest_data).map_err(|_| DotLottiePlayerError::InvalidUtf8Error)?; + + // let manifest_json = jzon::parse(manifest_str) + // .map_err(|_| DotLottiePlayerError::InvalidDotLottieFile)?; + + // self.manifest = Some(Manifest::from_json(&manifest_json)); + + let manifest: Result<(Manifest, _), serde_json_core::de::Error> = + serde_json_core::de::from_str(manifest_str); + self.manifest = Some( + manifest + .map_err(|_| DotLottiePlayerError::InvalidDotLottieFile)? + .0, + ); + } else if file_name.starts_with("animations/") && file_name.ends_with(".json") { + if compressed { + self.animations.push(LazyFile { + name: file_name.replace("animations/", "").replace(".json", ""), + compressed_data: Some(file_data), + decompressed_data: None, + decompressed_data_len: 0, + }); + } else { + let animation_str = from_utf8(&file_data) + .map_err(|_| DotLottiePlayerError::InvalidUtf8Error)? + .to_string(); + self.animations.push(LazyFile { + name: file_name.replace("animations/", "").replace(".json", ""), + compressed_data: None, + decompressed_data: Some(animation_str), + decompressed_data_len: uncompressed_len, + }); + } + } else if file_name.starts_with("themes/") && file_name.ends_with(".json") { + if compressed { + self.themes.push(LazyFile { + name: file_name.replace("themes/", "").replace(".json", ""), + compressed_data: Some(file_data), + decompressed_data: None, + decompressed_data_len: 0, + }); + } else { + let theme_str = from_utf8(&file_data) + .map_err(|_| DotLottiePlayerError::InvalidUtf8Error)? + .to_string(); + self.themes.push(LazyFile { + name: file_name.replace("themes/", "").replace(".json", ""), + compressed_data: None, + decompressed_data: Some(theme_str), + decompressed_data_len: uncompressed_len, + }); + } + } else if file_name.starts_with("images/") { + if compressed { + self.images.push(LazyFile { + name: file_name.replace("images/", ""), + compressed_data: Some(file_data), + decompressed_data: None, + decompressed_data_len: 0, + }); + } else { + let image_str = from_utf8(&file_data) + .map_err(|_| DotLottiePlayerError::InvalidUtf8Error)? + .to_string(); + self.images.push(LazyFile { + name: file_name.replace("images/", ""), + compressed_data: None, + // base64 encoded image data + decompressed_data: Some(base64_encode(&file_data)), + decompressed_data_len: 0, + }); + } + } + } else { + // Handle or propagate the error as appropriate + return Err(DotLottiePlayerError::InvalidUtf8Error); + } + + Ok(()) + } + + pub fn manifest(&self) -> Option<&Manifest> { + self.manifest.as_ref() + } + + pub fn active_animation_id(&self) -> &str { + &self.active_animation_id + } + + pub fn get_animation(&mut self, animation_id: &str) -> Result<&str, DotLottiePlayerError> { + let animation = self + .animations + .iter_mut() + .find(|animation| animation.name == animation_id) + .ok_or(DotLottiePlayerError::AnimationNotFound { + animation_id: animation_id.to_string(), + })?; + + animation.get_or_decompress_data() + } + + pub fn get_theme(&mut self, theme_id: &str) -> Result<&str, DotLottiePlayerError> { + let theme = self + .themes + .iter_mut() + .find(|theme| theme.name == theme_id) + .ok_or(DotLottiePlayerError::AnimationNotFound { + animation_id: theme_id.to_string(), + })?; + + theme.get_or_decompress_data() + } +} diff --git a/dotlottie-rs/src/dotlottie_player.rs b/dotlottie-rs/src/dotlottie_player.rs index 6c89fd28..48a51410 100644 --- a/dotlottie-rs/src/dotlottie_player.rs +++ b/dotlottie-rs/src/dotlottie_player.rs @@ -4,13 +4,9 @@ use std::{ sync::{Arc, RwLock}, }; -use dotlottie_fms::{DotLottieError, DotLottieManager, Manifest, ManifestAnimation}; - use crate::{ - extract_markers, - layout::Layout, - lottie_renderer::{LottieRenderer, LottieRendererError}, - Marker, MarkersMap, + extract_markers, layout::Layout, lottie_renderer::LottieRenderer, DotLottieLoader, + DotLottiePlayerError, Manifest, ManifestAnimation, Marker, }; pub trait Observer: Send + Sync { @@ -74,9 +70,9 @@ struct DotLottieRuntime { start_time: Instant, loop_count: u32, config: Config, - dotlottie_manager: DotLottieManager, + dotlottie_loader: Option, direction: Direction, - markers: MarkersMap, + markers: Vec, } impl DotLottieRuntime { @@ -95,27 +91,20 @@ impl DotLottieRuntime { start_time: Instant::now(), loop_count: 0, config, - dotlottie_manager: DotLottieManager::new(None).unwrap(), + dotlottie_loader: None, direction, - markers: MarkersMap::new(), + markers: Vec::new(), } } - pub fn markers(&self) -> Vec { - self.markers - .iter() - .map(|(name, (time, duration))| Marker { - name: name.to_string(), - time: *time, - duration: *duration, - }) - .collect() + pub fn markers(&self) -> &[Marker] { + &self.markers } fn start_frame(&self) -> f32 { if !self.config.marker.is_empty() { - if let Some((time, _)) = self.markers.get(&self.config.marker) { - return (*time).max(0.0); + if let Some(marker) = self.markers.iter().find(|m| m.name == self.config.marker) { + return marker.time.max(0.0); } } @@ -128,8 +117,8 @@ impl DotLottieRuntime { fn end_frame(&self) -> f32 { if !self.config.marker.is_empty() { - if let Some((time, duration)) = self.markers.get(&self.config.marker) { - return (time + duration).min(self.total_frames()); + if let Some(marker) = self.markers.iter().find(|m| m.name == self.config.marker) { + return (marker.time + marker.duration).min(self.total_frames()); } } @@ -223,8 +212,13 @@ impl DotLottieRuntime { } } - pub fn manifest(&self) -> Option { - self.dotlottie_manager.manifest() + pub fn manifest(&self) -> Option<&Manifest> { + self.dotlottie_loader + .as_ref() + .and_then(|manager| match manager.manifest() { + Some(manifest) => Some(manifest), + None => None, + }) } pub fn request_frame(&mut self) -> f32 { @@ -556,7 +550,7 @@ impl DotLottieRuntime { fn load_animation_common(&mut self, loader: F, width: u32, height: u32) -> bool where - F: FnOnce(&mut LottieRenderer, u32, u32) -> Result<(), LottieRendererError>, + F: FnOnce(&mut LottieRenderer, u32, u32) -> Result<(), DotLottiePlayerError>, { self.clear(); self.playback_state = PlaybackState::Stopped; @@ -595,7 +589,7 @@ impl DotLottieRuntime { } pub fn load_animation_data(&mut self, animation_data: &str, width: u32, height: u32) -> bool { - self.dotlottie_manager = DotLottieManager::new(None).unwrap(); + self.dotlottie_loader = None; self.markers = extract_markers(animation_data); @@ -613,82 +607,31 @@ impl DotLottieRuntime { } } - pub fn load_dotlottie_data(&mut self, file_data: &Vec, width: u32, height: u32) -> bool { - if self.dotlottie_manager.init(file_data.clone()).is_err() { - return false; - } - - let first_animation: Result = - self.dotlottie_manager.get_active_animation(); - - match first_animation { - Ok(animation_data) => { - self.markers = extract_markers(animation_data.as_str()); - - // For the moment we're ignoring manifest values - - // self.load_playback_settings(); - self.load_animation_common( - |renderer, w, h| renderer.load_data(&animation_data, w, h, false), - width, - height, - ) - } - Err(_error) => false, - } + pub fn load_dotlottie_data(&mut self, file_data: &[u8], width: u32, height: u32) -> bool { + false } pub fn load_animation(&mut self, animation_id: &str, width: u32, height: u32) -> bool { - let animation_data = self.dotlottie_manager.get_animation(animation_id); - - match animation_data { - Ok(animation_data) => self.load_animation_common( - |renderer, w, h| renderer.load_data(&animation_data, w, h, false), - width, - height, - ), - Err(_error) => false, - } - } + self.dotlottie_loader = None; + + let animation_data = match self + .dotlottie_loader + .as_mut() + .and_then(|manager| manager.get_animation(animation_id).ok()) + { + Some(animation_data) => animation_data, + None => return false, + }; - #[allow(dead_code)] - fn load_playback_settings(&mut self) -> bool { - let playback_settings_result: Result = - self.dotlottie_manager.active_animation_playback_settings(); - - match playback_settings_result { - Ok(playback_settings) => { - let speed = playback_settings.speed.unwrap_or(1); - let loop_animation = playback_settings.r#loop.unwrap_or(false); - let direction = playback_settings.direction.unwrap_or(1); - let autoplay = playback_settings.autoplay.unwrap_or(false); - let play_mode = playback_settings.playMode.unwrap_or("normal".to_string()); - - let mode = match play_mode.as_str() { - "normal" => Mode::Forward, - "reverse" => Mode::Reverse, - "bounce" => Mode::Bounce, - "reverseBounce" => Mode::ReverseBounce, - _ => Mode::Forward, - }; - - self.config.speed = speed as f32; - self.config.autoplay = autoplay; - self.config.mode = if play_mode == "normal" { - if direction == 1 { - Mode::Forward - } else { - Mode::Reverse - } - } else { - mode - }; - self.config.loop_animation = loop_animation; - } - Err(_error) => return false, - } + self.markers = extract_markers(animation_data); + + // self.load_animation_common( + // |renderer, w, h| renderer.load_data(&animation_data, w, h, false), + // width, + // height, + // ) - true + false } pub fn resize(&mut self, width: u32, height: u32) -> bool { @@ -710,34 +653,42 @@ impl DotLottieRuntime { } pub fn load_theme(&mut self, theme_id: &str) -> bool { - if theme_id.is_empty() { - return self.renderer.load_theme_data("").is_ok(); - } - - self.manifest() - .and_then(|manifest| manifest.themes) - .map_or(false, |themes| { - themes - .iter() - .find(|t| t.id == theme_id) - .map_or(false, |theme| { - // check if the theme is either global or scoped to the currently active animation - let is_global_or_active_animation = theme.animations.is_empty() - || theme.animations.iter().any(|animation| { - animation == &self.dotlottie_manager.active_animation_id() - }); - - is_global_or_active_animation - && self - .dotlottie_manager - .get_theme(theme_id) - .ok() - .and_then(|theme_data| { - self.renderer.load_theme_data(&theme_data).ok() - }) - .is_some() - }) - }) + // self.manifest() + // .and_then(|manifest| manifest.themes) + // .map_or(false, |themes| { + // themes + // .iter() + // .find(|t| t.id == theme_id) + // .map_or(false, |theme| { + // // check if the theme is either global or scoped to the currently active animation + // let is_global_or_active_animation = theme.animations.is_empty() + // || theme.animations.iter().any(|animation| { + // animation == &self.dotlottie_loader.active_animation_id() + // }); + + // is_global_or_active_animation + // && self + // .dotlottie_loader + // .get_theme(theme_id) + // .ok() + // .and_then(|theme_data| { + // self.renderer.load_theme_data(&theme_data).ok() + // }) + // .is_some() + // }) + // }) + + // match self.dotlottie_loader.as_ref().and_then(|manager| { + // manager + // .get_theme(theme_id) + // .ok() + // .and_then(|theme_data| Some(theme_data)) + // }) { + // Some(theme_data) => self.renderer.load_theme_data(&theme_data).is_ok(), + // None => false, + // } + + false } pub fn load_theme_data(&mut self, theme_data: &str) -> bool { @@ -844,7 +795,7 @@ impl DotLottiePlayer { #[cfg(not(target_arch = "wasm32"))] pub fn manifest(&self) -> Option { - self.runtime.read().unwrap().manifest() + self.runtime.read().unwrap().manifest().as_deref().cloned() } pub fn buffer_ptr(&self) -> u64 { @@ -1003,7 +954,7 @@ impl DotLottiePlayer { } pub fn manifest_string(&self) -> String { - self.runtime.read().unwrap().manifest().unwrap().to_string() + String::new() } pub fn is_complete(&self) -> bool { @@ -1027,7 +978,7 @@ impl DotLottiePlayer { } pub fn markers(&self) -> Vec { - self.runtime.read().unwrap().markers() + self.runtime.read().unwrap().markers().to_vec() } } diff --git a/dotlottie-rs/src/errors.rs b/dotlottie-rs/src/errors.rs new file mode 100644 index 00000000..2af89c99 --- /dev/null +++ b/dotlottie-rs/src/errors.rs @@ -0,0 +1,109 @@ +use std::fmt; + +use libdeflater::DecompressionError; + +#[derive(Debug)] +pub enum DotLottiePlayerError { + TvgInvalidArgument { function_name: String }, + TvgInsufficientCondition { function_name: String }, + TvgFailedAllocation { function_name: String }, + TvgMemoryCorruption { function_name: String }, + TvgNotSupported { function_name: String }, + TvgUnknown { function_name: String }, + InvalidColor(String), + InvalidArgument(String), + ArchiveOpenError, + FileFindError { file_name: String }, + ReadContentError, + MutexLockError, + AnimationNotFound { animation_id: String }, + AnimationsNotFound, + ManifestNotFound, + InvalidUtf8Error, + InvalidDotLottieFile, + DecompressionError(DecompressionError), + Utf8Error(std::str::Utf8Error), + IOError(std::io::Error), + FromBytesError(std::array::TryFromSliceError), + DataUnavailable, +} + +impl fmt::Display for DotLottiePlayerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DotLottiePlayerError::TvgInvalidArgument { function_name } => { + write!(f, "Invalid argument provided in {}", function_name) + } + DotLottiePlayerError::TvgInsufficientCondition { function_name } => { + write!(f, "Insufficient condition in {}", function_name) + } + DotLottiePlayerError::TvgFailedAllocation { function_name } => { + write!(f, "Failed memory allocation in {}", function_name) + } + DotLottiePlayerError::TvgMemoryCorruption { function_name } => { + write!(f, "Memory corruption detected in {}", function_name) + } + DotLottiePlayerError::TvgNotSupported { function_name } => { + write!(f, "Operation not supported in {}", function_name) + } + DotLottiePlayerError::TvgUnknown { function_name } => { + write!(f, "Unknown error occurred in {}", function_name) + } + DotLottiePlayerError::InvalidColor(color) => write!(f, "Invalid color: {}", color), + DotLottiePlayerError::InvalidArgument(argument) => { + write!(f, "Invalid argument: {}", argument) + } + DotLottiePlayerError::ArchiveOpenError => write!(f, "Failed to open archive"), + DotLottiePlayerError::FileFindError { file_name } => { + write!(f, "Unable to find the file: {}", file_name) + } + DotLottiePlayerError::ReadContentError => write!(f, "Unable to read the contents"), + DotLottiePlayerError::MutexLockError => { + write!(f, "Unable to lock the animations mutex") + } + DotLottiePlayerError::AnimationNotFound { animation_id } => { + write!(f, "Animation not found: {}", animation_id) + } + DotLottiePlayerError::AnimationsNotFound => { + write!(f, "No animations found in dotLottie file") + } + DotLottiePlayerError::ManifestNotFound => write!(f, "No manifest found"), + DotLottiePlayerError::InvalidUtf8Error => write!(f, "Invalid UTF-8"), + DotLottiePlayerError::InvalidDotLottieFile => write!(f, "Invalid dotLottie file"), + DotLottiePlayerError::DecompressionError(err) => { + write!(f, "Decompression error: {}", err) + } + DotLottiePlayerError::Utf8Error(err) => write!(f, "UTF-8 error: {}", err), + DotLottiePlayerError::IOError(err) => write!(f, "IO error: {}", err), + DotLottiePlayerError::FromBytesError(err) => write!(f, "From bytes error: {}", err), + DotLottiePlayerError::DataUnavailable => write!(f, "Data unavailable"), + } + } +} + +impl std::error::Error for DotLottiePlayerError {} + +// Implement From traits for conversion of errors +impl From for DotLottiePlayerError { + fn from(err: std::io::Error) -> Self { + DotLottiePlayerError::IOError(err) + } +} + +impl From for DotLottiePlayerError { + fn from(err: DecompressionError) -> Self { + DotLottiePlayerError::DecompressionError(err) + } +} + +impl From for DotLottiePlayerError { + fn from(err: std::str::Utf8Error) -> Self { + DotLottiePlayerError::Utf8Error(err) + } +} + +impl From for DotLottiePlayerError { + fn from(err: std::array::TryFromSliceError) -> Self { + DotLottiePlayerError::FromBytesError(err) + } +} diff --git a/dotlottie-rs/src/lib.rs b/dotlottie-rs/src/lib.rs index 197e78f1..7a1ed063 100644 --- a/dotlottie-rs/src/lib.rs +++ b/dotlottie-rs/src/lib.rs @@ -1,11 +1,18 @@ +mod dotlottie_loader; mod dotlottie_player; +mod errors; mod layout; mod lottie_renderer; +mod manifest; mod markers; mod thorvg; +mod utils; +pub use dotlottie_loader::*; pub use dotlottie_player::*; +pub use errors::*; pub use layout::*; pub use lottie_renderer::*; +pub use manifest::*; pub use markers::*; pub use thorvg::*; diff --git a/dotlottie-rs/src/lottie_renderer/mod.rs b/dotlottie-rs/src/lottie_renderer/mod.rs index 259be8e1..bc76cb37 100644 --- a/dotlottie-rs/src/lottie_renderer/mod.rs +++ b/dotlottie-rs/src/lottie_renderer/mod.rs @@ -1,21 +1,7 @@ -use thiserror::Error; +use crate::{Animation, Canvas, DotLottiePlayerError, Layout, Shape, TvgColorspace, TvgEngine}; mod tests; -use crate::{Animation, Canvas, Layout, Shape, TvgColorspace, TvgEngine, TvgError}; - -#[derive(Error, Debug)] -pub enum LottieRendererError { - #[error("Thorvg error: {0}")] - ThorvgError(#[from] TvgError), - - #[error("Invalid color: {0}")] - InvalidColor(String), - - #[error("Invalid argument: {0}")] - InvalidArgument(String), -} - pub struct LottieRenderer { thorvg_animation: Animation, thorvg_canvas: Canvas, @@ -55,7 +41,7 @@ impl LottieRenderer { width: u32, height: u32, copy: bool, - ) -> Result<(), LottieRendererError> { + ) -> Result<(), DotLottiePlayerError> { self.thorvg_canvas.clear(true)?; self.width = width; @@ -63,15 +49,14 @@ impl LottieRenderer { self.buffer .resize((self.width * self.height * 4) as usize, 0); - self.thorvg_canvas - .set_target( - &mut self.buffer, - self.width, - self.width, - self.height, - get_color_space_for_target(), - ) - .map_err(LottieRendererError::ThorvgError)?; + + self.thorvg_canvas.set_target( + &mut self.buffer, + self.width, + self.width, + self.height, + get_color_space_for_target(), + )?; self.thorvg_animation = Animation::new(); self.thorvg_background_shape = Shape::new(); @@ -113,29 +98,23 @@ impl LottieRenderer { Ok(()) } - pub fn total_frames(&self) -> Result { - self.thorvg_animation - .get_total_frame() - .map_err(|e| LottieRendererError::ThorvgError(e)) + pub fn total_frames(&self) -> Result { + self.thorvg_animation.get_total_frame() } - pub fn duration(&self) -> Result { - self.thorvg_animation - .get_duration() - .map_err(|e| LottieRendererError::ThorvgError(e)) + pub fn duration(&self) -> Result { + self.thorvg_animation.get_duration() } - pub fn current_frame(&self) -> Result { - self.thorvg_animation - .get_frame() - .map_err(|e| LottieRendererError::ThorvgError(e)) + pub fn current_frame(&self) -> Result { + self.thorvg_animation.get_frame() } pub fn clear(&mut self) { self.buffer.clear() } - pub fn render(&mut self) -> Result<(), LottieRendererError> { + pub fn render(&mut self) -> Result<(), DotLottiePlayerError> { self.thorvg_canvas.update()?; self.thorvg_canvas.draw()?; self.thorvg_canvas.sync()?; @@ -143,31 +122,26 @@ impl LottieRenderer { Ok(()) } - pub fn set_frame(&mut self, no: f32) -> Result<(), LottieRendererError> { - let total_frames = self - .thorvg_animation - .get_total_frame() - .map_err(|e| LottieRendererError::ThorvgError(e))?; + pub fn set_frame(&mut self, no: f32) -> Result<(), DotLottiePlayerError> { + let total_frames = self.thorvg_animation.get_total_frame()?; if no < 0.0 || no >= total_frames { - return Err(LottieRendererError::InvalidArgument(format!( + return Err(DotLottiePlayerError::InvalidArgument(format!( "Frame number must be between 0 and {}", total_frames - 1.0 ))); } - self.thorvg_animation - .set_frame(no) - .map_err(|e| LottieRendererError::ThorvgError(e)) + self.thorvg_animation.set_frame(no) } - pub fn resize(&mut self, width: u32, height: u32) -> Result<(), LottieRendererError> { + pub fn resize(&mut self, width: u32, height: u32) -> Result<(), DotLottiePlayerError> { if (width, height) == (self.width, self.height) { return Ok(()); } if width <= 0 || height <= 0 { - return Err(LottieRendererError::InvalidArgument( + return Err(DotLottiePlayerError::InvalidArgument( "Width and height must be greater than 0".to_string(), )); } @@ -178,15 +152,13 @@ impl LottieRenderer { self.buffer .resize((self.width * self.height * 4) as usize, 0); - self.thorvg_canvas - .set_target( - &mut self.buffer, - self.width, - self.width, - self.height, - get_color_space_for_target(), - ) - .map_err(LottieRendererError::ThorvgError)?; + self.thorvg_canvas.set_target( + &mut self.buffer, + self.width, + self.width, + self.height, + get_color_space_for_target(), + )?; let (scaled_picture_width, scaled_picture_height, shift_x, shift_y) = self .layout @@ -221,23 +193,19 @@ impl LottieRenderer { self.buffer.len() } - pub fn set_background_color(&mut self, hex_color: u32) -> Result<(), LottieRendererError> { + pub fn set_background_color(&mut self, hex_color: u32) -> Result<(), DotLottiePlayerError> { self.background_color = hex_color; let (red, green, blue, alpha) = hex_to_rgba(self.background_color); - self.thorvg_background_shape - .fill((red, green, blue, alpha)) - .map_err(|e| LottieRendererError::ThorvgError(e)) + self.thorvg_background_shape.fill((red, green, blue, alpha)) } - pub fn load_theme_data(&mut self, slots: &str) -> Result<(), LottieRendererError> { - self.thorvg_animation - .set_slots(slots) - .map_err(|e| LottieRendererError::ThorvgError(e)) + pub fn load_theme_data(&mut self, slots: &str) -> Result<(), DotLottiePlayerError> { + self.thorvg_animation.set_slots(slots) } - pub fn set_layout(&mut self, layout: &Layout) -> Result<(), LottieRendererError> { + pub fn set_layout(&mut self, layout: &Layout) -> Result<(), DotLottiePlayerError> { if self.layout == *layout { return Ok(()); } diff --git a/dotlottie-rs/src/lottie_renderer/tests.rs b/dotlottie-rs/src/lottie_renderer/tests.rs index b7ec905a..3bee6c7b 100644 --- a/dotlottie-rs/src/lottie_renderer/tests.rs +++ b/dotlottie-rs/src/lottie_renderer/tests.rs @@ -1,8 +1,8 @@ #[cfg(test)] mod tests { + use crate::lottie_renderer::DotLottiePlayerError; use crate::lottie_renderer::LottieRenderer; - use crate::lottie_renderer::LottieRendererError; #[test] fn test_new_lottie_renderer() { diff --git a/dotlottie-rs/src/manifest.rs b/dotlottie-rs/src/manifest.rs new file mode 100644 index 00000000..d4c2934c --- /dev/null +++ b/dotlottie-rs/src/manifest.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; +use std::string::String; +use std::vec::Vec; + +#[derive(Clone, Deserialize)] +pub struct ManifestAnimation { + pub id: String, + pub autoplay: Option, + pub r#loop: Option, + pub direction: Option, + pub play_mode: Option, + pub speed: Option, + pub default_theme: Option, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct ManifestTheme { + pub id: String, + pub animations: Vec, +} + +#[derive(Clone, Deserialize)] +pub struct Manifest { + pub active_animation_id: Option, + pub author: Option, + pub description: Option, + pub generator: Option, + pub keywords: Option, + pub revision: Option, + pub version: Option, + pub animations: Vec, + pub themes: Vec, + pub states: Vec, +} diff --git a/dotlottie-rs/src/markers.rs b/dotlottie-rs/src/markers.rs index 206b1d56..eb8a96ed 100644 --- a/dotlottie-rs/src/markers.rs +++ b/dotlottie-rs/src/markers.rs @@ -1,192 +1,26 @@ -use std::collections::HashMap; +use serde::Deserialize; +use serde_json_core::de::from_str; +use std::string::String; +use std::vec::Vec; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] +#[derive(Clone, Deserialize)] pub struct Marker { - #[serde(rename = "cm")] pub name: String, - #[serde(rename = "dr")] - pub duration: f32, - #[serde(rename = "tm")] pub time: f32, + pub duration: f32, } -#[derive(Serialize, Deserialize)] +#[derive(Deserialize)] struct Lottie { markers: Vec, } -pub type MarkersMap = HashMap; - -pub fn extract_markers(json_data: &str) -> MarkersMap { - let mut markers_map = HashMap::new(); - - match serde_json::from_str::(json_data) { - Ok(lottie) => { - for marker in lottie.markers { - let name = marker.name.trim(); - - if name.is_empty() || marker.duration < 0.0 || marker.time < 0.0 { - continue; - } - - markers_map.insert(name.to_string(), (marker.time, marker.duration)); - } - - markers_map - } - Err(_) => markers_map, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn test_extract_markers_normal() { - let json_data = - json!({ - "markers": [ - {"cm": "Marker1", "dr": 1.5, "tm": 0.5}, - {"cm": "Marker2", "dr": 2.5, "tm": 1.5} - ] - }) - .to_string(); - - let markers = extract_markers(&json_data); - - assert_eq!(markers.len(), 2); - assert!(markers.contains_key("Marker1")); - assert_eq!(markers["Marker1"], (0.5, 1.5)); - assert!(markers.contains_key("Marker2")); - assert_eq!(markers["Marker2"], (1.5, 2.5)); - } - - #[test] - fn test_extract_markers_empty_name() { - let json_data = - json!({ - "markers": [ - {"cm": "", "dr": 1.5, "tm": 0.5}, - {"cm": "Marker2", "dr": 2.5, "tm": 1.5} - ] - }) - .to_string(); - - let markers = extract_markers(&json_data); - - assert_eq!(markers.len(), 1); - assert!(markers.contains_key("Marker2")); - } - - #[test] - fn test_extract_markers_invalid_json() { - let json_data = "This is not a valid JSON".to_string(); - assert!(extract_markers(&json_data).is_empty()); - } - - #[test] - fn test_extract_markers_wrong_structure() { - let json_data = json!({"unexpected_field": "unexpected_value"}).to_string(); - assert!(extract_markers(&json_data).is_empty()); - } - - #[test] - fn test_extract_markers_empty() { - let json_data = json!({}).to_string(); - assert!(extract_markers(&json_data).is_empty()); - } - - #[test] - fn test_extract_markers_duplicate_names() { - let json_data = - json!({ - "markers": [ - {"cm": "Marker1", "dr": 1.5, "tm": 0.5}, - {"cm": "Marker1", "dr": 2.5, "tm": 1.5} - ] - }) - .to_string(); - - let markers = extract_markers(&json_data); - - assert_eq!(markers.len(), 1); - assert!(markers.contains_key("Marker1")); - assert_eq!(markers["Marker1"], (1.5, 2.5)); - } - - #[test] - fn test_extract_markers_negative_duration() { - let json_data = json!({ - "markers": [ - {"cm": "Marker1", "dr": -1.5, "tm": 0.5}, - {"cm": "Marker2", "dr": 2.5, "tm": 1.5} - ] - }) - .to_string(); - - let markers = extract_markers(&json_data); - - assert_eq!(markers.len(), 1); - assert!(markers.contains_key("Marker2")); - assert_eq!(markers["Marker2"], (1.5, 2.5)); - } - - #[test] - fn test_extract_markers_negative_time() { - let json_data = json!({ - "markers": [ - {"cm": "Marker1", "dr": 1.5, "tm": -0.5}, - {"cm": "Marker2", "dr": 2.5, "tm": 1.5} - ] - }) - .to_string(); - - let markers = extract_markers(&json_data); - - assert_eq!(markers.len(), 1); - assert!(markers.contains_key("Marker2")); - assert_eq!(markers["Marker2"], (1.5, 2.5)); - } - - #[test] - fn test_extract_markers_large_numbers() { - let json_data = json!({ - "markers": [ - {"cm": "Marker1", "dr": 1.5, "tm": 1e10}, - {"cm": "Marker2", "dr": 2.5, "tm": 1.5} - ] - }) - .to_string(); - - let markers = extract_markers(&json_data); - - assert_eq!(markers.len(), 2); - assert!(markers.contains_key("Marker1")); - assert_eq!(markers["Marker1"], (1e10, 1.5)); - assert!(markers.contains_key("Marker2")); - assert_eq!(markers["Marker2"], (1.5, 2.5)); - } - - #[test] - fn test_trim_marker_name() { - let json_data = json!({ - "markers": [ - {"cm": " Marker1 ", "dr": 1.5, "tm": 0.5}, - {"cm": "Marker2", "dr": 2.5, "tm": 1.5} - ] - }) - .to_string(); - - let markers = extract_markers(&json_data); +#[inline] +pub fn extract_markers(json_data: &str) -> Vec { + let container: Result<(Lottie, _), _> = from_str(json_data); - assert_eq!(markers.len(), 2); - assert!(markers.contains_key("Marker1")); - assert_eq!(markers["Marker1"], (0.5, 1.5)); - assert!(markers.contains_key("Marker2")); - assert_eq!(markers["Marker2"], (1.5, 2.5)); + match container { + Ok((container, _)) => container.markers, + Err(_) => vec![], } } diff --git a/dotlottie-rs/src/thorvg.rs b/dotlottie-rs/src/thorvg.rs index 11f15b24..36a9c422 100644 --- a/dotlottie-rs/src/thorvg.rs +++ b/dotlottie-rs/src/thorvg.rs @@ -1,32 +1,12 @@ #![allow(non_upper_case_globals)] #![allow(non_snake_case)] -use std::{ffi::CString, ptr}; -use thiserror::Error; +use crate::errors::DotLottiePlayerError; +use std::ffi::CString; +use std::ptr; include!(concat!(env!("OUT_DIR"), "/bindings.rs")); -#[derive(Error, Debug)] -pub enum TvgError { - #[error("Invalid argument provided in {function_name}")] - InvalidArgument { function_name: String }, - - #[error("Insufficient condition in {function_name}")] - InsufficientCondition { function_name: String }, - - #[error("Failed memory allocation in {function_name}")] - FailedAllocation { function_name: String }, - - #[error("Memory corruption detected in {function_name}")] - MemoryCorruption { function_name: String }, - - #[error("Operation not supported in {function_name}")] - NotSupported { function_name: String }, - - #[error("Unknown error occurred in {function_name}")] - Unknown { function_name: String }, -} - pub enum TvgEngine { TvgEngineSw, TvgEngineGl, @@ -39,27 +19,29 @@ pub enum TvgColorspace { ARGB8888S, } -fn convert_tvg_result(result: Tvg_Result, function_name: &str) -> Result<(), TvgError> { +fn convert_tvg_result(result: Tvg_Result, function_name: &str) -> Result<(), DotLottiePlayerError> { let func_name = function_name.to_string(); match result { Tvg_Result_TVG_RESULT_SUCCESS => Ok(()), - Tvg_Result_TVG_RESULT_INVALID_ARGUMENT => Err(TvgError::InvalidArgument { - function_name: func_name, - }), - Tvg_Result_TVG_RESULT_INSUFFICIENT_CONDITION => Err(TvgError::InsufficientCondition { + Tvg_Result_TVG_RESULT_INVALID_ARGUMENT => Err(DotLottiePlayerError::TvgInvalidArgument { function_name: func_name, }), - Tvg_Result_TVG_RESULT_FAILED_ALLOCATION => Err(TvgError::FailedAllocation { + Tvg_Result_TVG_RESULT_INSUFFICIENT_CONDITION => { + Err(DotLottiePlayerError::TvgInsufficientCondition { + function_name: func_name, + }) + } + Tvg_Result_TVG_RESULT_FAILED_ALLOCATION => Err(DotLottiePlayerError::TvgFailedAllocation { function_name: func_name, }), - Tvg_Result_TVG_RESULT_MEMORY_CORRUPTION => Err(TvgError::MemoryCorruption { + Tvg_Result_TVG_RESULT_MEMORY_CORRUPTION => Err(DotLottiePlayerError::TvgMemoryCorruption { function_name: func_name, }), - Tvg_Result_TVG_RESULT_NOT_SUPPORTED => Err(TvgError::NotSupported { + Tvg_Result_TVG_RESULT_NOT_SUPPORTED => Err(DotLottiePlayerError::TvgNotSupported { function_name: func_name, }), - Tvg_Result_TVG_RESULT_UNKNOWN | _ => Err(TvgError::Unknown { + Tvg_Result_TVG_RESULT_UNKNOWN | _ => Err(DotLottiePlayerError::TvgUnknown { function_name: func_name, }), } @@ -100,7 +82,7 @@ impl Canvas { width: u32, height: u32, color_space: TvgColorspace, - ) -> Result<(), TvgError> { + ) -> Result<(), DotLottiePlayerError> { let color_space = match color_space { TvgColorspace::ABGR8888 => Tvg_Colorspace_TVG_COLORSPACE_ABGR8888, TvgColorspace::ABGR8888S => Tvg_Colorspace_TVG_COLORSPACE_ABGR8888S, @@ -122,31 +104,31 @@ impl Canvas { convert_tvg_result(result, "tvg_swcanvas_set_target") } - pub fn clear(&self, free: bool) -> Result<(), TvgError> { + pub fn clear(&self, free: bool) -> Result<(), DotLottiePlayerError> { let result = unsafe { tvg_canvas_clear(self.raw_canvas, free) }; convert_tvg_result(result, "tvg_canvas_clear") } - pub fn push(&mut self, drawable: &T) -> Result<(), TvgError> { + pub fn push(&mut self, drawable: &T) -> Result<(), DotLottiePlayerError> { let result = unsafe { tvg_canvas_push(self.raw_canvas, drawable.as_raw_paint()) }; convert_tvg_result(result, "tvg_canvas_push") } - pub fn draw(&mut self) -> Result<(), TvgError> { + pub fn draw(&mut self) -> Result<(), DotLottiePlayerError> { let result = unsafe { tvg_canvas_draw(self.raw_canvas) }; convert_tvg_result(result, "tvg_canvas_draw") } - pub fn sync(&mut self) -> Result<(), TvgError> { + pub fn sync(&mut self) -> Result<(), DotLottiePlayerError> { let result = unsafe { tvg_canvas_sync(self.raw_canvas) }; convert_tvg_result(result, "tvg_canvas_sync") } - pub fn update(&mut self) -> Result<(), TvgError> { + pub fn update(&mut self) -> Result<(), DotLottiePlayerError> { let result = unsafe { tvg_canvas_update(self.raw_canvas) }; convert_tvg_result(result, "tvg_canvas_update") @@ -178,7 +160,12 @@ impl Animation { } } - pub fn load_data(&mut self, data: &str, mimetype: &str, copy: bool) -> Result<(), TvgError> { + pub fn load_data( + &mut self, + data: &str, + mimetype: &str, + copy: bool, + ) -> Result<(), DotLottiePlayerError> { let mimetype = CString::new(mimetype).expect("Failed to create CString"); let data = CString::new(data).expect("Failed to create CString"); @@ -198,7 +185,7 @@ impl Animation { Ok(()) } - pub fn get_size(&self) -> Result<(f32, f32), TvgError> { + pub fn get_size(&self) -> Result<(f32, f32), DotLottiePlayerError> { let mut width = 0.0; let mut height = 0.0; @@ -216,25 +203,25 @@ impl Animation { Ok((width, height)) } - pub fn set_size(&mut self, width: f32, height: f32) -> Result<(), TvgError> { + pub fn set_size(&mut self, width: f32, height: f32) -> Result<(), DotLottiePlayerError> { let result = unsafe { tvg_picture_set_size(self.raw_paint, width, height) }; convert_tvg_result(result, "tvg_picture_set_size") } - pub fn scale(&mut self, factor: f32) -> Result<(), TvgError> { + pub fn scale(&mut self, factor: f32) -> Result<(), DotLottiePlayerError> { let result = unsafe { tvg_paint_scale(self.raw_paint, factor) }; convert_tvg_result(result, "tvg_paint_scale") } - pub fn translate(&mut self, tx: f32, ty: f32) -> Result<(), TvgError> { + pub fn translate(&mut self, tx: f32, ty: f32) -> Result<(), DotLottiePlayerError> { let result = unsafe { tvg_paint_translate(self.raw_paint, tx, ty) }; convert_tvg_result(result, "tvg_paint_translate") } - pub fn get_total_frame(&self) -> Result { + pub fn get_total_frame(&self) -> Result { let mut total_frame: f32 = 0.0; let result = unsafe { @@ -246,7 +233,7 @@ impl Animation { return Ok(total_frame); } - pub fn get_duration(&self) -> Result { + pub fn get_duration(&self) -> Result { let mut duration: f32 = 0.0; let result = @@ -257,13 +244,13 @@ impl Animation { return Ok(duration); } - pub fn set_frame(&mut self, frame_no: f32) -> Result<(), TvgError> { + pub fn set_frame(&mut self, frame_no: f32) -> Result<(), DotLottiePlayerError> { let result = unsafe { tvg_animation_set_frame(self.raw_animation, frame_no) }; convert_tvg_result(result, "tvg_animation_set_frame") } - pub fn get_frame(&self) -> Result { + pub fn get_frame(&self) -> Result { let mut curr_frame: f32 = 0.0; let result = unsafe { tvg_animation_get_frame(self.raw_animation, &mut curr_frame as *mut f32) }; @@ -273,7 +260,7 @@ impl Animation { return Ok(curr_frame); } - pub fn set_slots(&mut self, slots: &str) -> Result<(), TvgError> { + pub fn set_slots(&mut self, slots: &str) -> Result<(), DotLottiePlayerError> { let result = if slots.is_empty() { unsafe { tvg_lottie_animation_override(self.raw_animation, ptr::null()) } } else { @@ -310,7 +297,7 @@ impl Shape { } } - pub fn fill(&mut self, color: (u8, u8, u8, u8)) -> Result<(), TvgError> { + pub fn fill(&mut self, color: (u8, u8, u8, u8)) -> Result<(), DotLottiePlayerError> { let result = unsafe { tvg_shape_set_fill_color(self.raw_shape, color.0, color.1, color.2, color.3) }; @@ -325,13 +312,13 @@ impl Shape { h: f32, rx: f32, ry: f32, - ) -> Result<(), TvgError> { + ) -> Result<(), DotLottiePlayerError> { let result = unsafe { tvg_shape_append_rect(self.raw_shape, x, y, w, h, rx, ry) }; convert_tvg_result(result, "tvg_shape_append_rect") } - pub fn reset(&mut self) -> Result<(), TvgError> { + pub fn reset(&mut self) -> Result<(), DotLottiePlayerError> { let result = unsafe { tvg_shape_reset(self.raw_shape) }; convert_tvg_result(result, "tvg_shape_reset") diff --git a/dotlottie-rs/src/utils.rs b/dotlottie-rs/src/utils.rs new file mode 100644 index 00000000..4a3da0ab --- /dev/null +++ b/dotlottie-rs/src/utils.rs @@ -0,0 +1,83 @@ +pub fn base64_encode(plain: &[u8]) -> String { + const BASE64_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + plain + .chunks(3) + .flat_map(|chunk| { + let (b1, b2, b3) = + match *chunk { + [b1, b2, b3] => (b1, b2, b3), + [b1, b2] => (b1, b2, 0), + [b1] => (b1, 0, 0), + _ => (0, 0, 0), + }; + [ + BASE64_CHARS[(b1 >> 2) as usize], + BASE64_CHARS[((b1 & 0x03) << 4 | (b2 >> 4)) as usize], + if chunk.len() > 1 { + BASE64_CHARS[((b2 & 0x0f) << 2 | (b3 >> 6)) as usize] + } else { + b'=' + }, + if chunk.len() == 3 { + BASE64_CHARS[(b3 & 0x3f) as usize] + } else { + b'=' + }, + ] + }) + .map(|b| b as char) + .collect() +} + +#[cfg(test)] +mod tests { + use super::base64_encode; + + #[test] + fn test_basic_encoding() { + let inputs_and_expected = + vec![ + (b"hello", "aGVsbG8="), + (b"world", "d29ybGQ="), + (b"rusty", "cnVzdHk="), + ]; + + for (input, expected) in inputs_and_expected { + assert_eq!(base64_encode(input), expected); + } + } + + #[test] + fn test_edge_cases() { + // Test empty input + assert_eq!(base64_encode(b""), ""); + + // Test input length is a multiple of 3 + assert_eq!(base64_encode(b"abc"), "YWJj"); + + // Test input length is not a multiple of 3 + assert_eq!(base64_encode(b"ab"), "YWI="); + assert_eq!(base64_encode(b"a"), "YQ=="); + } + + #[test] + fn test_special_characters() { + // Include numbers, plus, and slash which are part of the base64 character set + assert_eq!(base64_encode(b"123+"), "MTIzKw=="); + assert_eq!(base64_encode(b"/456"), "LzQ1Ng=="); + } + + #[test] + fn test_large_input() { + let large_input = vec![b'a'; 1000]; // 1000 'a's + // This is a simplistic approach; a real test might validate the length or specific patterns in the output + assert!(!base64_encode(&large_input).is_empty()); + } + + #[test] + fn test_known_values() { + // Using known base64 encoded strings to validate against + assert_eq!(base64_encode(b"OpenAI"), "T3BlbkFJ"); + assert_eq!(base64_encode(b"ChatGPT"), "Q2hhdEdQVA=="); + } +}