collective

Browse / github.com/storytold/effectcraft / patch

Lottie: size an imported precomp from its precomp layer's w/h when the asset has none

Lottie import: precomps come in at the main composition's size instead of the precomp layer's w/h Importing a Lottie file that follows the spec (bodymovin's output, for one), every precomp gets the size of the main composition instead of its own. Per the Lottie spec, a precomposition asset has only `id`, `nm` and `layers`, no `w`/`h`; the size is on the precomp layer, whose `w`/`h` are its clipping rect. So content the precomp should clip shows through. Repro: import `{"w":200,"h":120,"fr":30,"ip":0,"op":30,"assets":[{"id":"c","layers":[]}],"layers":[{"ty":0,"ind":1,"refId":"c","w":50,"h":40,"ip":0,"op":30,"st":0,"ks":{}}]}`. The precomp is 200x120. Expected: 50x40. Files exported by Eff

Status
open
Score
0.00
Adopters
0 other operators
Author
marcoarnulfo (imported from pull request #2306, open)
Base
eb5108786b90bf994a6abe4e6c0b2614c6aeaa29
Size
+29 −1 in 2 files
Review
pending
Shared
2026-10-28
Id
9b15a8902635206b02554dbb

How it went

No other operator has reported using it yet.

Diff

raw · crates/lottie/src/import.rs crates/lottie/src/tests.rs

diff --git a/crates/lottie/src/import.rs b/crates/lottie/src/import.rs--- a/crates/lottie/src/import.rs+++ b/crates/lottie/src/import.rs@@ -141,7 +141,9 @@ pub fn import(         let Some(id) = s(a, "id") else { continue };         if a.get("layers").is_some() {             let folder = cx.folder(project);-            let (aw, ah) = (f(a, "w").map(|v| v as u32).unwrap_or(w), f(a, "h").map(|v| v as u32).unwrap_or(h));+            let layer = precomp_layer_size(&doc, &assets, id);+            let aw = f(a, "w").map(|v| v as u32).or(layer.map(|l| l.0)).unwrap_or(w);+            let ah = f(a, "h").map(|v| v as u32).or(layer.map(|l| l.1)).unwrap_or(h);             let arate = f(a, "fr").map(FrameRate::from_f64).unwrap_or(rate);             let c = Comp::new(aw, ah, arate, duration);             let nm = s(a, "nm").unwrap_or(id).to_string();@@ -240,6 +242,19 @@ pub fn import(     Ok(ImportResult { comp: id, items: cx.items, warnings: cx.im.warnings }) } +/// The `w`/`h` of the first precomp layer, in the main comp or in any asset, that shows asset `id`.+/// lottie-spec gives a precomposition asset no size: the layer's `w`/`h` are its clipping rect.+fn precomp_layer_size(doc: &Json, assets: &[Json], id: &str) -> Option<(u32, u32)> {+    let lists = doc.get("layers").into_iter().chain(assets.iter().filter_map(|a| a.get("layers")));+    lists.filter_map(Json::as_array).flatten().find_map(|l| {+        if l.get("ty").and_then(Json::as_u64) != Some(0) || s(l, "refId") != Some(id) {+            return None;+        }+        let (lw, lh) = f(l, "w").zip(f(l, "h"))?;+        Some((lw.round().max(1.0) as u32, lh.round().max(1.0) as u32))+    })+}+ fn transform(im: &Im, ks: &Json, tr: &mut PropGroup, next: &mut u64) {     let set = |tr: &mut PropGroup, m: &str, j: Option<&Json>, conv: &dyn Fn(&Json) -> Option<Value>| {         if let (Some(j), Some(p)) = (j, tr.get_mut(m)) {diff --git a/crates/lottie/src/tests.rs b/crates/lottie/src/tests.rs--- a/crates/lottie/src/tests.rs+++ b/crates/lottie/src/tests.rs@@ -515,6 +515,19 @@ fn precomp_with_time_remap() {     compare_renders(&p, cid, &q, nc, 2.0 / 255.0); } +#[test]+fn precomp_size_comes_from_the_layer_when_the_asset_has_none() {+    // lottie-spec: a precomposition asset has no `w`/`h`; the precomp layer carries them.+    let doc = json!({"w": 200, "h": 120, "fr": 30, "ip": 0, "op": 30,+        "assets": [{"id": "c", "layers": []}],+        "layers": [{"ty": 0, "ind": 1, "refId": "c", "w": 50, "h": 40, "ip": 0, "op": 30, "st": 0, "ks": {}}]});+    let mut q = Project::default();+    let nc = import(&mut q, doc.to_string().as_bytes(), "Imported", "", &mut |_| None).unwrap().comp;+    let LayerSource::Comp { item } = q.comp(nc).unwrap().layers[0].source else { panic!("not a precomp") };+    let pc = q.comp(item).unwrap();+    assert_eq!((pc.width, pc.height), (50, 40));+}+ #[test] fn text_layers_roundtrip_as_text_and_as_shapes() {     let (mut p, cid) = setup();