mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
279 lines
10 KiB
Rust
279 lines
10 KiB
Rust
use super::attributes::ObjectAttributes;
|
|
use super::extras::{Extras, NO_EXTRAS, NodeExtras};
|
|
use super::ids::NodeId;
|
|
use super::kind::NodeKind;
|
|
use super::opacity::{Knockout, OpacityMask};
|
|
use super::{Appearance, BlendMode, Deserialize, Node, PathData, Serialize, one, yes};
|
|
|
|
/// Field `name` of a [`Node`] written as `v`, or skipped. A function of its own, so the long field
|
|
/// list stays one straight run of calls.
|
|
fn write_field<S: serde::ser::SerializeStruct, T: Serialize + ?Sized>(st: &mut S, skip: bool, name: &'static str, v: &T) -> Result<(), S::Error> {
|
|
if skip { st.skip_field(name) } else { st.serialize_field(name, v) }
|
|
}
|
|
|
|
/// How a [`Node`] is written: the flat field list of the file format, in its order, borrowed from
|
|
/// the node and its extras (nothing is copied). The node reads back through `NodeWire`.
|
|
impl Serialize for Node {
|
|
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
|
use crate::skip::{is_default, is_one, is_true};
|
|
use serde::ser::SerializeStruct;
|
|
let e = self.extras();
|
|
// As the derive did: the fields written, then each field written or skipped in order.
|
|
let skips = [
|
|
false,
|
|
self.name.is_none(),
|
|
is_true(&self.visible),
|
|
is_default(&self.locked),
|
|
is_one(&self.opacity),
|
|
is_default(&self.blend),
|
|
is_default(&self.isolate),
|
|
is_default(&self.knockout),
|
|
is_default(&self.knockout_shape),
|
|
is_default(&self.appearance),
|
|
e.mask.is_none(),
|
|
e.trace.is_none(),
|
|
e.wrap.is_none(),
|
|
e.graph.is_none(),
|
|
e.series_index.is_none(),
|
|
is_default(&e.type_outlines),
|
|
is_default(&e.pixelated),
|
|
e.shaper.is_none(),
|
|
false,
|
|
e.graphic_style.is_none(),
|
|
e.attrs.is_none(),
|
|
e.slice.is_none(),
|
|
is_default(&self.bbox_angle),
|
|
e.perspective.is_none(),
|
|
e.dimension.is_none(),
|
|
e.intertwine.is_none(),
|
|
e.intertwine_tiers.is_empty(),
|
|
e.live_paint_gaps.is_none(),
|
|
e.objects_on_path.is_none(),
|
|
e.colorize.is_none(),
|
|
is_default(&e.compound_shape),
|
|
is_default(&e.shape_mode),
|
|
e.symbol_overrides.is_empty(),
|
|
];
|
|
let mut st = s.serialize_struct("Node", skips.iter().filter(|s| !**s).count())?;
|
|
let mut skip = skips.into_iter();
|
|
macro_rules! field {
|
|
($name:literal, $v:expr) => {
|
|
write_field(&mut st, skip.next().unwrap_or(false), $name, $v)?;
|
|
};
|
|
}
|
|
field!("id", &self.id);
|
|
field!("name", &self.name);
|
|
field!("visible", &self.visible);
|
|
field!("locked", &self.locked);
|
|
field!("opacity", &self.opacity);
|
|
field!("blend", &self.blend);
|
|
field!("isolate", &self.isolate);
|
|
field!("knockout", &self.knockout);
|
|
field!("knockout_shape", &self.knockout_shape);
|
|
field!("appearance", &self.appearance);
|
|
field!("mask", &e.mask);
|
|
field!("trace", &e.trace);
|
|
field!("wrap", &e.wrap);
|
|
field!("graph", &e.graph);
|
|
field!("series_index", &e.series_index);
|
|
field!("type_outlines", &e.type_outlines);
|
|
field!("pixelated", &e.pixelated);
|
|
field!("shaper", &e.shaper);
|
|
field!("kind", &self.kind);
|
|
field!("graphic_style", &e.graphic_style);
|
|
field!("attrs", &e.attrs);
|
|
field!("slice", &e.slice);
|
|
field!("bbox_angle", &self.bbox_angle);
|
|
field!("perspective", &e.perspective);
|
|
field!("dimension", &e.dimension);
|
|
field!("intertwine", &e.intertwine);
|
|
field!("intertwine_tiers", &e.intertwine_tiers);
|
|
field!("live_paint_gaps", &e.live_paint_gaps);
|
|
field!("objects_on_path", &e.objects_on_path);
|
|
field!("colorize", &e.colorize);
|
|
field!("compound_shape", &e.compound_shape);
|
|
field!("shape_mode", &e.shape_mode);
|
|
field!("symbol_overrides", &e.symbol_overrides);
|
|
st.end()
|
|
}
|
|
}
|
|
|
|
/// Most objects [`with_node_budget`] lets a document read: 4M (about 1 GB of the smallest
|
|
/// objects), 512K in the web build. Paths have their own anchor budget
|
|
/// ([`vectorcraft_geom::path::MAX_READ_ANCHORS`]); this one bounds the objects themselves.
|
|
pub const MAX_READ_NODES: usize = if cfg!(target_arch = "wasm32") { 512 << 10 } else { 4 << 20 };
|
|
|
|
std::thread_local! {
|
|
/// Objects the document being read on this thread may still have, and the budget they come
|
|
/// from ([`with_node_budget`]).
|
|
static NODES_LEFT: std::cell::Cell<Option<(usize, usize)>> = const { std::cell::Cell::new(None) };
|
|
}
|
|
|
|
/// Run `f` (which reads nodes with serde) letting it read at most `max` objects together; reading
|
|
/// more fails. A compressed file of repeated empty objects stays small while they take gigabytes
|
|
/// once read, about 270 bytes each against some 50 in the file (#3587).
|
|
pub fn with_node_budget<T>(max: usize, f: impl FnOnce() -> T) -> T {
|
|
struct Restore(Option<(usize, usize)>);
|
|
impl Drop for Restore {
|
|
fn drop(&mut self) {
|
|
NODES_LEFT.set(self.0);
|
|
}
|
|
}
|
|
let _restore = Restore(NODES_LEFT.replace(Some((max, max))));
|
|
f()
|
|
}
|
|
|
|
/// A node's id, counting the node against [`with_node_budget`]. Every node has exactly one id, so
|
|
/// each counts once, and the id usually comes first, before the node's children are read.
|
|
fn counted_id<'de, D: serde::Deserializer<'de>>(d: D) -> Result<NodeId, D::Error> {
|
|
let id = NodeId::deserialize(d)?;
|
|
count_one()?;
|
|
Ok(id)
|
|
}
|
|
|
|
/// Count one object against [`with_node_budget`].
|
|
fn count_one<E: serde::de::Error>() -> Result<(), E> {
|
|
if let Some((left, max)) = NODES_LEFT.get() {
|
|
let left = left.checked_sub(1).ok_or_else(|| E::custom(format!("the document has more than {max} objects")))?;
|
|
NODES_LEFT.set(Some((left, max)));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// A list read from a file (a member's Intertwine areas of tier 2 and up, an instance's symbol
|
|
/// overrides), each item counting against [`with_node_budget`] as an object does: a compressed
|
|
/// file of repeated empty items stays small while they take memory once read.
|
|
fn counted_list<'de, D: serde::Deserializer<'de>, T: Deserialize<'de>>(d: D) -> Result<Vec<T>, D::Error> {
|
|
struct Items<T>(std::marker::PhantomData<T>);
|
|
impl<'de, T: Deserialize<'de>> serde::de::Visitor<'de> for Items<T> {
|
|
type Value = Vec<T>;
|
|
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
f.write_str("a list")
|
|
}
|
|
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<T>, A::Error> {
|
|
let mut out = Vec::new();
|
|
while let Some(item) = seq.next_element::<T>()? {
|
|
count_one()?;
|
|
out.push(item);
|
|
}
|
|
Ok(out)
|
|
}
|
|
}
|
|
d.deserialize_seq(Items(std::marker::PhantomData))
|
|
}
|
|
|
|
/// How a [`Node`] is read: the flat field list of the file format. Fields move into the node
|
|
/// (and its extras, allocated only when one is set) without a copy of what they own.
|
|
#[derive(Deserialize)]
|
|
#[serde(rename = "Node")]
|
|
pub(super) struct NodeWire {
|
|
#[serde(deserialize_with = "counted_id")]
|
|
id: NodeId,
|
|
#[serde(default)]
|
|
name: Option<String>,
|
|
#[serde(default = "yes")]
|
|
visible: bool,
|
|
#[serde(default)]
|
|
locked: bool,
|
|
#[serde(default = "one")]
|
|
opacity: f32,
|
|
#[serde(default)]
|
|
blend: BlendMode,
|
|
#[serde(default)]
|
|
isolate: bool,
|
|
#[serde(default)]
|
|
knockout: Knockout,
|
|
#[serde(default)]
|
|
knockout_shape: bool,
|
|
#[serde(default)]
|
|
appearance: crate::Shared<Appearance>,
|
|
#[serde(default)]
|
|
mask: Option<Box<OpacityMask>>,
|
|
#[serde(default)]
|
|
trace: Option<Box<serde_json::Value>>,
|
|
#[serde(default)]
|
|
wrap: Option<crate::text::TextWrap>,
|
|
#[serde(default)]
|
|
graph: Option<Box<crate::graph::GraphSpec>>,
|
|
#[serde(default)]
|
|
series_index: Option<u32>,
|
|
#[serde(default)]
|
|
type_outlines: bool,
|
|
#[serde(default)]
|
|
pixelated: bool,
|
|
#[serde(default)]
|
|
shaper: Option<Box<crate::shaper::ShaperSpec>>,
|
|
kind: NodeKind,
|
|
#[serde(default)]
|
|
graphic_style: Option<u32>,
|
|
#[serde(default)]
|
|
attrs: Option<Box<ObjectAttributes>>,
|
|
#[serde(default)]
|
|
slice: Option<Box<crate::SliceOptions>>,
|
|
#[serde(default)]
|
|
bbox_angle: f64,
|
|
#[serde(default)]
|
|
perspective: Option<Box<crate::PerspectiveAttachment>>,
|
|
#[serde(default)]
|
|
dimension: Option<Box<crate::dimension::Dimension>>,
|
|
#[serde(default)]
|
|
intertwine: Option<Box<PathData>>,
|
|
#[serde(default, deserialize_with = "counted_list")]
|
|
intertwine_tiers: Vec<PathData>,
|
|
#[serde(default)]
|
|
live_paint_gaps: Option<Box<crate::live_paint::GapOptions>>,
|
|
#[serde(default)]
|
|
objects_on_path: Option<Box<crate::objects_on_path::ObjectsOnPath>>,
|
|
#[serde(default)]
|
|
colorize: Option<Box<vectorcraft_color::Paint>>,
|
|
#[serde(default)]
|
|
compound_shape: bool,
|
|
#[serde(default)]
|
|
shape_mode: crate::ShapeMode,
|
|
#[serde(default, deserialize_with = "counted_list")]
|
|
symbol_overrides: Vec<crate::SymbolOverride>,
|
|
}
|
|
|
|
impl From<NodeWire> for Node {
|
|
fn from(w: NodeWire) -> Self {
|
|
let extras = NodeExtras {
|
|
mask: w.mask,
|
|
trace: w.trace,
|
|
wrap: w.wrap,
|
|
graph: w.graph,
|
|
series_index: w.series_index,
|
|
type_outlines: w.type_outlines,
|
|
pixelated: w.pixelated,
|
|
shaper: w.shaper,
|
|
graphic_style: w.graphic_style,
|
|
attrs: w.attrs,
|
|
slice: w.slice,
|
|
perspective: w.perspective,
|
|
dimension: w.dimension,
|
|
intertwine: w.intertwine,
|
|
intertwine_tiers: w.intertwine_tiers,
|
|
live_paint_gaps: w.live_paint_gaps,
|
|
objects_on_path: w.objects_on_path,
|
|
colorize: w.colorize,
|
|
compound_shape: w.compound_shape,
|
|
shape_mode: w.shape_mode,
|
|
symbol_overrides: w.symbol_overrides,
|
|
};
|
|
Node {
|
|
id: w.id,
|
|
name: w.name,
|
|
visible: w.visible,
|
|
locked: w.locked,
|
|
opacity: w.opacity,
|
|
blend: w.blend,
|
|
isolate: w.isolate,
|
|
knockout: w.knockout,
|
|
knockout_shape: w.knockout_shape,
|
|
appearance: w.appearance,
|
|
kind: w.kind,
|
|
bbox_angle: w.bbox_angle,
|
|
extras: Extras((extras != NO_EXTRAS).then(|| Box::new(extras))),
|
|
}
|
|
}
|
|
}
|