Files
effectcraft/crates/lottie/src/lib.rs
T
Brandon ThomasandClaude Opus 5.5 1da7dc4659 Never crash: Lottie enum values can't overflow
Lottie stores line cap, line join, merge mode and some text-selector enums 1-based, and the
importer subtracted 1 from whatever number the file held: -1e308 (rounded to i64::MIN)
overflowed, a panic in debug builds (found by structural fuzzing). Use saturating_sub. Denies
the panic lints in the crate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:24:14 -04:00

102 lines
3.5 KiB
Rust

//! Lottie JSON import and export.
//!
//! Implemented from the public Lottie format description (the lottie-spec 1.0 schema and the
//! LottieFiles format documentation); no player or exporter source was used.
//!
//! **Export** ([`export_comp`]) turns a composition into a Lottie document: nested compositions
//! become precomp assets, still footage becomes embedded base64 image assets, and shape, solid,
//! null, precomp, image and text layers map to their Lottie layer types (text optionally as glyph
//! shapes). Transforms (with parenting, separated position and anchor), keyframes (temporal ease
//! converted from speed/influence to normalised Bezier `i`/`o` tangents, hold keys, spatial
//! `ti`/`to` tangents), shape contents, masks, track mattes, blend modes, time remapping, layer
//! timing and composition markers are exported. Anything Lottie cannot express (most effects,
//! cameras and lights, layer styles, audio, video footage…) is listed in
//! [`ExportResult::warnings`].
//!
//! **Import** ([`import`]) is the inverse mapping into a new composition.
//!
//! Keyframe times in Lottie are frames of the layer's local time, which is EffectCraft's layer
//! time; layer `ip`/`op`/`st` are composition frames and `sr` is the stretch factor.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
mod anim;
mod base64;
mod export;
mod import;
mod shapes;
mod text;
pub mod zip;
use effectcraft_color::BlendMode;
use effectcraft_time::{FrameRate, TICKS_PER_SECOND, Tick};
pub use export::{ExportOptions, ExportResult, export_comp, to_dotlottie};
pub use import::{ImportResult, ImportedImage, import};
#[derive(Debug, thiserror::Error)]
pub enum LottieError {
#[error("not a Lottie document: {0}")]
Parse(String),
#[error("{0}")]
Invalid(String),
}
/// Frame ↔ tick conversion at a frame rate.
#[derive(Clone, Copy, Debug)]
pub(crate) struct Timebase {
pub rate: FrameRate,
}
impl Timebase {
/// Frame number (fractional) of a time.
pub fn frame(&self, t: Tick) -> f64 {
let f = t.0 as f64 * self.rate.num as f64 / (self.rate.den as f64 * TICKS_PER_SECOND as f64);
// Keep exact frame numbers clean (no 23.999999).
if (f - f.round()).abs() < 1e-6 { f.round() } else { f }
}
pub fn tick(&self, f: f64) -> Tick {
Tick((f * TICKS_PER_SECOND as f64 * self.rate.den as f64 / self.rate.num as f64).round() as i64)
}
}
/// Lottie layer / shape blend modes (`bm`) in order.
const LOTTIE_BLEND: [BlendMode; 18] = [
BlendMode::Normal,
BlendMode::Multiply,
BlendMode::Screen,
BlendMode::Overlay,
BlendMode::Darken,
BlendMode::Lighten,
BlendMode::ColorDodge,
BlendMode::ColorBurn,
BlendMode::HardLight,
BlendMode::SoftLight,
BlendMode::Difference,
BlendMode::Exclusion,
BlendMode::Hue,
BlendMode::Saturation,
BlendMode::Color,
BlendMode::Luminosity,
BlendMode::Add,
BlendMode::HardMix,
];
/// Lottie `bm` of a blend mode (`None` = Lottie has no equivalent).
pub(crate) fn blend_to_lottie(m: BlendMode) -> Option<u32> {
LOTTIE_BLEND.iter().position(|b| *b == m).map(|i| i as u32).or(match m {
BlendMode::LinearDodge => Some(16),
BlendMode::ClassicColorDodge => Some(6),
BlendMode::ClassicColorBurn => Some(7),
BlendMode::ClassicDifference => Some(10),
_ => None,
})
}
pub(crate) fn blend_from_lottie(bm: u64) -> BlendMode {
LOTTIE_BLEND.get(bm as usize).copied().unwrap_or_default()
}
#[cfg(test)]
mod tests;