Skip to main content

ravenrs_lottie/
document.rs

1//! A Lottie file, read.
2
3use std::collections::{BTreeSet, HashMap};
4use std::fmt;
5use std::sync::Arc;
6
7use ravenrs_core::{ImageData, PathFill};
8use serde_json::Value;
9
10use crate::player;
11use crate::property::{Numbers, Shape, numbers};
12
13/// Why a file could not be read.
14#[derive(Clone, Debug, PartialEq, Eq)]
15#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
16pub enum LottieError {
17    /// It is not JSON.
18    NotJson(String),
19    /// It is JSON, but not a Lottie: this field is missing.
20    Missing(&'static str),
21}
22
23impl fmt::Display for LottieError {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        match self {
26            Self::NotJson(why) => write!(f, "the file is not JSON: {why}"),
27            Self::Missing(field) => write!(f, "the file is not a Lottie: it has no `{field}`"),
28        }
29    }
30}
31
32impl std::error::Error for LottieError {}
33
34#[derive(Clone, Debug)]
35pub(crate) struct Transform {
36    pub anchor: Numbers,
37    pub position: Numbers,
38    pub split: Option<(Numbers, Numbers)>,
39    pub scale: Numbers,
40    pub rotation: Numbers,
41    pub opacity: Numbers,
42    pub skew: Numbers,
43    pub skew_axis: Numbers,
44    /// Turns the layer along its path of motion.
45    pub orient: bool,
46}
47
48#[derive(Clone, Debug)]
49pub(crate) struct GradientPaint {
50    pub radial: bool,
51    pub start: Numbers,
52    pub end: Numbers,
53    pub stops: Numbers,
54    pub count: usize,
55}
56
57#[derive(Clone, Debug)]
58pub(crate) struct StrokeLook {
59    pub width: Numbers,
60    pub cap: i64,
61    pub join: i64,
62    pub miter_limit: f32,
63    pub dashes: Vec<(String, Numbers)>,
64}
65
66#[derive(Clone, Debug)]
67pub(crate) enum Item {
68    Group(Vec<Item>, Option<Transform>),
69    Path(Shape),
70    Rect {
71        position: Numbers,
72        size: Numbers,
73        radius: Numbers,
74    },
75    Ellipse {
76        position: Numbers,
77        size: Numbers,
78    },
79    Star {
80        polygon: bool,
81        position: Numbers,
82        rotation: Numbers,
83        points: Numbers,
84        inner: Numbers,
85        outer: Numbers,
86        inner_round: Numbers,
87        outer_round: Numbers,
88    },
89    Fill {
90        color: Numbers,
91        opacity: Numbers,
92        even_odd: bool,
93    },
94    Stroke {
95        color: Numbers,
96        opacity: Numbers,
97        look: StrokeLook,
98    },
99    GradientFill {
100        paint: GradientPaint,
101        opacity: Numbers,
102        even_odd: bool,
103    },
104    GradientStroke {
105        paint: GradientPaint,
106        opacity: Numbers,
107        look: StrokeLook,
108    },
109    Trim {
110        start: Numbers,
111        end: Numbers,
112        offset: Numbers,
113        together: bool,
114    },
115    Repeater {
116        copies: Numbers,
117        offset: Numbers,
118        transform: Transform,
119        start_opacity: Numbers,
120        end_opacity: Numbers,
121    },
122    RoundedCorners(Numbers),
123}
124
125#[derive(Clone, Debug)]
126pub(crate) enum LayerKind {
127    Shape(Vec<Item>),
128    Solid { width: f32, height: f32, color: [f32; 3] },
129    Image { image: ImageData, width: f32, height: f32 },
130    Precomp { reference: String, width: f32, height: f32, remap: Option<Numbers> },
131    Empty,
132}
133
134#[derive(Clone, Debug)]
135pub(crate) struct Mask {
136    pub path: Shape,
137    pub subtract: bool,
138    pub inverted: bool,
139}
140
141#[derive(Clone, Debug)]
142pub(crate) struct Layer {
143    pub kind: LayerKind,
144    pub index: Option<i64>,
145    pub parent: Option<i64>,
146    pub transform: Transform,
147    pub in_frame: f32,
148    pub out_frame: f32,
149    pub start: f32,
150    pub stretch: f32,
151    pub hidden: bool,
152    pub masks: Vec<Mask>,
153    /// How the layer above it, or the one `matte_index` names, masks it: 1 and 3 keep
154    /// what it covers, 2 and 4 what it does not.
155    pub matte_mode: i64,
156    pub matte_index: Option<i64>,
157    pub is_matte: bool,
158    /// The Fill effect, which paints everything in the layer one color and opacity.
159    pub fill_effect: Option<(Numbers, Numbers)>,
160}
161
162#[derive(Debug)]
163pub(crate) struct Document {
164    pub width: f32,
165    pub height: f32,
166    pub frame_rate: f32,
167    pub in_frame: f32,
168    pub out_frame: f32,
169    pub layers: Vec<Layer>,
170    pub precomps: HashMap<String, Vec<Layer>>,
171    pub unsupported: BTreeSet<String>,
172}
173
174/// A Lottie animation. Cloning is cheap, and it can be drawn at any moment.
175///
176/// ```
177/// use ravenrs_lottie::Animation;
178///
179/// let json = br#"{"v":"5.5.7","fr":30,"ip":0,"op":30,"w":100,"h":100,"layers":[]}"#;
180/// let animation = Animation::from_json(json).expect("a Lottie");
181/// assert_eq!(animation.duration(), 1.0);
182/// assert!(animation.paths_at(0.5, 1.0).is_empty());
183/// ```
184#[derive(Clone, Debug)]
185#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
186pub struct Animation(pub(crate) Arc<Document>);
187
188fn number(value: Option<&Value>, default: f32) -> f32 {
189    value.and_then(Value::as_f64).map_or(default, |v| v as f32)
190}
191
192struct Reader {
193    unsupported: BTreeSet<String>,
194    images: HashMap<String, (ImageData, f32, f32)>,
195}
196
197impl Reader {
198    /// The color and opacity of a Fill effect, which go by their place among its controls.
199    fn fill_effect(&mut self, effect: &Value) -> (Numbers, Numbers) {
200        let controls = effect.get("ef").and_then(Value::as_array).map_or(&[][..], Vec::as_slice);
201        let control = |index: i64| {
202            controls
203                .iter()
204                .find(|c| c.get("ix").and_then(Value::as_i64) == Some(index))
205                .and_then(|c| c.get("v"))
206        };
207        if control(1).and_then(|v| v.get("k")).and_then(Value::as_f64).is_some_and(|mask| mask != 0.0) {
208            self.note("efeito Fill limitado a uma máscara, aplicado à camada toda");
209        }
210        (self.numbers(control(3)), self.numbers(control(7)))
211    }
212
213    fn note(&mut self, what: &str) {
214        self.unsupported.insert(what.to_owned());
215    }
216
217    /// Reads a property and notes an expression, whose saved value is what is drawn.
218    fn numbers(&mut self, property: Option<&Value>) -> Numbers {
219        if property.and_then(|p| p.get("x")).and_then(Value::as_str).is_some_and(|x| !x.is_empty()) {
220            self.note("expressões");
221        }
222        Numbers::parse(property)
223    }
224
225    fn transform(&mut self, value: Option<&Value>) -> Transform {
226        let empty = Value::Null;
227        let v = value.unwrap_or(&empty);
228        let position = v.get("p");
229        let split = position
230            .filter(|p| {
231                p.get("s").and_then(Value::as_bool) == Some(true)
232                    || p.get("s").and_then(Value::as_i64) == Some(1)
233            })
234            .map(|p| (self.numbers(p.get("x")), self.numbers(p.get("y"))));
235        if v.get("rx").is_some() || v.get("ry").is_some() {
236            self.note("rotação em 3D");
237        }
238        Transform {
239            anchor: self.numbers(v.get("a")),
240            position: self.numbers(position),
241            split,
242            scale: self.numbers(v.get("s")),
243            rotation: self.numbers(v.get("r").or_else(|| v.get("rz"))),
244            opacity: self.numbers(v.get("o")),
245            skew: self.numbers(v.get("sk")),
246            skew_axis: self.numbers(v.get("sa")),
247            orient: false,
248        }
249    }
250
251    fn stroke_look(&mut self, item: &Value) -> StrokeLook {
252        let dashes = item
253            .get("d")
254            .and_then(Value::as_array)
255            .map(|list| {
256                list.iter()
257                    .map(|dash| {
258                        (
259                            dash.get("n").and_then(Value::as_str).unwrap_or("d").to_owned(),
260                            self.numbers(dash.get("v")),
261                        )
262                    })
263                    .collect()
264            })
265            .unwrap_or_default();
266        StrokeLook {
267            width: self.numbers(item.get("w")),
268            cap: item.get("lc").and_then(Value::as_i64).unwrap_or(1),
269            join: item.get("lj").and_then(Value::as_i64).unwrap_or(1),
270            miter_limit: number(item.get("ml"), 4.0),
271            dashes,
272        }
273    }
274
275    fn gradient(&mut self, item: &Value) -> GradientPaint {
276        if item
277            .get("h")
278            .is_some_and(|h| !numbers(h.get("k").unwrap_or(&Value::Null)).iter().all(|v| *v == 0.0))
279        {
280            self.note("gradiente com foco deslocado");
281        }
282        let g = item.get("g");
283        GradientPaint {
284            radial: item.get("t").and_then(Value::as_i64) == Some(2),
285            start: self.numbers(item.get("s")),
286            end: self.numbers(item.get("e")),
287            stops: self.numbers(g.and_then(|g| g.get("k"))),
288            count: g.and_then(|g| g.get("p")).and_then(Value::as_u64).unwrap_or(0) as usize,
289        }
290    }
291
292    fn items(&mut self, list: Option<&Value>) -> Vec<Item> {
293        let Some(list) = list.and_then(Value::as_array) else { return Vec::new() };
294        let mut items = Vec::with_capacity(list.len());
295        for item in list {
296            if item.get("hd").and_then(Value::as_bool) == Some(true) {
297                continue;
298            }
299            let even_odd = item.get("r").and_then(Value::as_i64) == Some(2);
300            let kind = item.get("ty").and_then(Value::as_str).unwrap_or("");
301            let read = match kind {
302                "gr" => {
303                    let inner = item.get("it").and_then(Value::as_array);
304                    let transform = inner
305                        .and_then(|list| {
306                            list.iter().find(|i| i.get("ty").and_then(Value::as_str) == Some("tr"))
307                        })
308                        .map(|tr| self.transform(Some(tr)));
309                    Some(Item::Group(self.items(item.get("it")), transform))
310                }
311                // Read with its group.
312                "tr" => None,
313                "sh" => Some(Item::Path(Shape::parse(item.get("ks")))),
314                "rc" => Some(Item::Rect {
315                    position: self.numbers(item.get("p")),
316                    size: self.numbers(item.get("s")),
317                    radius: self.numbers(item.get("r")),
318                }),
319                "el" => Some(Item::Ellipse {
320                    position: self.numbers(item.get("p")),
321                    size: self.numbers(item.get("s")),
322                }),
323                "sr" => Some(Item::Star {
324                    polygon: item.get("sy").and_then(Value::as_i64) == Some(2),
325                    position: self.numbers(item.get("p")),
326                    rotation: self.numbers(item.get("r")),
327                    points: self.numbers(item.get("pt")),
328                    inner: self.numbers(item.get("ir")),
329                    outer: self.numbers(item.get("or")),
330                    inner_round: self.numbers(item.get("is")),
331                    outer_round: self.numbers(item.get("os")),
332                }),
333                "fl" => Some(Item::Fill {
334                    color: self.numbers(item.get("c")),
335                    opacity: self.numbers(item.get("o")),
336                    even_odd,
337                }),
338                "st" => Some(Item::Stroke {
339                    color: self.numbers(item.get("c")),
340                    opacity: self.numbers(item.get("o")),
341                    look: self.stroke_look(item),
342                }),
343                "gf" => Some(Item::GradientFill {
344                    paint: self.gradient(item),
345                    opacity: self.numbers(item.get("o")),
346                    even_odd,
347                }),
348                "gs" => Some(Item::GradientStroke {
349                    paint: self.gradient(item),
350                    opacity: self.numbers(item.get("o")),
351                    look: self.stroke_look(item),
352                }),
353                "tm" => Some(Item::Trim {
354                    start: self.numbers(item.get("s")),
355                    end: self.numbers(item.get("e")),
356                    offset: self.numbers(item.get("o")),
357                    together: item.get("m").and_then(Value::as_i64) == Some(2),
358                }),
359                "rp" => {
360                    let tr = item.get("tr");
361                    Some(Item::Repeater {
362                        copies: self.numbers(item.get("c")),
363                        offset: self.numbers(item.get("o")),
364                        transform: self.transform(tr),
365                        start_opacity: self.numbers(tr.and_then(|t| t.get("so"))),
366                        end_opacity: self.numbers(tr.and_then(|t| t.get("eo"))),
367                    })
368                }
369                "rd" => Some(Item::RoundedCorners(self.numbers(item.get("r")))),
370                "mm" => {
371                    self.note("juntar caminhos (merge paths)");
372                    None
373                }
374                "tw" => {
375                    self.note("torcer (twist)");
376                    None
377                }
378                "pb" => {
379                    self.note("inflar e contrair (pucker and bloat)");
380                    None
381                }
382                "op" => {
383                    self.note("deslocar caminho (offset path)");
384                    None
385                }
386                other => {
387                    self.note(&format!("forma do tipo {other}"));
388                    None
389                }
390            };
391            items.extend(read);
392        }
393        items
394    }
395
396    fn layers(&mut self, list: Option<&Value>) -> Vec<Layer> {
397        let Some(list) = list.and_then(Value::as_array) else { return Vec::new() };
398        let mut layers = Vec::with_capacity(list.len());
399        for layer in list {
400            let kind = match layer.get("ty").and_then(Value::as_i64).unwrap_or(3) {
401                0 => {
402                    let remap = layer.get("tm").map(|tm| self.numbers(Some(tm)));
403                    LayerKind::Precomp {
404                        reference: layer.get("refId").and_then(Value::as_str).unwrap_or("").to_owned(),
405                        width: number(layer.get("w"), 0.0),
406                        height: number(layer.get("h"), 0.0),
407                        remap,
408                    }
409                }
410                1 => LayerKind::Solid {
411                    width: number(layer.get("sw"), 0.0),
412                    height: number(layer.get("sh"), 0.0),
413                    color: solid_color(layer.get("sc").and_then(Value::as_str).unwrap_or("#000000")),
414                },
415                2 => {
416                    let found = layer.get("refId").and_then(Value::as_str).and_then(|id| self.images.get(id));
417                    match found {
418                        Some((image, width, height)) => {
419                            LayerKind::Image { image: image.clone(), width: *width, height: *height }
420                        }
421                        None => {
422                            self.note("imagens fora do arquivo");
423                            LayerKind::Empty
424                        }
425                    }
426                }
427                3 => LayerKind::Empty,
428                4 => LayerKind::Shape(self.items(layer.get("shapes"))),
429                5 => {
430                    self.note("texto");
431                    LayerKind::Empty
432                }
433                other => {
434                    self.note(&format!("camada do tipo {other}"));
435                    LayerKind::Empty
436                }
437            };
438            if layer.get("bm").and_then(Value::as_i64).unwrap_or(0) != 0 {
439                self.note("modos de mistura");
440            }
441            let mut fill_effect = None;
442            for effect in layer.get("ef").and_then(Value::as_array).into_iter().flatten() {
443                if effect.get("en").and_then(Value::as_i64) == Some(0) {
444                    continue;
445                }
446                match effect.get("ty").and_then(Value::as_i64) {
447                    Some(21) => fill_effect = Some(self.fill_effect(effect)),
448                    _ => self.note("efeitos do After Effects"),
449                }
450            }
451            if layer.get("ddd").and_then(Value::as_i64) == Some(1) {
452                self.note("camadas em 3D");
453            }
454            let matte_mode = layer.get("tt").and_then(Value::as_i64).unwrap_or(0);
455            if matte_mode >= 3 {
456                self.note("matte de luminosidade, tratado como alfa");
457            }
458            let masks = layer
459                .get("masksProperties")
460                .and_then(Value::as_array)
461                .map(|list| {
462                    list.iter()
463                        .filter_map(|mask| {
464                            let mode = mask.get("mode").and_then(Value::as_str).unwrap_or("a");
465                            if mode == "n" {
466                                return None;
467                            }
468                            if mode != "a" && mode != "s" {
469                                self.note("máscara que não soma nem subtrai, tratada como soma");
470                            }
471                            Some(Mask {
472                                path: Shape::parse(mask.get("pt")),
473                                subtract: mode == "s",
474                                inverted: mask.get("inv").and_then(Value::as_bool).unwrap_or(false),
475                            })
476                        })
477                        .collect()
478                })
479                .unwrap_or_default();
480            let start = number(layer.get("st"), 0.0);
481            layers.push(Layer {
482                kind,
483                index: layer.get("ind").and_then(Value::as_i64),
484                parent: layer.get("parent").and_then(Value::as_i64),
485                transform: Transform {
486                    orient: layer.get("ao").and_then(Value::as_i64) == Some(1),
487                    ..self.transform(layer.get("ks"))
488                },
489                in_frame: number(layer.get("ip"), f32::MIN),
490                out_frame: number(layer.get("op"), f32::MAX),
491                start,
492                stretch: {
493                    let stretch = number(layer.get("sr"), 1.0);
494                    if stretch == 0.0 { 1.0 } else { stretch }
495                },
496                hidden: layer.get("hd").and_then(Value::as_bool).unwrap_or(false),
497                masks,
498                matte_mode,
499                matte_index: layer.get("tp").and_then(Value::as_i64),
500                is_matte: layer.get("td").and_then(Value::as_i64).unwrap_or(0) != 0,
501                fill_effect,
502            });
503        }
504        layers
505    }
506}
507
508/// The images a file carries inside it, by name, with the size each shows at.
509fn images_of(root: &Value) -> HashMap<String, (ImageData, f32, f32)> {
510    let mut images = HashMap::new();
511    for asset in root.get("assets").and_then(Value::as_array).into_iter().flatten() {
512        let (Some(id), Some(path)) =
513            (asset.get("id").and_then(Value::as_str), asset.get("p").and_then(Value::as_str))
514        else {
515            continue;
516        };
517        let Some(image) = path.split_once(";base64,").and_then(|(_, data)| decoded_image(data)) else {
518            continue;
519        };
520        let width = number(asset.get("w"), image.width() as f32);
521        let height = number(asset.get("h"), image.height() as f32);
522        images.insert(id.to_owned(), (image, width, height));
523    }
524    images
525}
526
527fn decoded_image(base64: &str) -> Option<ImageData> {
528    let picture = image::load_from_memory(&from_base64(base64)?).ok()?.into_rgba8();
529    ImageData::from_rgba8(picture.width(), picture.height(), picture.into_raw())
530}
531
532fn from_base64(text: &str) -> Option<Vec<u8>> {
533    let mut out = Vec::with_capacity(text.len() / 4 * 3);
534    let (mut bits, mut count) = (0u32, 0u32);
535    for byte in text.bytes() {
536        let value = match byte {
537            b'A'..=b'Z' => byte - b'A',
538            b'a'..=b'z' => byte - b'a' + 26,
539            b'0'..=b'9' => byte - b'0' + 52,
540            b'+' | b'-' => 62,
541            b'/' | b'_' => 63,
542            b'=' | b'\n' | b'\r' | b' ' => continue,
543            _ => return None,
544        };
545        bits = (bits << 6) | u32::from(value);
546        count += 6;
547        if count >= 8 {
548            count -= 8;
549            out.push((bits >> count) as u8);
550            bits &= (1 << count) - 1;
551        }
552    }
553    Some(out)
554}
555
556fn solid_color(text: &str) -> [f32; 3] {
557    let hex = text.trim_start_matches('#');
558    let part = |at: usize| {
559        hex.get(at..at + 2).and_then(|h| u8::from_str_radix(h, 16).ok()).map_or(0.0, |v| f32::from(v) / 255.0)
560    };
561    [part(0), part(2), part(4)]
562}
563
564#[cfg_attr(feature = "live", ravenrs_live::expose)]
565impl Animation {
566    /// Reads the JSON of a Lottie file.
567    pub fn from_json(bytes: &[u8]) -> Result<Self, LottieError> {
568        let root: Value = serde_json::from_slice(bytes).map_err(|e| LottieError::NotJson(e.to_string()))?;
569        let field =
570            |name: &'static str| root.get(name).and_then(Value::as_f64).ok_or(LottieError::Missing(name));
571        let (width, height) = (field("w")? as f32, field("h")? as f32);
572        let frame_rate = field("fr")? as f32;
573        let (in_frame, out_frame) = (field("ip")? as f32, field("op")? as f32);
574        if root.get("layers").is_none() {
575            return Err(LottieError::Missing("layers"));
576        }
577        let mut reader = Reader { unsupported: BTreeSet::new(), images: images_of(&root) };
578        let layers = reader.layers(root.get("layers"));
579        let mut precomps = HashMap::new();
580        if let Some(assets) = root.get("assets").and_then(Value::as_array) {
581            for asset in assets {
582                if let (Some(id), Some(_)) = (asset.get("id").and_then(Value::as_str), asset.get("layers")) {
583                    let inner = reader.layers(asset.get("layers"));
584                    precomps.insert(id.to_owned(), inner);
585                }
586            }
587        }
588        Ok(Self(Arc::new(Document {
589            width,
590            height,
591            frame_rate: frame_rate.max(1.0),
592            in_frame,
593            out_frame: out_frame.max(in_frame),
594            layers,
595            precomps,
596            unsupported: reader.unsupported,
597        })))
598    }
599
600    /// The width of its composition.
601    pub fn width(&self) -> f32 {
602        self.0.width
603    }
604
605    /// The height of its composition.
606    pub fn height(&self) -> f32 {
607        self.0.height
608    }
609
610    /// How long it runs, in seconds.
611    pub fn duration(&self) -> f64 {
612        f64::from(self.0.out_frame - self.0.in_frame) / f64::from(self.0.frame_rate)
613    }
614
615    /// Its frames per second, which only says how it was authored: it can be drawn at
616    /// any moment in between.
617    pub fn frame_rate(&self) -> f32 {
618        self.0.frame_rate
619    }
620
621    /// What the file asks for that is not drawn as asked, in words a person reads.
622    /// Empty when nothing is missing.
623    pub fn unsupported(&self) -> Vec<String> {
624        self.0.unsupported.iter().cloned().collect()
625    }
626
627    /// The shapes to draw at this moment, in painter's order and in the coordinates of
628    /// the composition. `scale` is how many device pixels a unit will take: curves that
629    /// become straight pieces, as in a stroke, are cut finely enough for it.
630    pub fn paths_at(&self, seconds: f64, scale: f32) -> Vec<PathFill> {
631        player::paths_at(&self.0, seconds, scale)
632    }
633}