diff --git a/map.rs b/map.rs deleted file mode 100644 index 98239cd7b..000000000 --- a/map.rs +++ /dev/null @@ -1,490 +0,0 @@ -//! 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)) - } -}