//! Decoded `.fig` message → REST API file JSON, which [`crate::rest`] then imports. //! //! The message lists node changes; each names its id (`guid`), its parent and its position among //! the siblings (`parentIndex`). Nodes are read by field name, so files written with newer //! schemas still import what is understood. Instances carry only their overrides: their layers //! are expanded here from the main component (overrides and derived layout matched by id path, //! component property values applied) so the import looks as saved. use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::rc::Rc; use figcraft_doc::VectorNetwork; use figcraft_doc::geom::{Affine, Point}; use figcraft_doc::proto::{MAX_ACTION_DEPTH, MAX_EXPR_DEPTH}; use serde_json::{Map, Value, json}; use super::kiwi::Kv; use super::network; mod component; mod connector; mod document; mod layer; mod layout; mod paint; mod proto; mod text; mod variables; use component::OverrideScope; #[cfg(test)] use paint::paint; type Obj = Map; /// Deepest nesting followed. const MAX_DEPTH: usize = 256; /// Most path commands read from one geometry blob. const MAX_COMMANDS: usize = 200_000; const MAX_GRID_TRACKS: usize = 1_000; pub(super) const MAX_GRID_TRACK_VALUE: f64 = 1e6; /// Id of "no style". const NO_STYLE: &str = "4294967295:4294967295"; /// Longest asset `key` or `version` kept for resolving references given as assets. const MAX_ASSET_PART: usize = 256; /// The REST API file JSON for message `msg`, with the warnings about what was skipped and the /// vector networks its layers name by `vectorNetworkRef` (for [`crate::rest::import_with_networks`]). pub(super) fn to_rest(msg: &Kv, name: &str, max_nodes: usize) -> (Value, Vec, Vec>) { let mut m = Mapper::new(msg, max_nodes); let _canon = VarCanon::install(&m); let doc = m.document(); // Large values are moved in, never passed through `json!`, which serialises its arguments: // a deep copy of the whole tree, briefly holding it twice. let mut out = json!({"name": name}); out["document"] = doc; if !m.library_contexts.is_empty() { out[crate::rest::FIG_LIBRARY_CONTEXTS_FIELD] = Value::Array(std::mem::take(&mut m.library_contexts)); } // Mains in the file that the layer cap kept out: their instances are told apart from those of // components the file doesn't have (#981). if m.count >= m.max_nodes { let capped: Vec = m.referenced_mains.difference(&m.mapped_mains).map(|id| json!(id)).collect(); if !capped.is_empty() { out[crate::rest::FIG_CAPPED_MAINS_FIELD] = Value::Array(capped); } } if !m.styles.is_empty() { out["styles"] = Value::Object(std::mem::take(&mut m.styles)); } if !m.components.is_empty() { out["components"] = Value::Object(std::mem::take(&mut m.components)); } if !m.sets.is_empty() { out["componentSets"] = Value::Object(std::mem::take(&mut m.sets)); } if let Some(vars) = m.variables() { out["meta"] = vars; } let styles = m.style_definitions(); if !styles.is_empty() { out["styleDefinitions"] = Value::Array(styles); } (out, m.warnings.finish(), m.networks) } #[cfg(test)] mod image_filter_tests; /// REST importer warnings reworded for a `.fig` import (`None`: drop it). pub(super) fn adapt_warning(w: String) -> Option { if w.contains("geometry=paths") { return None; } Some(w.replace("the variables JSON", "the file").replace("the file JSON", "the file")) } // ---- small readers ---- /// A generated child is identified by one exact template GUID, not its array position. fn generated_record<'a>(v: &View<'a>, field: &str, local_id: u32) -> Option<&'a Kv<'a>> { v.get(field)?.get("overrides")?.list().iter().find(|row| { row.get("guidPath").and_then(|path| path.get("guids")).map(Kv::list).is_some_and(|guids| { guids.len() == 1 && num(&guids[0], "sessionID") == Some(40_000_000.0) && num(&guids[0], "localID") == Some(f64::from(local_id)) }) }) } fn num(n: &Kv, k: &str) -> Option { n.get(k).and_then(Kv::f64) } fn guid(v: &Kv) -> Option { Some(format!("{}:{}", num(v, "sessionID")? as u64, num(v, "localID")? as u64)) } #[cfg(test)] mod connector_snapshot_budget_tests; /// A `GUID`-holding reference (`StyleId`, `VariableID`, …): its `guid`. fn ref_guid(v: &Kv) -> Option { v.get("guid").and_then(guid).filter(|g| g != NO_STYLE) } fn vec2(v: &Kv) -> Option { Some(json!({"x": num(v, "x")?, "y": num(v, "y")?})) } fn size(v: &Kv) -> Option<(f64, f64)> { Some((num(v, "x")?, num(v, "y")?)) } fn matrix(m: &Kv) -> Option { let c = |k: &str| num(m, k); let a = Affine::new([c("m00")?, c("m10")?, c("m01")?, c("m11")?, c("m02")?, c("m12")?]); a.as_coeffs().iter().all(|x| x.is_finite()).then_some(a) } fn rows(a: Affine) -> Value { let [a, b, c, d, e, f] = a.as_coeffs(); json!([[a, c, e], [b, d, f]]) } fn copy_num(o: &mut Obj, n: &View, from: &str, to: &str) { if let Some(x) = n.num(from) { o.insert(to.into(), json!(x)); } } fn copy_str(o: &mut Obj, n: &View, from: &str, to: &str) { if let Some(s) = n.str(from) { o.insert(to.into(), json!(s)); } } /// A node seen through override layers: the last layer with a field wins. #[derive(Clone)] struct View<'a> { layers: Vec<&'a Kv<'a>>, /// Size and transform worked out from the constraints, over the layers' own. fitted: Option<((f64, f64), Affine)>, } impl<'a> View<'a> { fn of(n: &'a Kv<'a>) -> Self { Self { layers: vec![n], fitted: None } } fn size(&self) -> Option<(f64, f64)> { self.fitted.map(|f| f.0).or_else(|| self.get("size").and_then(size)) } fn transform(&self) -> Option { self.fitted.map(|f| f.1).or_else(|| self.get("transform").and_then(matrix)) } fn with(&self, top: impl IntoIterator>) -> Self { let mut v = self.clone(); v.layers.extend(top); v } /// This view with `style` just above the layer that links it in `key` (or `legacy`): the style /// beats that layer's and lower layers' copies of its fields (stale when the style changed /// since, or written for the style linked before), and override rows above the link beat it. fn styled(&self, style: Option<&'a Kv<'a>>, key: &str, legacy: &str) -> Self { let mut v = self.clone(); if let Some(s) = style { let linked_in = |k: &str| v.layers.iter().rposition(|l| l.get(k).is_some()); let at = linked_in(key).or_else(|| linked_in(legacy)).map_or(1, |i| i + 1); v.layers.insert(at.min(v.layers.len()), s); } v } fn get(&self, k: &str) -> Option<&'a Kv<'a>> { self.layers.iter().rev().find_map(|l| l.get(k)) } fn num(&self, k: &str) -> Option { self.get(k).and_then(Kv::f64) } fn str(&self, k: &str) -> Option<&'a str> { self.get(k).and_then(Kv::str) } fn flag(&self, k: &str) -> Option { self.get(k).and_then(Kv::bool) } fn list(&self, k: &str) -> &'a [Kv<'a>] { self.get(k).map(Kv::list).unwrap_or_default() } } struct Mapper<'a> { nodes: BTreeMap>, kids: BTreeMap>, blobs: Vec<&'a [u8]>, /// Component property definitions: id → (REST name "Name#id", type). prop_defs: BTreeMap, /// The copies of a set's definitions each variant keeps (no name or type of their own; the /// variant's layers and its instances' assignments use them): copy id → the definition's id. prop_parents: BTreeMap, /// File nodes by asset `key` and `version`: references can name a collection, variable or /// style as the asset instead of the guid, and the file carries the resources to resolve /// them (the collection node itself says the same `key`). assets: BTreeMap>, styles: Obj, /// Descriptions of components and component sets (REST `components` / `componentSets`). components: Obj, sets: Obj, /// Layers and keys of each main, built when an instance of it first needs them. override_scopes: BTreeMap>, /// Entries held by `override_scopes` (bounded by `max_nodes` together). override_scope_entries: usize, /// Component-bearing branches of the file's internal canvases (cached library definitions). library_contexts: Vec, /// Layers visited while scanning internal canvases, across all of them. library_scan_count: usize, warnings: crate::rest::Warnings, count: usize, /// The mains in the file that instances or instance swap defaults name, and the mains mapped /// as layers: the difference is what the layer cap kept out. referenced_mains: BTreeSet, mapped_mains: BTreeSet, /// Most layers produced or scanned, instance expansion included; the one place the cap comes /// from (see [`super::layer_budget`]). max_nodes: usize, /// Layers kept back for the visible pages' top level while library copies are mapped, so the /// copies can't take the whole budget and leave the pages empty. reserved: usize, /// Vector networks, each built once: the layers that show one (every instance of a main /// holding it) name it by `vectorNetworkRef`. Kept out of the JSON, where a large file's /// networks took gigabytes. networks: Vec>, /// Index in `networks` by what a network is made from: its blob, its scale and its style table. network_ids: HashMap<(usize, u64, u64, usize), usize>, /// Region paint elements (paints and gradient stops) read so far: one budget for the whole /// file, since layers with region paints don't share their network. region_paint_elements: usize, } impl<'a> Mapper<'a> { fn new(msg: &'a Kv<'a>, max_nodes: usize) -> Self { let mut nodes = BTreeMap::new(); let mut kids: BTreeMap> = BTreeMap::new(); let mut prop_defs = BTreeMap::new(); let mut prop_parents = BTreeMap::new(); let changes = msg.get("nodeChanges").map(Kv::list).unwrap_or_default(); // The last change of a GUID wins; a REMOVED change deletes what came before it. for n in changes { let Some(id) = n.get("guid").and_then(guid) else { continue }; if n.get("phase").and_then(Kv::str) == Some("REMOVED") { nodes.remove(&id); } else { nodes.insert(id, n); } } for n in changes { let Some(id) = n.get("guid").and_then(guid) else { continue }; if !nodes.get(&id).is_some_and(|latest| std::ptr::eq(*latest, n)) { continue; } // Variables and their collections are read from the node list (`of_type`), never // as layers: out of the layer tree, an instance's expansion doesn't walk them again for // every instance without counting them (#1077). if let Some(p) = n.get("parentIndex") && let Some(parent) = p.get("guid").and_then(guid) && !matches!(n.get("type").and_then(Kv::str), Some("VARIABLE" | "VARIABLE_SET")) { kids.entry(parent).or_default().push((p.get("position").and_then(Kv::str).unwrap_or(""), id.clone())); } for d in n.get("componentPropDefs").map(Kv::list).unwrap_or_default() { let Some(did) = d.get("id").and_then(guid) else { continue }; if let (Some(name), Some(ty)) = (d.get("name").and_then(Kv::str), d.get("type").and_then(Kv::str)) { prop_defs.insert(did.clone(), (format!("{name}#{did}"), ty)); } else if let Some(parent) = d.get("parentPropDefId").and_then(guid) { prop_parents.insert(did, parent); } } } for list in kids.values_mut() { list.sort(); list.dedup_by(|a, b| a.1 == b.1); } let blobs = msg.get("blobs").map(Kv::list).unwrap_or_default().iter().map(|b| b.get("bytes").and_then(Kv::bytes).unwrap_or_default()).collect(); let mut assets: BTreeMap> = BTreeMap::new(); for n in msg.get("nodeChanges").map(Kv::list).unwrap_or_default() { let (Some(key), Some(id)) = (n.get("key").and_then(Kv::str), n.get("guid").and_then(guid)) else { continue }; if key.is_empty() || key.len() > MAX_ASSET_PART { continue; } let version = n.get("version").and_then(Kv::str).filter(|v| !v.is_empty() && v.len() <= MAX_ASSET_PART).unwrap_or(""); assets.entry(key.to_string()).or_default().entry(version.to_string()).or_insert(id); } Self { nodes, kids, blobs, prop_defs, prop_parents, assets, styles: Obj::new(), components: Obj::new(), sets: Obj::new(), override_scopes: BTreeMap::new(), override_scope_entries: 0, library_contexts: Vec::new(), library_scan_count: 0, warnings: crate::rest::Warnings::default(), count: 0, referenced_mains: BTreeSet::new(), mapped_mains: BTreeSet::new(), max_nodes, reserved: 0, networks: Vec::new(), network_ids: HashMap::new(), region_paint_elements: 0, } } /// The id of the resource a `VariableSetID` (or `VariableID`, `StyleId`) names: its `guid`, /// else the file node its asset `key` and `version` names (a collection from a library is /// referenced as the asset, and the file's own node for it says the same `key`). `None` when /// the reference has neither or the asset names nothing (or ambiguously) in the file. /// Original code for this importer: the field names are those of the schema each file embeds. fn resource_id(&self, v: &Kv) -> Option { // A guid names the resource when this file holds a node by it. One from another file (the // library a style or collection came from, as a paste carries) names nothing here: the // asset it also names finds the file's copy (#1638). if let Some(g) = v.get("guid").and_then(guid).filter(|g| *g != NO_STYLE && self.nodes.contains_key(g)) { return Some(g); } let asset = v.get("assetRef")?; let key = asset.get("key").and_then(Kv::str)?; let versions = self.assets.get(key)?; let version = asset.get("version").and_then(Kv::str); match version { Some(v) => versions.get(v), // Without a version: an unversioned record, else the file's only one. None => versions.get("").or_else(|| (versions.len() == 1).then(|| versions.values().next()).flatten()), } .cloned() } /// The collection a `VariableSetID` names, as one collection per library asset: a file that /// took several updates of a library holds a copy of the collection for each version it met, /// all with the same asset `key`. Variables and explicit modes may name any of those copies, /// while the mode ids stay the same across them, so every copy maps to the newest one (the /// one [`Self::variables`] keeps, with the modes of the others added). Without this, a page set /// to a mode an older copy didn't have yet lost its mode, and the layers fell back to the /// default mode of the newest copy. pub(super) fn collection_id(&self, v: &Kv) -> Option { let id = self.resource_id(v)?; Some(self.canonical_collection(&id).unwrap_or(id)) } /// The newest live copy of collection node `id`'s library asset, when it has a key. pub(super) fn canonical_collection(&self, id: &str) -> Option { let key = self.nodes.get(id)?.get("key").and_then(Kv::str).filter(|k| !k.is_empty())?; let live_set = |n: &Kv| n.get("type").and_then(Kv::str) == Some("VARIABLE_SET") && n.get("isSoftDeleted").and_then(Kv::bool) != Some(true); self.assets .get(key)? .iter() .filter(|(_, nid)| self.nodes.get(nid.as_str()).is_some_and(|n| live_set(n))) .max_by_key(|(ver, _)| version_rank(ver)) .map(|(_, nid)| nid.clone()) } /// A note about what this file's mapping left out or changed. Counted, as the REST importer /// counts its own: these notes are met once per layer, so the kinds were named once each with /// no sign of how much of the file they covered -- a file whose every text layer lost a /// setting read the same as one where a single layer did (#467). The collector keeps the /// first-seen order and the same cap, since some notes name fields of the file (image /// filters). fn warn(&mut self, w: impl Into) { self.warnings.add(w.into()); } fn children(&self, id: &str) -> Vec { self.kids.get(id).map(|k| k.iter().map(|(_, c)| c.clone()).collect()).unwrap_or_default() } /// How many layers may be produced now: the cap, less what is kept for the visible pages. fn budget(&self) -> usize { self.max_nodes.saturating_sub(self.reserved) } } /// A library version (`"40727:1183"`) as numbers to order by; unparsable parts rank lowest. fn version_rank(v: &str) -> (u64, u64) { let mut parts = v.split(':').map(|p| p.trim().parse::().unwrap_or(0)); (parts.next().unwrap_or(0), parts.next().unwrap_or(0)) } /// Library variables, one per asset. A file that took several updates of a library keeps a copy /// of a variable for each version it met (same `key`, another `version`), and layers and other /// variables name them by that asset (`assetRef {key, version}`) rather than by guid. Each /// reference goes to the newest copy: it has the values of every mode the library has now, where /// an older one lacks the modes added since (a layer under a page set to such a mode fell back to /// the default one). Kept per thread while a file is mapped: the paint and variable readers that /// meet references are free functions. #[derive(Default)] pub(super) struct VarCanon { by_key: HashMap, by_id: HashMap, } thread_local! { static VAR_CANON: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; } /// Removes the thread's table when the mapping is done. pub(super) struct VarCanonGuard; impl Drop for VarCanonGuard { fn drop(&mut self) { VAR_CANON.with(|c| c.borrow_mut().take()); } } impl VarCanon { fn install(m: &Mapper) -> VarCanonGuard { let mut newest: HashMap = HashMap::new(); let mut copies: Vec<(String, String)> = Vec::new(); for (id, n) in &m.nodes { if n.get("type").and_then(Kv::str) != Some("VARIABLE") || n.get("isSoftDeleted").and_then(Kv::bool) == Some(true) { continue; } let Some(key) = n.get("key").and_then(Kv::str).filter(|k| !k.is_empty()) else { continue }; let rank = version_rank(n.get("version").and_then(Kv::str).unwrap_or("")); copies.push((id.clone(), key.to_string())); let e = newest.entry(key.to_string()).or_insert_with(|| (rank, id.clone())); if rank > e.0 || (rank == e.0 && *id > e.1) { *e = (rank, id.clone()); } } let mut t = VarCanon::default(); for (id, key) in copies { if let Some((_, canon)) = newest.get(&key) { t.by_id.insert(id, canon.clone()); } } t.by_key = newest.into_iter().map(|(k, (_, id))| (k, id)).collect(); VAR_CANON.with(|c| *c.borrow_mut() = Some(t)); VarCanonGuard } /// The variable a reference (`{guid}` or `{assetRef}`) names, as its newest copy. pub(super) fn resolve(r: &Kv) -> Option { let by_guid = r.get("guid").and_then(guid).filter(|g| g != NO_STYLE); VAR_CANON.with(|c| { let c = c.borrow(); let Some(t) = c.as_ref() else { return by_guid.clone() }; if let Some(g) = &by_guid { return Some(t.by_id.get(g).cloned().unwrap_or_else(|| g.clone())); } let key = r.get("assetRef")?.get("key").and_then(Kv::str)?; t.by_key.get(key).cloned() }) } /// Whether variable node `id` is an older copy another one replaces. pub(super) fn superseded(id: &str) -> bool { VAR_CANON.with(|c| c.borrow().as_ref().and_then(|t| t.by_id.get(id)).is_some_and(|canon| canon != id)) } }