Files
craft-libs/crates/fig/src/rest.rs
T
Dylan M. Taylor d13ac58a15 craft-fig: map FigJam stickies to native board items
* craft-fig: map FigJam stickies to native board items
2026-11-12 06:26:14 -05:00

730 lines
32 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! A decoded `.fig` message mapped onto the REST API file JSON ([`craft_figma_rest`]), the shape
//! an application's importer takes.
//!
//! 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 map 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 result looks as saved. Layer kinds the REST shape
//! doesn't have (sticky notes, connectors, tables, …) come out as the closest frames, text and
//! vectors. Anything not understood is skipped with a warning; malformed input never panics.
//!
//! Besides the public REST fields, the result carries the extensions [`craft_figma_rest::ext`]
//! documents (`vectorNetwork`, `vectorNetworkRef`, `pointCount`, `innerRadius`,
//! `_letterSpacingPercent`) and private `_fig*` handoffs for an importer that wants the file's
//! provenance ([`FIG_SOURCE_FIELD`], [`FIG_LIBRARY_CONTEXTS_FIELD`], [`FIG_CAPPED_MAINS_FIELD`],
//! `_figRegionFills`, `_figLineStarts`, `_figTextPath`).
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::rc::Rc;
use craft_figma_rest::ext::VectorNetwork;
use kurbo::{Affine, Point, Rect};
use serde_json::{Map, Value, json};
use crate::kiwi::Kv;
use crate::network;
mod component;
mod connector;
mod document;
mod geometry;
mod layer;
mod layout;
mod paint;
mod proto;
mod swap;
mod text;
mod text_path;
mod variables;
pub use geometry::{Geometry, MAX_RUNS, RouteEnd, Routing, SavedRuns, Side, Straight};
use component::OverrideScope;
#[cfg(test)]
use paint::paint;
type Obj = Map<String, Value>;
/// Most layers mapped from a file when the caller has no budget of its own
/// ([`layer_budget`] gives one that follows the file's size).
pub const MAX_NODES: usize = 200_000;
/// The provenance of a layer mapped from the file (its guid, whether it was expanded from a
/// main, …), on each layer.
pub const FIG_SOURCE_FIELD: &str = "_figSource";
/// Component-bearing branches of the file's internal canvases (cached library definitions), on
/// the file.
pub const FIG_LIBRARY_CONTEXTS_FIELD: &str = "_figLibraryContexts";
/// Mains in the file that the layer cap kept out, on the file: their instances are told apart
/// from those of components the file doesn't have.
pub const FIG_CAPPED_MAINS_FIELD: &str = "_figCappedMains";
/// Deepest nesting followed.
const MAX_DEPTH: usize = 256;
/// Deepest nesting of conditional prototype actions followed.
const MAX_ACTION_DEPTH: usize = 16;
/// Deepest nesting of variable expressions followed.
const MAX_EXPR_DEPTH: usize = 32;
const MAX_GRID_TRACKS: usize = 1_000;
const MAX_GRID_TRACK_VALUE: f64 = 1e6;
/// Smallest and largest scale factor of a scaled instance (`symbolData.uniformScaleFactor`, times
/// those of the instances around it) that is followed: a junk factor changes nothing.
const MIN_SCALE_FACTOR: f64 = 1e-3;
const MAX_SCALE_FACTOR: f64 = 1e3;
/// 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;
/// Compressed canvas bytes per layer of [`layer_budget`].
const PACKED_BYTES_PER_LAYER: usize = 8;
/// Most layers [`layer_budget`] gives.
pub const MAX_FIG_NODES: usize = 2_000_000;
/// Most layers to map from a file whose compressed canvas is `packed` bytes: one per 8 bytes, at
/// least [`MAX_NODES`] and at most [`MAX_FIG_NODES`]. Instances expand into a layer per layer of
/// their component, and chunks inflate up to [`crate::MAX_CHUNK`], so the budget follows what the
/// file takes, not what it unpacks to: a small file can't claim a large count, however it's
/// built; a large one gets room. Design systems are mostly instances, which pack tightly: a 7.8 MB
/// canvas stopped at 244 248 layers at 32 bytes a layer. At 8, files under about 1.6 MB keep the
/// floor and the ceiling comes at a 16 MB canvas.
pub fn layer_budget(packed: usize) -> usize {
(packed / PACKED_BYTES_PER_LAYER).clamp(MAX_NODES, MAX_FIG_NODES)
}
/// How to map a message.
#[derive(Clone, Copy)]
pub struct Options<'g> {
/// Most layers produced or scanned, instance expansion included ([`layer_budget`]).
pub max_nodes: usize,
/// Connector routing and text path checks ([`Straight`] when the caller has none).
pub geometry: &'g dyn Geometry,
/// The plugin data (plugin id, key) under which a writer kept what a layer written as the
/// outline of its stroke was: read back as `outlinedStroke` (an extension of the file JSON).
pub outlined_stroke: Option<(&'g str, &'g str)>,
/// Map FigJam sticky notes to native board items instead of generic frames.
pub board: bool,
}
impl Default for Options<'_> {
fn default() -> Self {
Options { max_nodes: MAX_NODES, geometry: &Straight, outlined_stroke: None, board: false }
}
}
/// What [`to_rest`] makes of a message.
#[derive(Debug)]
pub struct Mapped {
/// The file JSON, in the shape [`craft_figma_rest::File`] reads.
pub json: Value,
/// What was skipped or approximated, each once (`(×n)` after one met n times).
pub warnings: Vec<String>,
/// The vector networks layers name by `vectorNetworkRef` (each built once, however many
/// instances show it). `None` where one couldn't be built.
pub networks: Vec<Option<VectorNetwork>>,
}
/// The REST API file JSON for message `msg` (decoded with its file's schema), named `name`.
pub fn to_rest<'a>(msg: &'a Kv<'a>, name: &str, options: &Options<'a>) -> Mapped {
let mut m = Mapper::new(msg, options);
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[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.
if m.count >= m.max_nodes {
let capped: Vec<Value> = m.referenced_mains.difference(&m.mapped_mains).map(|id| json!(id)).collect();
if !capped.is_empty() {
out[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);
}
let networks = std::mem::take(&mut m.networks);
Mapped { json: out, warnings: std::mem::take(&mut m.warnings).finish(), networks }
}
/// The axis-aligned bounds of `rect` after `xf`.
fn bbox(xf: Affine, rect: Rect) -> Rect {
let corners = [Point::new(rect.x0, rect.y0), Point::new(rect.x1, rect.y0), Point::new(rect.x1, rect.y1), Point::new(rect.x0, rect.y1)];
let mut it = corners.into_iter().map(|p| xf * p);
let Some(first) = it.next() else { return Rect::ZERO };
it.fold(Rect::from_points(first, first), |r, p| r.union_pt(p))
}
/// `s` cut at 200 characters, for quoting names from the file in warnings.
fn clip_str(s: &str) -> &str {
s.char_indices().nth(200).and_then(|(i, _)| s.get(..i)).unwrap_or(s)
}
#[cfg(test)]
mod image_filter_tests;
#[cfg(test)]
mod swap_tests;
#[cfg(test)]
mod tests;
// ---- 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<f64> {
n.get(k).and_then(Kv::f64)
}
fn guid(v: &Kv) -> Option<String> {
Some(format!("{}:{}", num(v, "sessionID")? as u64, num(v, "localID")? as u64))
}
/// The main an `overriddenSymbolID` swaps to: none for the empty GUID written there for "no
/// swap", which names no node.
fn swap_guid(v: &Kv) -> Option<String> {
guid(v).filter(|g| g != NO_STYLE)
}
#[cfg(test)]
mod connector_snapshot_budget_tests;
/// A `GUID`-holding reference (`StyleId`, `VariableID`, …): its `guid`.
fn ref_guid(v: &Kv) -> Option<String> {
v.get("guid").and_then(guid).filter(|g| g != NO_STYLE)
}
fn vec2(v: &Kv) -> Option<Value> {
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<Affine> {
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));
}
}
/// [`copy_num`] for a length: the layers of a scaled instance give it at the instance's scale.
fn copy_len(o: &mut Obj, n: &View, from: &str, to: &str) {
if let Some(x) = n.len(from) {
o.insert(to.into(), json!(x));
}
}
/// `x` at scale `k`; `None` when the result is not finite.
fn scaled(x: f64, k: f64) -> Option<f64> {
if k == 1.0 {
return Some(x);
}
let r = x * k;
r.is_finite().then_some(r)
}
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>>,
/// The factor each layer's lengths take (`scales[i]` is for `layers[i]`): the layers of a main
/// inside a scaled instance have the instance's, the override and derived rows of the instance
/// 1, being at the instance's scale already (#2091).
scales: Vec<f64>,
/// 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::of_scaled(n, 1.0)
}
/// A node of a main inside an instance scaled by `k`: its lengths read `k` times as long.
fn of_scaled(n: &'a Kv<'a>, k: f64) -> Self {
Self { layers: vec![n], scales: vec![k], fitted: None }
}
/// This view with the layers of `upper` over it (`upper`'s fitted size and place).
fn over(&self, upper: &Self) -> Self {
let mut v = self.clone();
v.layers.extend(upper.layers.iter().copied());
v.scales.extend(upper.scales.iter().copied());
v.fitted = upper.fitted;
v
}
/// The factor of the lowest layer (the layer's own main), for values a style gives.
fn base_scale(&self) -> f64 {
self.scales.first().copied().unwrap_or(1.0)
}
fn size(&self) -> Option<(f64, f64)> {
self.fitted.map(|f| f.0).or_else(|| {
let (v, k) = self.find("size")?;
let (x, y) = size(v)?;
Some((scaled(x, k)?, scaled(y, k)?))
})
}
fn transform(&self) -> Option<Affine> {
self.fitted.map(|f| f.1).or_else(|| {
let (v, k) = self.find("transform")?;
let m = matrix(v)?;
if k == 1.0 {
return Some(m);
}
let [a, b, c, d, e, f] = m.as_coeffs();
let m = Affine::new([a, b, c, d, scaled(e, k)?, scaled(f, k)?]);
Some(m)
})
}
/// This view with `top` over it, each layer with the factor its lengths take.
fn with(&self, top: impl IntoIterator<Item = (&'a Kv<'a>, f64)>) -> Self {
let mut v = self.clone();
for (layer, scale) in top {
v.layers.push(layer);
v.scales.push(scale);
}
v
}
/// The factor of the layer that links a style in `key` (or `legacy`), the lowest layer's when
/// none does: what the linked style's lengths take.
fn link_scale(&self, key: &str, legacy: &str) -> f64 {
self.find(key).or_else(|| self.find(legacy)).map_or_else(|| self.base_scale(), |f| f.1)
}
/// 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);
let at = at.min(v.layers.len());
// The style is at the scale of the layer that links it.
let k = at.checked_sub(1).and_then(|i| v.scales.get(i)).copied().unwrap_or(1.0);
v.layers.insert(at, s);
v.scales.insert(at, k);
}
v
}
/// An effect/grid list follows the same precedence as text styles: an override above the
/// style link wins, while the style beats stale copies on and below its linking row. A style
/// without the list is empty; it must not expose a stale list from a lower row.
fn styled_list(&self, style: &'a Kv<'a>, list: &str, key: &str, legacy: &str) -> (&'a [Kv<'a>], f64) {
let linked_in = |k: &str| self.layers.iter().rposition(|l| l.get(k).is_some());
let above = linked_in(key).or_else(|| linked_in(legacy)).map_or(1, |i| i + 1);
self.layers
.iter()
.zip(&self.scales)
.skip(above)
.rev()
.find_map(|(row, scale)| row.get(list).map(|v| (v.list(), *scale)))
.unwrap_or_else(|| (style.get(list).map(Kv::list).unwrap_or_default(), self.link_scale(key, legacy)))
}
fn get(&self, k: &str) -> Option<&'a Kv<'a>> {
self.layers.iter().rev().find_map(|l| l.get(k))
}
/// [`Self::get`], and the factor of the layer that has it.
fn find(&self, k: &str) -> Option<(&'a Kv<'a>, f64)> {
self.layers.iter().zip(&self.scales).rev().find_map(|(l, s)| l.get(k).map(|v| (v, *s)))
}
/// A length: the number `k` holds at the scale of the layer that has it.
fn len(&self, k: &str) -> Option<f64> {
let (v, s) = self.find(k)?;
scaled(v.f64()?, s)
}
/// [`Self::list`], and the factor of the layer that has it.
fn list_scaled(&self, k: &str) -> (&'a [Kv<'a>], f64) {
self.find(k).map_or((&[], 1.0), |(v, s)| (v.list(), s))
}
fn num(&self, k: &str) -> Option<f64> {
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<bool> {
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<String, &'a Kv<'a>>,
kids: BTreeMap<String, Vec<(&'a str, String)>>,
blobs: Vec<&'a [u8]>,
/// Component property definitions: id → (REST name "Name#id", type).
prop_defs: BTreeMap<String, (String, &'a str)>,
/// 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<String, String>,
/// 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<String, BTreeMap<String, String>>,
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<String, Rc<OverrideScope>>,
/// 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<Value>,
/// Layers visited while scanning internal canvases, across all of them.
library_scan_count: usize,
warnings: craft_figma_rest::Warnings,
geometry: &'a dyn Geometry,
outlined_stroke: Option<(&'a str, &'a str)>,
board: bool,
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<String>,
mapped_mains: BTreeSet<String>,
/// Most layers produced or scanned, instance expansion included; the one place the cap comes
/// from (see [`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<Option<VectorNetwork>>,
/// Index in `networks` by its blob, position scale, style table, point-radius scale and the
/// layer's corner radius (which points without one of their own take).
network_ids: HashMap<(usize, u64, u64, usize, u64, u64), 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>, options: &Options<'a>) -> Self {
let max_nodes = options.max_nodes;
let mut nodes = BTreeMap::new();
let mut kids: BTreeMap<String, Vec<(&'a str, String)>> = 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<String, BTreeMap<String, String>> = 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: craft_figma_rest::Warnings::default(),
geometry: options.geometry,
outlined_stroke: options.outlined_stroke,
board: options.board,
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<String> {
// 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. A reference by asset that names no copy by itself (no
/// version, or a version the file doesn't hold, while it holds several) still names the asset:
/// its newest live copy.
pub(super) fn collection_id(&self, v: &Kv) -> Option<String> {
let by_asset = || v.get("assetRef")?.get("key").and_then(Kv::str).and_then(VarCanon::newest_set);
match self.resource_id(v) {
Some(id) if self.nodes.contains_key(&id) => Some(self.canonical_collection(&id).unwrap_or(id)),
// The asset index can name a node a later change removed: the asset's live copy instead.
Some(id) => by_asset().or(Some(id)),
None => by_asset(),
}
}
/// 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<String> {
let key = self.nodes.get(id)?.get("key").and_then(Kv::str).filter(|k| !k.is_empty())?;
VarCanon::newest_set(key)
}
/// 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<String>) {
self.warnings.add(w.into());
}
fn children(&self, id: &str) -> Vec<String> {
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::<u64>().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<String, String>,
by_id: HashMap<String, String>,
/// Every live variable node of the file, with a key or not.
local: BTreeSet<String>,
/// The newest live copy of each library collection, by asset key. Read from the file's live
/// nodes (the last change of each guid, REMOVED ones gone), not from the asset index, which
/// keeps the first node it met for a key and version even when a later change removed it.
sets_by_key: HashMap<String, String>,
}
thread_local! {
static VAR_CANON: std::cell::RefCell<Option<VarCanon>> = const { std::cell::RefCell::new(None) };
}
/// Removes the thread's table when the mapping is done.
pub(super) struct VarCanonGuard;
impl Drop for VarCanonGuard {
/// Clears the thread's table, so a later file on this thread starts without it.
fn drop(&mut self) {
VAR_CANON.with(|c| c.borrow_mut().take());
}
}
impl VarCanon {
/// Builds the table from mapper `m`'s live nodes (newest copy per asset key, for variables and
/// for collections) and installs it for this thread until the returned guard is dropped.
fn install(m: &Mapper) -> VarCanonGuard {
let mut newest: HashMap<String, ((u64, u64), String)> = HashMap::new();
let mut copies: Vec<(String, String)> = Vec::new();
let mut local = BTreeSet::new();
let mut sets: HashMap<String, ((u64, u64), String)> = HashMap::new();
for (id, n) in &m.nodes {
let ty = n.get("type").and_then(Kv::str);
if !matches!(ty, Some("VARIABLE" | "VARIABLE_SET")) || n.get("isSoftDeleted").and_then(Kv::bool) == Some(true) {
continue;
}
if ty == Some("VARIABLE_SET") {
if let Some(key) = n.get("key").and_then(Kv::str).filter(|k| !k.is_empty()) {
let rank = version_rank(n.get("version").and_then(Kv::str).unwrap_or(""));
let e = sets.entry(key.to_string()).or_insert_with(|| (rank, id.clone()));
if rank > e.0 || (rank == e.0 && *id > e.1) {
*e = (rank, id.clone());
}
}
continue;
}
local.insert(id.clone());
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();
t.local = local;
t.sets_by_key = sets.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. A guid
/// counts when this file holds a variable by it; one from another file (a paste carries the
/// library's own guids) names nothing here, so the asset it also names is tried before it is
/// kept as it is.
pub(super) fn resolve(r: &Kv) -> Option<String> {
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.as_ref().filter(|g| t.local.contains(*g)) {
return Some(t.by_id.get(g).cloned().unwrap_or_else(|| g.clone()));
}
let by_asset = r.get("assetRef").and_then(|a| a.get("key")).and_then(Kv::str).and_then(|k| t.by_key.get(k)).cloned();
by_asset.or(by_guid)
})
}
/// The newest live copy of the library collection with asset key `key`.
pub(super) fn newest_set(key: &str) -> Option<String> {
VAR_CANON.with(|c| c.borrow().as_ref().and_then(|t| t.sets_by_key.get(key).cloned()))
}
/// Whether a table is installed on this thread (for tests: none outlives the mapping).
#[cfg(test)]
fn installed() -> bool {
VAR_CANON.with(|c| c.borrow().is_some())
}
/// 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))
}
}