Files
vectorcraft/crates/engine/src/cmd/clipboard/resources.rs
T
dotCooCoo ac8e39fa2e Effects: a pasted SVG filter also takes a free punctuation character when no letter or digit is free
A filter's id can be any character, so when every letter and digit is in
use a pasted filter at the size limit with a one-letter name now tries the
other printable ASCII characters (`!` to `~`) before the paste is refused.
2026-11-11 07:26:02 -08:00

1455 lines
64 KiB
Rust

//! The document resources copied objects use, and adding them to the document they're pasted into.
//!
//! Objects refer to resources by name (image blobs, symbols, patterns, swatches, character and
//! paragraph styles, brushes) and to graphic styles by id. Copy follows those references, also
//! through the symbols, patterns, styles and brush art it finds, and keeps what they lead to.
//!
//! Pasting matches each resource by name: an identical one in the document is used, a missing one
//! is added, and a different one of the same name is added under a free name, which the pasted
//! objects follow. Global and spot swatches are the exception: for a name the document gives
//! another colour the caller picks Merge (the objects take the document's swatch) or Add (the
//! pasted swatch comes in renamed), the Swatch Conflict dialog's question. Colours linked to a
//! swatch then show it as the document does: its colour at their tint, and a Lab spot colour as
//! the document's Spot Colors option says. Pasting back into the document the objects came from
//! uses its resources as they are now.
use std::cell::OnceCell;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::sync::Arc;
use serde_json::{Map, Value, json};
use vectorcraft_brush::Brush;
use vectorcraft_color::{Color, Paint, Swatch};
use vectorcraft_doc::{
Appearance, AppearanceItem, Document, Effect, GraphicStyle, Node, NodeKind, PatternDef, Symbol, SymbolExportType, SymbolNineSlice,
SymbolRegistration, TextStyleDef,
};
use super::super::brushsym::{set_natural_size, stored_natural_size};
use super::super::patterncmds::add_pattern;
use super::super::{EngineError, Result, bad, unique_name};
use super::Clipboard;
/// A resource kind objects refer to (graphic styles by id, the others by name).
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum Res {
Image,
Symbol,
Pattern,
Swatch,
CharStyle,
ParaStyle,
Brush,
GraphicStyle,
}
impl Res {
fn id(self) -> &'static str {
match self {
Res::Image => "image",
Res::Symbol => "symbol",
Res::Pattern => "pattern",
Res::Swatch => "swatch",
Res::CharStyle => "charStyle",
Res::ParaStyle => "paraStyle",
Res::Brush => "brush",
Res::GraphicStyle => "graphicStyle",
}
}
}
/// A reference from art to a resource.
enum Ref<'a> {
Name(Res, &'a str),
/// A colour linked to a swatch, at a tint.
Swatch(&'a str, f32),
/// A link to the graphic style with this id.
Style(u32),
}
type Visit<'v> = &'v mut dyn FnMut(Ref<'_>);
fn paint_refs(p: &Paint, f: Visit) {
match p {
Paint::Solid { swatch: Some(s), tint, .. } => f(Ref::Swatch(s, *tint)),
Paint::Gradient(g) => {
// The gradient swatch it was applied from, and its stops' links.
if let Some(s) = &g.swatch {
f(Ref::Name(Res::Swatch, s));
}
g.gradient.stops.iter().filter_map(|st| Some((st.swatch.as_deref()?, st.tint))).for_each(|(s, t)| f(Ref::Swatch(s, t)));
}
Paint::Pattern { pattern, .. } => f(Ref::Name(Res::Pattern, pattern)),
_ => {}
}
}
fn appearance_refs(a: &Appearance, f: Visit) {
for it in &a.items {
paint_refs(it.paint(), f);
if let AppearanceItem::Stroke(l) = it
&& let Some(b) = &l.brush
{
f(Ref::Name(Res::Brush, b));
}
}
}
/// The paints a character or paragraph style can set (by their serialized names).
const STYLE_PAINTS: [&str; 2] = ["fill", "stroke"];
fn style_paint(attrs: &Map<String, Value>, key: &str) -> Option<Paint> {
serde_json::from_value(attrs.get(key)?.clone()).ok()
}
fn attrs_refs(attrs: &Map<String, Value>, f: Visit) {
for p in STYLE_PAINTS.iter().filter_map(|k| style_paint(attrs, k)) {
paint_refs(&p, f);
}
}
/// Every reference in the subtree of `n` (opacity-mask art included).
fn node_refs(n: &Node, f: Visit) {
n.walk(&mut |m| {
appearance_refs(&m.appearance, f);
if let Some(id) = m.extras().graphic_style {
f(Ref::Style(id));
}
if let Some(mask) = &m.extras().mask {
node_refs(&mask.art, f);
}
match &m.kind {
NodeKind::Text(t) => {
for r in &t.runs {
paint_refs(&r.style.fill, f);
paint_refs(&r.style.stroke, f);
if let Some(s) = &r.style.style_name {
f(Ref::Name(Res::CharStyle, s));
}
}
for s in t.para_styles().filter_map(|pa| pa.style_name.as_ref()) {
f(Ref::Name(Res::ParaStyle, s));
}
}
NodeKind::Image(im) => f(Ref::Name(Res::Image, &im.key)),
NodeKind::PlacedDocument(pl) => f(Ref::Name(Res::Image, &pl.key)),
NodeKind::SymbolInstance { symbol, .. } => f(Ref::Name(Res::Symbol, symbol)),
_ => {}
}
});
}
/// The `(name, xml)` of `e` when it is an SVG filter effect.
fn filter_of(e: &Effect) -> Option<(String, String)> {
let text = |k: &str| e.params.get(k).and_then(Value::as_str).map(str::to_string);
if e.id != Appearance::SVG_FILTER {
return None;
}
Some((text("name")?, text("xml")?))
}
/// The SVG filter effects of appearance `a` (its own and its fills' and strokes').
fn appearance_filters(a: &Appearance, out: &mut BTreeSet<(String, String)>) {
let items = a.items.iter().flat_map(|i| i.effects().iter());
out.extend(a.effects.iter().chain(items).filter_map(filter_of));
}
/// The SVG filter effects in the subtree of `n` (opacity-mask art included).
fn node_filters(n: &Node, out: &mut BTreeSet<(String, String)>) {
n.walk(&mut |m| {
appearance_filters(&m.appearance, out);
if let Some(mask) = &m.extras().mask {
node_filters(&mask.art, out);
}
});
}
/// The names the SVG filter effects of appearance `a` give (its own and its fills' and strokes').
fn appearance_filter_names(a: &Appearance, out: &mut BTreeSet<String>) {
let items = a.items.iter().flat_map(|i| i.effects().iter());
let named = a.effects.iter().chain(items).filter(|e| e.id == Appearance::SVG_FILTER);
out.extend(named.filter_map(|e| e.params.get("name").and_then(Value::as_str)).map(str::to_string));
}
/// The names the SVG filter effects in the subtree of `n` give (opacity-mask art included).
fn node_filter_names(n: &Node, out: &mut BTreeSet<String>) {
n.walk(&mut |m| {
appearance_filter_names(&m.appearance, out);
if let Some(mask) = &m.extras().mask {
node_filter_names(&mask.art, out);
}
});
}
/// A global or spot colour of its own (not a tint of another swatch): what colours link to.
fn is_base(w: &Swatch) -> bool {
(w.global || w.spot) && matches!(w.paint, Paint::Solid { swatch: None, .. })
}
/// The definitions references lead to: a document's, or the clipboard's own.
struct Defs<'a> {
swatches: Vec<&'a Swatch>,
symbols: &'a [Symbol],
patterns: &'a [PatternDef],
styles: &'a [GraphicStyle],
char_styles: &'a [TextStyleDef],
para_styles: &'a [TextStyleDef],
brushes: Brushes<'a>,
/// Where each name is defined, built the first time a kind is followed.
index: DefIndex,
}
/// The positions of a kind's definitions by name.
type NameIndex = HashMap<String, Vec<usize>>;
/// [`Defs`]' definitions by name (graphic styles by id), each kind indexed the first time a
/// reference to it is followed: finding a name's definitions by scanning them all made a
/// document or library of N definitions that use each other cost N² comparisons.
#[derive(Default)]
struct DefIndex {
swatches: OnceCell<NameIndex>,
symbols: OnceCell<NameIndex>,
patterns: OnceCell<NameIndex>,
styles: OnceCell<HashMap<u32, Vec<usize>>>,
char_styles: OnceCell<NameIndex>,
para_styles: OnceCell<NameIndex>,
brushes: OnceCell<NameIndex>,
}
/// The items of `list` named `name`, in order, `index` holding their positions (built from
/// `list` the first time).
fn named<'l, T>(index: &OnceCell<NameIndex>, list: &'l [T], name_of: impl Fn(&T) -> &str, name: &str) -> impl Iterator<Item = &'l T> {
let index = index.get_or_init(|| {
let mut by: NameIndex = HashMap::new();
for (i, item) in list.iter().enumerate() {
by.entry(name_of(item).to_string()).or_default().push(i);
}
by
});
let at: &[usize] = index.get(name).map_or(&[], Vec::as_slice);
at.iter().filter_map(move |&i| list.get(i))
}
/// The brushes references lead to: a document's library, read the first time a brush is used
/// (`vectorcraft_brush::library` parses it), or the clipboard's own.
enum Brushes<'a> {
Lazy(&'a Document, &'a OnceCell<Vec<Brush>>),
Ready(&'a [Brush]),
}
impl<'a> Brushes<'a> {
fn get(&self) -> &'a [Brush] {
match *self {
Brushes::Lazy(d, cell) => cell.get_or_init(|| vectorcraft_brush::library(d)),
Brushes::Ready(list) => list,
}
}
}
impl<'a> Defs<'a> {
/// `d`'s definitions; `brushes` holds its brush library once a brush is used.
fn of(d: &'a Document, brushes: &'a OnceCell<Vec<Brush>>) -> Self {
Self {
swatches: d.swatches_iter().collect(),
symbols: &d.symbols,
patterns: &d.patterns,
styles: &d.graphic_styles,
char_styles: &d.char_styles,
para_styles: &d.para_styles,
brushes: Brushes::Lazy(d, brushes),
index: DefIndex::default(),
}
}
}
/// What references lead out of.
enum Source<'a> {
Node(&'a Node),
Appearance(&'a Appearance),
Attrs(&'a Map<String, Value>),
Paint(&'a Paint),
}
/// The resources a set of objects uses, through the definitions they lead to.
#[derive(Default)]
pub(super) struct Used {
names: BTreeMap<Res, BTreeSet<String>>,
/// The tints (below 100 %) of the swatches used: (swatch, tint).
tints: Vec<(String, f32)>,
styles: BTreeSet<u32>,
}
impl Used {
fn of<'a>(defs: &Defs<'a>, nodes: impl IntoIterator<Item = &'a Node>) -> Self {
let mut used = Used::default();
used.follow(defs, nodes.into_iter().map(Source::Node).collect());
used
}
/// Every character style of `defs` (with `chars`) and every paragraph style (with `paras`), and
/// the resources they use, styles of the other kind included.
fn of_text_styles(defs: &Defs<'_>, chars: bool, paras: bool) -> Self {
let mut used = Used::default();
let mut todo = vec![];
for s in defs.char_styles.iter().filter(|_| chars) {
used.note(defs, Ref::Name(Res::CharStyle, &s.name), &mut todo);
}
for s in defs.para_styles.iter().filter(|_| paras) {
used.note(defs, Ref::Name(Res::ParaStyle, &s.name), &mut todo);
}
used.follow(defs, todo);
used
}
/// Note the references of `todo`'s sources and of the definitions they lead to.
fn follow<'a>(&mut self, defs: &Defs<'a>, mut todo: Vec<Source<'a>>) {
while let Some(src) = todo.pop() {
let mut found = |r: Ref<'_>| self.note(defs, r, &mut todo);
match src {
Source::Node(n) => node_refs(n, &mut found),
Source::Appearance(a) => appearance_refs(a, &mut found),
Source::Attrs(a) => attrs_refs(a, &mut found),
Source::Paint(p) => paint_refs(p, &mut found),
}
}
}
/// Note `r`; a definition seen for the first time joins `todo`.
fn note<'a>(&mut self, defs: &Defs<'a>, r: Ref<'_>, todo: &mut Vec<Source<'a>>) {
let (res, name) = match r {
Ref::Style(id) => {
if self.styles.insert(id) && id != 0 {
let index = defs.index.styles.get_or_init(|| {
let mut by: HashMap<u32, Vec<usize>> = HashMap::new();
for (i, g) in defs.styles.iter().enumerate() {
by.entry(g.id).or_default().push(i);
}
by
});
let at = index.get(&id).map_or(&[][..], Vec::as_slice);
todo.extend(at.iter().filter_map(|&i| defs.styles.get(i)).map(|g| Source::Appearance(&g.appearance)));
}
return;
}
Ref::Swatch(name, tint) => {
if tint < 1.0 && !self.has_tint(name, tint) {
self.tints.push((name.to_string(), tint));
}
(Res::Swatch, name)
}
Ref::Name(res, name) => (res, name),
};
let set = self.names.entry(res).or_default();
if set.contains(name) {
return;
}
set.insert(name.to_string());
let ix = &defs.index;
match res {
// A tint swatch's base, a gradient swatch's linked stops.
Res::Swatch => todo.extend(named(&ix.swatches, &defs.swatches, |w| &w.name, name).map(|w| Source::Paint(&w.paint))),
Res::Symbol => todo.extend(named(&ix.symbols, defs.symbols, |s| &s.name, name).map(|s| Source::Node(&s.art))),
Res::Pattern => todo.extend(named(&ix.patterns, defs.patterns, |p| &p.name, name).flat_map(|p| p.art.iter().map(|a| Source::Node(a)))),
Res::CharStyle => todo.extend(named(&ix.char_styles, defs.char_styles, |s| &s.name, name).map(|s| Source::Attrs(&s.attrs))),
Res::ParaStyle => todo.extend(named(&ix.para_styles, defs.para_styles, |s| &s.name, name).map(|s| Source::Attrs(&s.attrs))),
// A scatter or art brush's art, a pattern brush's tiles.
Res::Brush => todo.extend(named(&ix.brushes, defs.brushes.get(), |b| &b.name, name).flat_map(|b| b.art().into_iter().map(Source::Node))),
_ => {}
}
}
fn has(&self, r: Res, name: &str) -> bool {
self.names.get(&r).is_some_and(|s| s.contains(name))
}
fn has_tint(&self, name: &str, tint: f32) -> bool {
self.tints.iter().any(|(n, t)| n == name && *t == tint)
}
/// Does the clipboard carry swatch `w`: a used global or spot colour or gradient swatch, or a
/// tint swatch of a tint used? (Never the built-in Registration swatch.)
fn carries(&self, w: &Swatch) -> bool {
let named = is_base(w) || matches!(w.paint, Paint::Gradient(_));
!w.is_reserved() && (named && self.has(Res::Swatch, &w.name) || w.tint_of().is_some_and(|(b, t)| self.has_tint(b, t)))
}
}
/// What a preview of some art shows besides the art itself ([`art_resources`]).
pub struct ArtResources {
/// Changes when any resource the art uses changes, and only then.
pub key: u64,
/// The art and image bytes counted in `key` by their address, held weakly: their allocations
/// stay theirs (no other art can take an address) for as long as the key is kept.
_kept: (Vec<std::sync::Weak<Node>>, Vec<std::sync::Weak<Vec<u8>>>),
}
/// The resources `art` uses in `doc`, through the definitions they lead to as Copy follows them
/// ([`Used`]): images, symbols, patterns (tile type, overlap and spacing too), swatches (linked
/// colours, tints, gradient swatches; colour groups' included), character and paragraph styles,
/// brushes and graphic styles, and the document's Spot Colors option. Art and image bytes count
/// by identity, the other definitions by their saved form: working the key out walks `art` and
/// the definitions it leads to, and never reads an image's bytes.
pub fn art_resources<'a>(doc: &'a Document, art: impl IntoIterator<Item = &'a Node>) -> ArtResources {
use std::hash::{Hash, Hasher};
let library = OnceCell::new();
let defs = Defs::of(doc, &library);
// The art's references shortened to the brush library's life.
let mut nodes: Vec<&Node> = vec![];
for n in art {
nodes.push(n);
}
let used = Used::of(&defs, nodes);
let mut h = std::collections::hash_map::DefaultHasher::new();
let mut kept = (vec![], vec![]);
fn saved(h: &mut impl Hasher, v: &impl serde::Serialize) {
serde_json::to_vec(v).unwrap_or_default().hash(h);
}
fn node(h: &mut impl Hasher, kept: &mut Vec<std::sync::Weak<Node>>, a: &Arc<Node>) {
(Arc::as_ptr(a) as usize).hash(h);
kept.push(Arc::downgrade(a));
}
for (res, names) in &used.names {
for name in names {
(*res as u8, name).hash(&mut h);
match res {
Res::Image => {
if let Some(b) = doc.images.get(name) {
b.mime.hash(&mut h);
for bytes in std::iter::once(&b.bytes).chain(&b.proxy) {
(Arc::as_ptr(bytes) as usize).hash(&mut h);
kept.1.push(Arc::downgrade(bytes));
}
}
}
Res::Symbol => doc.symbols.iter().filter(|s| s.name == *name).for_each(|s| node(&mut h, &mut kept.0, &s.art)),
Res::Pattern => {
for p in doc.patterns.iter().filter(|p| p.name == *name) {
saved(&mut h, &PatternDef { art: vec![], ..p.clone() });
p.art.iter().for_each(|a| node(&mut h, &mut kept.0, a));
}
}
Res::CharStyle => doc.char_styles.iter().filter(|s| s.name == *name).for_each(|s| saved(&mut h, s)),
Res::ParaStyle => doc.para_styles.iter().filter(|s| s.name == *name).for_each(|s| saved(&mut h, s)),
Res::Brush => defs.brushes.get().iter().filter(|b| b.name == *name).for_each(|b| saved(&mut h, b)),
// Swatches below (tints count too); graphic styles go by id.
Res::Swatch | Res::GraphicStyle => {}
}
}
}
for w in doc.swatches_iter().filter(|w| used.carries(w) || used.has(Res::Swatch, &w.name)) {
saved(&mut h, w);
}
for g in doc.graphic_styles.iter().filter(|g| g.id != 0 && used.styles.contains(&g.id)) {
saved(&mut h, g);
}
if used.names.contains_key(&Res::Swatch) {
doc.spot_use_lab.hash(&mut h);
}
ArtResources { key: h.finish(), _kept: kept }
}
/// A resource list entry known by its name.
trait Named: Clone {
fn name(&self) -> &str;
fn set_name(&mut self, name: String);
}
macro_rules! named {
($($t:ty),+) => {$(
impl Named for $t {
fn name(&self) -> &str {
&self.name
}
fn set_name(&mut self, name: String) {
self.name = name;
}
}
)+};
}
named!(Symbol, TextStyleDef, Brush);
/// Where a pasted resource ended up in the document.
enum Placed {
/// The document's own of that name is used.
Existing,
/// The document's own numbered copy ("Name 2", from an earlier paste) is used.
Reused(String),
Added,
/// Added under a free name.
Renamed(String),
}
impl Placed {
/// Added as `name`, which `from` was free or not.
fn added(from: &str, name: String) -> Self {
if name == from { Placed::Added } else { Placed::Renamed(name) }
}
/// The name of the definition the paste added (`None` when the document's own is used).
fn new_name(&self, from: &str) -> Option<String> {
match self {
Placed::Existing | Placed::Reused(_) => None,
Placed::Added => Some(from.to_string()),
Placed::Renamed(n) => Some(n.clone()),
}
}
}
/// Is `name` `base` or a numbered copy of it ("base 2")?
fn copy_of(base: &str, name: &str) -> bool {
name == base || name.strip_prefix(base).and_then(|r| r.strip_prefix(' ')).is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()))
}
/// The document's own resource for pasted resource `name`, among the document's `names`: the
/// one of that name when `same` holds for it (or, pasting back into the source document, at all),
/// else a numbered copy of it (added by an earlier paste) that is the same. `None`: add it.
fn existing<'a>(name: &str, names: impl IntoIterator<Item = &'a str>, same_doc: bool, same: impl Fn(&str) -> bool) -> Option<Placed> {
let mut found: Vec<&str> = names.into_iter().filter(|n| copy_of(name, n)).collect();
// The name itself first.
found.sort_by_key(|n| *n != name);
if same_doc && found.first() == Some(&name) {
return Some(Placed::Existing);
}
found.into_iter().find(|n| same(n)).map(|n| if n == name { Placed::Existing } else { Placed::Reused(n.to_string()) })
}
/// Find `item` in `list` ([`existing`], `same` comparing it under the entry's name) or add it
/// under a free name.
fn place<T: Named>(list: &mut Vec<T>, item: &T, same_doc: bool, same: impl Fn(&T, &T) -> bool) -> Placed {
let same_as = |n: &str| {
let mut c = item.clone();
c.set_name(n.to_string());
list.iter().find(|x| x.name() == n).is_some_and(|x| same(x, &c))
};
if let Some(p) = existing(item.name(), list.iter().map(Named::name), same_doc, same_as) {
return p;
}
let name = unique_name(item.name(), |n| list.iter().any(|x| x.name() == n));
let mut c = item.clone();
c.set_name(name.clone());
list.push(c);
Placed::added(item.name(), name)
}
/// Merge or Add, for a swatch whose name the document gives another colour.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum SwatchChoice {
#[default]
Merge,
Add,
}
/// The `swatchConflict` parameter: one choice for every conflict, or one per swatch name (the
/// others merge).
#[derive(Clone, Debug, Default)]
pub(crate) struct SwatchChoices {
all: SwatchChoice,
by_name: BTreeMap<String, SwatchChoice>,
}
impl SwatchChoices {
pub(crate) fn parse(cmd: &str, p: &Value) -> Result<Self> {
let one = |v: &Value| match v.as_str() {
Some("merge") => Ok(SwatchChoice::Merge),
Some("add") => Ok(SwatchChoice::Add),
_ => Err(bad(cmd, format!("swatchConflict must be \"merge\" or \"add\", not {v}"))),
};
match p.get("swatchConflict") {
None | Some(Value::Null) => Ok(Self::default()),
Some(Value::Object(m)) => {
Ok(Self { all: SwatchChoice::Merge, by_name: m.iter().map(|(k, v)| Ok((k.clone(), one(v)?))).collect::<Result<_>>()? })
}
Some(v) => Ok(Self { all: one(v)?, by_name: BTreeMap::new() }),
}
}
fn of(&self, name: &str) -> SwatchChoice {
self.by_name.get(name).copied().unwrap_or(self.all)
}
}
/// How pasted art is relinked to the document's resources.
#[derive(Clone, Debug, Default)]
struct Relink {
renames: BTreeMap<Res, BTreeMap<String, String>>,
/// An SVG filter effect's `(name, xml)` on the clipboard → its `(name, xml)` in the document
/// ([`Clipboard::filter_links`]).
filters: BTreeMap<(String, String), (String, String)>,
/// Graphic style id on the clipboard → its id in the document. Links to other ids are dropped.
styles: BTreeMap<u32, u32>,
/// Swatch (its name in the document) → the colour linked colours show at 100 %.
colors: BTreeMap<String, Color>,
}
impl Relink {
fn name(&self, r: Res, s: &mut String) {
if let Some(n) = self.renames.get(&r).and_then(|m| m.get(s.as_str())) {
*s = n.clone();
}
}
/// `name` as the document knows it.
fn renamed(&self, r: Res, name: &str) -> String {
let mut n = name.to_string();
self.name(r, &mut n);
n
}
fn paint(&self, p: &mut Paint) {
match p {
Paint::Pattern { pattern, .. } => self.name(Res::Pattern, pattern),
Paint::Gradient(g) => {
if let Some(s) = &mut g.swatch {
self.name(Res::Swatch, s);
}
}
_ => {}
}
p.map_links(&mut |c, link, tint| {
if let Some(l) = link {
self.name(Res::Swatch, l);
if let Some(base) = self.colors.get(l.as_str()) {
*c = base.tinted(*tint);
}
}
false
});
}
fn appearance(&self, a: &mut Appearance) {
if !self.filters.is_empty() {
let items = a.items.iter_mut().flat_map(|i| i.effects_mut().iter_mut());
for e in a.effects.iter_mut().chain(items) {
if let Some((name, xml)) = filter_of(e).and_then(|k| self.filters.get(&k)) {
e.params["name"] = json!(name);
e.params["xml"] = json!(xml);
}
}
}
for it in &mut a.items {
self.paint(it.paint_mut());
if let AppearanceItem::Stroke(l) = it
&& let Some(b) = &mut l.brush
{
self.name(Res::Brush, b);
}
}
}
fn attrs(&self, a: &mut Map<String, Value>) {
for k in STYLE_PAINTS {
let Some(before) = style_paint(a, k) else { continue };
let mut p = before.clone();
self.paint(&mut p);
if p != before
&& let Ok(v) = serde_json::to_value(&p)
{
a.insert(k.to_string(), v);
}
}
}
/// Relink a node tree (the mirror of [`node_refs`]).
fn node(&self, n: &mut Node) {
self.appearance(&mut n.appearance);
n.extras_mut().graphic_style = n.extras().graphic_style.and_then(|id| self.styles.get(&id).copied());
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) {
self.node(Arc::make_mut(&mut m.art));
}
match &mut n.kind {
NodeKind::Text(t) => {
for r in &mut t.runs {
self.paint(&mut r.style.fill);
self.paint(&mut r.style.stroke);
if let Some(s) = &mut r.style.style_name {
self.name(Res::CharStyle, s);
}
}
for pa in t.para_styles_mut() {
if let Some(s) = &mut pa.style_name {
self.name(Res::ParaStyle, s);
}
}
}
NodeKind::Image(im) => self.name(Res::Image, &mut im.key),
NodeKind::PlacedDocument(pl) => self.name(Res::Image, &mut pl.key),
NodeKind::SymbolInstance { symbol, .. } => self.name(Res::Symbol, symbol),
_ => {}
}
for c in n.children_mut().into_iter().flatten() {
self.node(Arc::make_mut(c));
}
}
}
/// Is `ours` (a document definition's art) the art of `theirs` (a pasted one's) once that is
/// relinked by `r`? Graphic style links don't count: they are relinked last.
fn same_art(r: &Relink, ours: &[Arc<Node>], theirs: &[Arc<Node>]) -> bool {
let unlinked = Relink::default();
let relinked = |r: &Relink, n: &Node| {
let mut n = n.clone();
r.node(&mut n);
n
};
ours.len() == theirs.len() && ours.iter().zip(theirs).all(|(a, b)| relinked(&unlinked, a) == relinked(r, b))
}
/// Is `ours` (a document brush) brush `theirs` (a pasted one) once the art of that is relinked by
/// `r`? Graphic style links don't count, as in [`same_art`].
fn same_brush(r: &Relink, ours: &Brush, theirs: &Brush) -> bool {
let relinked = |r: &Relink, b: &Brush| {
let mut b = b.clone();
b.art_mut().into_iter().for_each(|a| r.node(a));
b
};
relinked(&Relink::default(), ours) == relinked(r, theirs)
}
/// Does brush `b`'s art show symbols or patterns?
fn shows_definitions(b: &Brush) -> bool {
let mut found = false;
for a in b.art() {
node_refs(a, &mut |r| found |= matches!(r, Ref::Name(Res::Symbol | Res::Pattern, _)));
}
found
}
/// A symbol's Symbol Options, which its document keeps by symbol name: copied with the symbol and
/// given to it under the name pasting adds it as.
#[derive(Clone, Debug, PartialEq)]
pub struct SymbolOptions {
/// Natural size `(w, h)` (`None`: the document records none).
pub size: Option<(f64, f64)>,
pub registration: SymbolRegistration,
pub export_type: SymbolExportType,
/// 9-slice guides (`None`: 9-slice scaling off).
pub nine_slice: Option<SymbolNineSlice>,
}
impl SymbolOptions {
/// The options `d` keeps for symbol `name`.
fn of(d: &Document, name: &str) -> Self {
Self {
size: stored_natural_size(d, name),
registration: d.symbol_registration(name),
export_type: d.symbol_export_type(name),
nine_slice: d.symbol_nine_slice(name),
}
}
/// Make them the options of `d`'s symbol `name`.
fn give(&self, d: &mut Document, name: &str) {
set_natural_size(d, name, self.size);
d.set_symbol_registration(name, self.registration);
d.set_symbol_export_type(name, self.export_type);
d.set_symbol_nine_slice(name, self.nine_slice);
}
}
/// A character or paragraph style, symbol (with the Symbol Options copied with it), pattern or
/// brush (placed with the others only when they are placed in use order) to place
/// ([`Clipboard::import_used`]).
#[derive(Clone, Copy)]
enum Def<'c> {
Style(Res, &'c TextStyleDef),
Symbol(&'c Symbol, Option<&'c SymbolOptions>),
Pattern(&'c PatternDef),
Brush(&'c Brush),
}
impl<'c> Def<'c> {
fn key(&self) -> (Res, &'c str) {
match self {
Def::Style(r, s) => (*r, &s.name),
Def::Symbol(s, _) => (Res::Symbol, &s.name),
Def::Pattern(p) => (Res::Pattern, &p.name),
Def::Brush(b) => (Res::Brush, &b.name),
}
}
/// Call `f` with each resource it refers to by name.
fn each_use(&self, f: &mut dyn FnMut(Res, &str)) {
let mut g = |r: Ref<'_>| {
if let Ref::Name(res, n) = r {
f(res, n);
}
};
match self {
Def::Style(_, s) => attrs_refs(&s.attrs, &mut g),
Def::Symbol(s, _) => node_refs(&s.art, &mut g),
Def::Pattern(p) => p.art.iter().for_each(|a| node_refs(a, &mut g)),
Def::Brush(b) => b.art().into_iter().for_each(|a| node_refs(a, &mut g)),
}
}
/// Is it the definition `d` has under `name`, once relinked by `r`? (`brushes`: the brush
/// library of `d`.)
fn same_in(&self, d: &Document, brushes: &Brushes<'_>, name: &str, r: &Relink) -> bool {
match self {
Def::Style(res, s) => {
let theirs = if *res == Res::CharStyle { &d.char_styles } else { &d.para_styles };
let mut attrs = s.attrs.clone();
r.attrs(&mut attrs);
theirs.iter().any(|x| x.name == name && x.attrs == attrs)
}
Def::Symbol(s, options) => d.symbols.iter().any(|x| {
x.name == name
&& options.is_none_or(|o| SymbolOptions::of(d, name) == *o)
&& same_art(r, std::slice::from_ref(&x.art), std::slice::from_ref(&s.art))
}),
Def::Pattern(p) => {
let bare = |p: &PatternDef| PatternDef { name: String::new(), art: vec![], ..p.clone() };
d.pattern(name).is_some_and(|x| bare(x) == bare(p) && same_art(r, &x.art, &p.art))
}
Def::Brush(b) => brushes.get().iter().any(|x| x.name == name && same_brush(r, x, b)),
}
}
/// The names of its kind in `d` (`brushes`: its brush library) that are its name or a numbered
/// copy of it.
fn names_in(&self, d: &Document, brushes: &Brushes<'_>) -> Vec<String> {
let (res, name) = self.key();
let names: Vec<String> = match res {
Res::CharStyle => d.char_styles.iter().map(|s| s.name.clone()).collect(),
Res::ParaStyle => d.para_styles.iter().map(|s| s.name.clone()).collect(),
Res::Symbol => d.symbols.iter().map(|s| s.name.clone()).collect(),
Res::Brush => brushes.get().iter().map(|b| b.name.clone()).collect(),
_ => d.patterns.iter().map(|p| p.name.clone()).collect(),
};
names.into_iter().filter(|n| copy_of(name, n)).collect()
}
}
/// `defs` in rounds: each round takes those that use none of the others left, in their order.
/// → (those, the rest, which use each other in a loop or use one that does, in their order)
fn use_order(defs: Vec<Def<'_>>) -> (Vec<Def<'_>>, Vec<Def<'_>>) {
let uses = uses_of(&defs);
// The round of each definition; `None` for one in a loop or one that uses such a definition.
let mut round: Vec<Option<usize>> = vec![None; defs.len()];
// A group outside a loop holds one definition, and comes after the groups it uses.
for (group, looped) in components(&uses) {
for v in group.into_iter().filter(|_| !looped) {
let r = uses[v].iter().try_fold(0, |r, &w| round.get(w).copied().flatten().map(|k| r.max(k + 1)));
round[v] = r;
}
}
let mut ready: Vec<(usize, usize)> = round.iter().enumerate().filter_map(|(i, r)| r.map(|r| (r, i))).collect();
ready.sort_unstable();
let order = ready.into_iter().filter_map(|(_, i)| defs.get(i).copied()).collect();
let rest = defs.iter().zip(&round).filter(|(_, r)| r.is_none()).map(|(def, _)| *def).collect();
(order, rest)
}
/// The positions in `defs` of the definitions each one uses, itself included.
fn uses_of(defs: &[Def<'_>]) -> Vec<Vec<usize>> {
let mut at: BTreeMap<(Res, &str), Vec<usize>> = BTreeMap::new();
for (i, def) in defs.iter().enumerate() {
at.entry(def.key()).or_default().push(i);
}
defs.iter()
.map(|def| {
let mut to = vec![];
def.each_use(&mut |res, n| to.extend(at.get(&(res, n)).into_iter().flatten()));
to.sort_unstable();
to.dedup();
to
})
.collect()
}
/// `rest` ([`use_order`]'s definitions that use each other in a loop or use one that does) split
/// into its loops and the definitions outside a loop, each group after the groups it uses:
/// (positions in `rest`, in their order; true for a loop, definitions that use each other or one
/// that uses itself).
fn loop_groups(rest: &[Def<'_>]) -> Vec<(Vec<usize>, bool)> {
components(&uses_of(rest))
}
/// The strongly connected components of the graph `uses` (each node's successors), each after
/// those it reaches: (nodes, in their order; true for a loop, two or more nodes or one that uses
/// itself).
fn components(uses: &[Vec<usize>]) -> Vec<(Vec<usize>, bool)> {
// Tarjan's algorithm, with an explicit stack of calls.
const NONE: usize = usize::MAX;
let (mut index, mut low, mut on) = (vec![NONE; uses.len()], vec![0; uses.len()], vec![false; uses.len()]);
let (mut stack, mut next, mut groups) = (vec![], 0, vec![]);
for root in 0..uses.len() {
if index[root] != NONE {
continue;
}
let mut calls: Vec<(usize, usize)> = vec![(root, 0)];
(index[root], low[root], on[root]) = (next, next, true);
next += 1;
stack.push(root);
while let Some(&(v, k)) = calls.last() {
if let Some(&w) = uses[v].get(k) {
if let Some(top) = calls.last_mut() {
top.1 += 1;
}
if index[w] == NONE {
(index[w], low[w], on[w]) = (next, next, true);
next += 1;
stack.push(w);
calls.push((w, 0));
} else if on[w] {
low[v] = low[v].min(index[w]);
}
continue;
}
calls.pop();
if let Some(&(u, _)) = calls.last() {
low[u] = low[u].min(low[v]);
}
if low[v] == index[v] {
let mut group = vec![];
while let Some(w) = stack.pop() {
on[w] = false;
group.push(w);
if w == v {
break;
}
}
group.sort_unstable();
let looped = group.len() > 1 || uses[v].contains(&v);
groups.push((group, looped));
}
}
}
groups
}
/// The names in `d` that `looped` (a loop of definitions that use each other, [`loop_groups`])
/// all have at once, each the same once relinked by `r` with those names: each one's name or a
/// numbered copy of it. The names are picked one definition at a time, in order, and each
/// definition is compared as soon as the names of those it uses are picked, so a pick that does
/// not fit is dropped with every combination that follows from it; at most 65,536 comparisons are
/// made. `None` when no combination fits, or none was found within that many comparisons. The brush
/// library of `d` is read once, the first time a brush is compared.
fn names_of_loop(d: &Document, looped: &[Def<'_>], r: &Relink) -> Option<Vec<String>> {
let library = OnceCell::new();
let brushes = Brushes::Lazy(d, &library);
let names: Vec<Vec<String>> = looped.iter().map(|def| def.names_in(d, &brushes)).collect();
if names.is_empty() || names.iter().any(Vec::is_empty) {
return None;
}
// The definitions compared once the name at each position is picked.
let mut compare_at: Vec<Vec<usize>> = vec![vec![]; looped.len()];
for (i, uses) in uses_of(looped).iter().enumerate() {
compare_at[uses.iter().copied().fold(i, usize::max)].push(i);
}
let (mut pick, mut guess, mut budget, mut k) = (vec![0usize; looped.len()], r.clone(), 1usize << 16, 0);
loop {
let (res, own) = looped[k].key();
guess.renames.entry(res).or_default().insert(own.to_string(), names[k][pick[k]].clone());
let fits = compare_at[k].iter().all(|&i| {
budget = budget.saturating_sub(1);
looped[i].same_in(d, &brushes, &names[i][pick[i]], &guess)
});
if fits && k + 1 == looped.len() {
return Some(names.iter().zip(&pick).map(|(l, &p)| l[p].clone()).collect());
}
if budget == 0 {
return None;
}
if fits {
k += 1;
pick[k] = 0;
continue;
}
// The next name at this position, or back to the position before.
loop {
pick[k] += 1;
if pick[k] < names[k].len() {
break;
}
k = k.checked_sub(1)?;
}
}
}
/// What pasting did to the document's resources (part of the paste result).
#[derive(Debug, Default)]
pub(crate) struct Imported {
relink: Relink,
/// Whether relinking can change a pasted copy: a resource has another name or color in the
/// document, or the objects link a graphic style.
relinks: bool,
/// Resources added to the document (renamed ones included).
pub added: usize,
/// Swatch conflicts resolved by merging.
pub merged: usize,
/// `{kind, from, to}` for every resource the pasted objects now know under another name: added
/// renamed, or a copy added renamed by an earlier paste.
pub renamed: Vec<Value>,
}
impl Imported {
fn placed(&mut self, r: Res, from: &str, p: &Placed) {
if matches!(p, Placed::Added | Placed::Renamed(_)) {
self.added += 1;
}
if let Placed::Reused(to) | Placed::Renamed(to) = p {
self.renamed.push(json!({"kind": r.id(), "from": from, "to": to}));
self.relink.renames.entry(r).or_default().insert(from.to_string(), to.clone());
}
}
/// Point a pasted copy at the document's resources.
pub(crate) fn apply(&self, n: &mut Node) {
if self.relinks {
self.relink.node(n);
}
}
}
impl Clipboard {
/// `nodes` (from `src`) and the resources of `src` they use.
pub(crate) fn gather(src: &Document, nodes: Vec<Node>) -> Self {
let library = OnceCell::new();
let used = Used::of(&Defs::of(src, &library), &nodes);
Self::with_used(src, &used, &library, nodes)
}
/// `nodes` and the resources of `src` that `used` names (`library`: the brush library of `src`,
/// read when a brush was used).
fn with_used(src: &Document, used: &Used, library: &OnceCell<Vec<Brush>>, nodes: Vec<Node>) -> Self {
let pick = |r: Res, list: &[TextStyleDef]| list.iter().filter(|s| used.has(r, &s.name)).cloned().collect::<Vec<_>>();
// Read when a brush was used.
let brushes = match (used.names.get(&Res::Brush), library.get()) {
(Some(names), Some(library)) => library.iter().filter(|b| names.contains(&b.name)).cloned().collect(),
_ => vec![],
};
Self {
images: src.images.iter().filter(|(k, _)| used.has(Res::Image, k)).map(|(k, b)| (k.clone(), b.clone())).collect(),
symbols: src.symbols.iter().filter(|s| used.has(Res::Symbol, &s.name)).cloned().collect(),
symbol_options: src
.symbols
.iter()
.filter(|s| used.has(Res::Symbol, &s.name))
.map(|s| (s.name.clone(), SymbolOptions::of(src, &s.name)))
.collect(),
patterns: src.patterns.iter().filter(|p| used.has(Res::Pattern, &p.name)).cloned().collect(),
swatches: src.swatches_iter().filter(|w| used.carries(w)).cloned().collect(),
graphic_styles: src.graphic_styles.iter().filter(|g| g.id != 0 && used.styles.contains(&g.id)).cloned().collect(),
char_styles: pick(Res::CharStyle, &src.char_styles),
para_styles: pick(Res::ParaStyle, &src.para_styles),
brushes,
nodes,
..Default::default()
}
}
/// The resources the objects use now (Paste without Formatting leaves text styles behind).
pub(super) fn used(&self) -> Used {
let defs = Defs {
swatches: self.swatches.iter().collect(),
symbols: &self.symbols,
patterns: &self.patterns,
styles: &self.graphic_styles,
char_styles: &self.char_styles,
para_styles: &self.para_styles,
brushes: Brushes::Ready(&self.brushes),
index: DefIndex::default(),
};
Used::of(&defs, &self.nodes)
}
/// The used global and spot colours whose name `d` gives another one: (pasted, document).
pub(super) fn conflicts<'a, 'd>(&'a self, d: &'d Document, used: &Used) -> Vec<(&'a Swatch, &'d Swatch)> {
self.swatches
.iter()
.filter(|w| is_base(w) && used.has(Res::Swatch, &w.name))
.filter_map(|w| d.swatch(&w.name).filter(|t| is_base(t) && (t.paint != w.paint || t.spot != w.spot)).map(|t| (w, t)))
.collect()
}
/// Add the resources the objects use to `d` (inside the paste's undo step) and work out how
/// pasted copies relink to them. An error, with `d` unchanged, when a pasted SVG filter finds no
/// free name ([`Self::filter_links`]).
pub(crate) fn import_into(&self, d: &mut Document, choices: &SwatchChoices, same_doc: bool) -> Result<Imported> {
self.import_used(d, &self.used(), choices, same_doc, false)
}
/// [`Self::import_into`] for the resources `used` names. The character and paragraph styles
/// come before the symbols and patterns, whose art may set type in them; with `in_use_order`,
/// each style, symbol, pattern and brush comes after the styles, symbols, patterns and brushes it uses
/// ([`use_order`]), so that it is compared and relinked with their names here, and each loop
/// of definitions that use each other is matched on its own ([`loop_groups`]).
fn import_used(&self, d: &mut Document, used: &Used, choices: &SwatchChoices, same_doc: bool, in_use_order: bool) -> Result<Imported> {
let mut out = Imported::default();
out.relink.filters = self.filter_links(d, same_doc)?;
self.import_images(d, used, same_doc, &mut out);
self.import_swatches(d, used, choices, same_doc, &mut out);
// Brushes whose art shows no symbols or patterns, before the symbols and patterns whose art
// they may brush; the others once those are in. With `in_use_order`, the brushes are
// placed in use order with the others.
let mut added_brushes: HashSet<String> =
if in_use_order { HashSet::new() } else { self.import_brushes(d, used, same_doc, false, &mut out).into_iter().collect() };
let mut defs: Vec<Def<'_>> = vec![];
for (r, list) in [(Res::CharStyle, &self.char_styles), (Res::ParaStyle, &self.para_styles)] {
defs.extend(list.iter().filter(|s| used.has(r, &s.name)).map(|s| Def::Style(r, s)));
}
defs.extend(self.symbols.iter().filter(|s| used.has(Res::Symbol, &s.name)).map(|s| Def::Symbol(s, self.symbol_options.get(&s.name))));
defs.extend(self.patterns.iter().filter(|p| used.has(Res::Pattern, &p.name)).map(Def::Pattern));
if in_use_order {
defs.extend(self.brushes.iter().filter(|b| used.has(Res::Brush, &b.name)).map(Def::Brush));
}
// Groups to place in order; true for a loop of definitions that use each other.
let groups: Vec<(Vec<Def<'_>>, bool)> = if in_use_order {
let (order, rest) = use_order(defs);
let loops = loop_groups(&rest).into_iter().map(|(at, looped)| (at.into_iter().filter_map(|i| rest.get(i).copied()).collect(), looped));
std::iter::once((order, false)).chain(loops).collect()
} else {
vec![(defs, false)]
};
// Styles (their colors relinked first), symbols and patterns (compared once relinked,
// symbols with their options too). The symbols and patterns added are relinked at the end,
// when every new name and graphic style id is known; with `in_use_order`, so are the
// styles added, which a loop of uses may place before a pattern they paint with.
let (mut added_symbols, mut added_patterns, mut added_styles) = (HashSet::new(), HashSet::new(), vec![]);
// Each definition, whether it is in a loop, and the loop it starts.
let steps = groups
.iter()
.flat_map(|(group, looped)| group.iter().enumerate().map(move |(i, def)| (*def, *looped, (*looped && i == 0).then_some(group))));
// In a loop the document does not have all at once, every definition uses one that is
// added under a free name, so each one is added too.
let mut fresh = false;
// The names in the document of the loop being placed, when it has them all at once.
let mut matched: BTreeMap<(Res, String), String> = BTreeMap::new();
// The document's brush library, read the first time a brush is placed in use order and
// stored again after each brush added; nothing else in the loop changes it.
let mut library: Option<Vec<Brush>> = None;
for (def, looped, starts) in steps {
fresh &= looped;
if !looped {
matched.clear();
}
// A loop of uses that the document has all at once is known under those names before
// its definitions are compared.
if let Some(group) = starts {
matched.clear();
match names_of_loop(d, group, &out.relink) {
Some(names) => {
for (def, name) in group.iter().zip(names) {
let (res, own) = def.key();
if own != name {
out.relink.renames.entry(res).or_default().insert(own.to_string(), name.clone());
}
matched.insert((res, own.to_string()), name);
}
fresh = false;
}
None => fresh = true,
}
}
// A definition of such a loop is the document's under the name found for it.
let (res, own) = def.key();
if let Some(name) = matched.get(&(res, own.to_string())) {
let p = if name == own { Placed::Existing } else { Placed::Reused(name.clone()) };
out.placed(res, own, &p);
continue;
}
match def {
Def::Style(r, def) => {
let mut s = def.clone();
out.relink.attrs(&mut s.attrs);
let theirs = if r == Res::CharStyle { &mut d.char_styles } else { &mut d.para_styles };
let p = place(theirs, &s, same_doc, |a, b| !fresh && a == b);
if in_use_order && let Some(name) = p.new_name(&def.name) {
added_styles.push((r, name, def));
}
out.placed(r, &def.name, &p);
}
Def::Symbol(def, options) => {
let theirs: HashMap<String, SymbolOptions> = match options {
Some(_) => d
.symbols
.iter()
.filter(|s| copy_of(&def.name, &s.name))
.map(|s| (s.name.clone(), SymbolOptions::of(d, &s.name)))
.collect(),
None => HashMap::new(),
};
let r = &out.relink;
let p = place(&mut d.symbols, def, same_doc, |a, b| {
!fresh
&& options.is_none_or(|o| theirs.get(&a.name) == Some(o))
&& same_art(r, std::slice::from_ref(&a.art), std::slice::from_ref(&b.art))
});
if let Some(name) = p.new_name(&def.name) {
if let Some(o) = options {
o.give(d, &name);
}
added_symbols.insert(name);
}
out.placed(Res::Symbol, &def.name, &p);
}
Def::Pattern(def) => {
// Tile options alike, and the art once relinked.
let bare = |p: &PatternDef| PatternDef { name: String::new(), art: vec![], ..p.clone() };
let same = |n: &str| !fresh && d.pattern(n).is_some_and(|x| bare(x) == bare(def) && same_art(&out.relink, &x.art, &def.art));
let p = existing(&def.name, d.patterns.iter().map(|p| p.name.as_str()), same_doc, same).unwrap_or_else(|| {
// Its swatch comes along under the same name.
let name = unique_name(&def.name, |n| d.pattern(n).is_some() || d.swatch_name_taken(n));
add_pattern(d, PatternDef { name: name.clone(), ..def.clone() });
Placed::added(&def.name, name)
});
added_patterns.extend(p.new_name(&def.name));
out.placed(Res::Pattern, &def.name, &p);
}
Def::Brush(b) => {
let lib = library.get_or_insert_with(|| vectorcraft_brush::library(d));
let r = &out.relink;
let p = place(lib, b, same_doc, |ours, theirs| !fresh && same_brush(r, ours, theirs));
if let Some(name) = p.new_name(&b.name) {
vectorcraft_brush::store(d, lib);
added_brushes.insert(name);
}
out.placed(Res::Brush, &b.name, &p);
}
}
}
if !in_use_order {
added_brushes.extend(self.import_brushes(d, used, same_doc, true, &mut out));
}
self.import_graphic_styles(d, used, same_doc, &mut out);
for (r, name, def) in added_styles {
let theirs = if r == Res::CharStyle { &mut d.char_styles } else { &mut d.para_styles };
if let Some(s) = theirs.iter_mut().find(|s| s.name == name) {
s.attrs = def.attrs.clone();
out.relink.attrs(&mut s.attrs);
}
}
for s in d.symbols.iter_mut().filter(|s| added_symbols.contains(&s.name)) {
out.relink.node(Arc::make_mut(&mut s.art));
}
for p in d.patterns.iter_mut().filter(|p| added_patterns.contains(&p.name)) {
for a in &mut p.art {
out.relink.node(Arc::make_mut(a));
}
}
if !added_brushes.is_empty() {
let mut lib = vectorcraft_brush::library(d);
for b in lib.iter_mut().filter(|b| added_brushes.contains(&b.name)) {
b.art_mut().into_iter().for_each(|a| out.relink.node(a));
}
vectorcraft_brush::store(d, &lib);
}
out.relinks = !(out.relink.renames.is_empty()
&& out.relink.filters.is_empty()
&& out.relink.styles.is_empty()
&& out.relink.colors.is_empty()
&& used.styles.is_empty());
Ok(out)
}
/// How the pasted SVG filter effects relink ([`Relink::filters`]). Each names the document's
/// filter of its name; one that draws another definition than that filter takes the document's
/// filter when pasted back into the document it came from, and elsewhere keeps its own under a
/// name no filter or effect of the document gives, so editing or deleting the document's filter leaves it alone. When that name would
/// make the filter too long to draw, the free name is a shorter start of its name ("Shif"), else
/// a free letter, digit or other printable ASCII character; the filter's id is always its name. An error when no free name fits.
fn filter_links(&self, d: &Document, same_doc: bool) -> Result<BTreeMap<(String, String), (String, String)>> {
let mut found = BTreeSet::new();
for n in &self.nodes {
node_filters(n, &mut found);
}
for s in &self.symbols {
node_filters(&s.art, &mut found);
}
for a in self.patterns.iter().flat_map(|p| p.art.iter()) {
node_filters(a, &mut found);
}
for a in self.brushes.iter().flat_map(Brush::art) {
node_filters(a, &mut found);
}
for g in &self.graphic_styles {
appearance_filters(&g.appearance, &mut found);
}
let mut taken: BTreeSet<String> = d.svg_filters.iter().map(|f| f.name.clone()).chain(found.iter().map(|(n, _)| n.clone())).collect();
let mut effects_read = false;
let mut links = BTreeMap::new();
for (name, xml) in found {
let Some(held) = d.svg_filters.iter().find(|f| f.name == name).filter(|f| f.xml != xml.trim()) else { continue };
let to = if same_doc {
(name.clone(), held.xml.clone())
} else {
// The names the document's effects give are taken too, those whose filter was
// deleted included: defining a filter of that name later changes every effect that
// names it. They are read at the first rename only.
if !effects_read {
effects_read = true;
for root in d.art_roots() {
node_filter_names(root, &mut taken);
}
for a in vectorcraft_brush::library(d).iter().flat_map(Brush::art) {
node_filter_names(a, &mut taken);
}
for g in &d.graphic_styles {
appearance_filter_names(&g.appearance, &mut taken);
}
}
// Free names made from the starts of the name, longest first, then free letters and
// digits, then other single printable ASCII characters: the first under which the
// filter still draws.
let mut ends: Vec<usize> = name.char_indices().map(|(i, _)| i).skip(1).collect();
ends.push(name.len());
let starts = ends.iter().rev().filter_map(|&end| name.get(..end).map(str::trim_end)).filter(|b| !b.is_empty());
let shorter = starts.map(|b| unique_name(b, |n| taken.contains(n)));
let single = ('a'..='z')
.chain('0'..='9')
.chain('A'..='Z')
.chain(('!'..='~').filter(|c| !c.is_ascii_alphanumeric()))
.map(String::from)
.filter(|n| !taken.contains(n));
let renamed = shorter.chain(single).find_map(|new| {
let drawn = vectorcraft_render::effects::svg_filter::with_id(&xml, &new)
.filter(|x| vectorcraft_render::effects::svg_filter::check(x).is_ok_and(|id| id == new))?;
Some((new, drawn))
});
let Some((new, drawn)) = renamed else {
return Err(EngineError::Other(format!(
"the pasted SVG filter {name:?} draws differently from the document's filter of that name, and no free name fits it"
)));
};
taken.insert(new.clone());
(new, drawn)
};
links.insert((name, xml), to);
}
Ok(links)
}
/// Image blobs: a different blob under a key the document uses gets a new key.
fn import_images(&self, d: &mut Document, used: &Used, same_doc: bool, out: &mut Imported) {
for (key, blob) in self.images.iter().filter(|(k, _)| used.has(Res::Image, k)) {
let p = existing(key, d.images.keys().map(String::as_str), same_doc, |k| d.images.get(k) == Some(blob)).unwrap_or_else(|| {
let k = unique_name(key, |n| d.images.contains_key(n));
d.images.insert(k.clone(), blob.clone());
Placed::added(key, k)
});
out.placed(Res::Image, key, &p);
}
}
/// The brushes whose art shows symbols or patterns (`late`), or the others: each one the
/// document has (its art compared once relinked) is used, the others are added. → the names of
/// those added, whose art is relinked at the end.
fn import_brushes(&self, d: &mut Document, used: &Used, same_doc: bool, late: bool, out: &mut Imported) -> Vec<String> {
let brushes: Vec<&Brush> = self.brushes.iter().filter(|b| used.has(Res::Brush, &b.name) && shows_definitions(b) == late).collect();
if brushes.is_empty() {
return vec![];
}
let mut lib = vectorcraft_brush::library(d);
let mut added = vec![];
for b in brushes {
let r = &out.relink;
let p = place(&mut lib, b, same_doc, |ours, theirs| same_brush(r, ours, theirs));
added.extend(p.new_name(&b.name));
out.placed(Res::Brush, &b.name, &p);
}
if !added.is_empty() {
vectorcraft_brush::store(d, &lib);
}
added
}
/// Global and spot colours (a name conflict merges or adds as `choices` say), then the tint
/// swatches of the tints used and the gradient swatches.
fn import_swatches(&self, d: &mut Document, used: &Used, choices: &SwatchChoices, same_doc: bool, out: &mut Imported) {
let conflicts: BTreeSet<String> =
if same_doc { BTreeSet::new() } else { self.conflicts(d, used).into_iter().map(|(w, _)| w.name.clone()).collect() };
let bases: Vec<&Swatch> = self.swatches.iter().filter(|w| is_base(w) && used.has(Res::Swatch, &w.name)).collect();
for w in &bases {
let p = if conflicts.contains(&w.name) && choices.of(&w.name) == SwatchChoice::Merge {
out.merged += 1;
Placed::Existing
} else {
add_swatch(d, w, same_doc)
};
out.placed(Res::Swatch, &w.name, &p);
}
// Linked colours show the document's swatch of their link.
for w in &bases {
let name = out.relink.renamed(Res::Swatch, &w.name);
if let Some(c) = d.global_color(&name) {
out.relink.colors.insert(name, c);
}
}
// Then the tint and gradient swatches, linked like the objects.
for def in self.swatches.iter().filter(|w| !is_base(w) && used.carries(w)) {
let mut w = def.clone();
out.relink.paint(&mut w.paint);
let p = add_swatch(d, &w, same_doc);
out.placed(Res::Swatch, &def.name, &p);
}
}
/// Graphic styles: in the source document by id, elsewhere by name and look.
fn import_graphic_styles(&self, d: &mut Document, used: &Used, same_doc: bool, out: &mut Imported) {
for g in self.graphic_styles.iter().filter(|g| used.styles.contains(&g.id)) {
let mut new = g.clone();
out.relink.appearance(&mut new.appearance);
let found = match d.graphic_styles.iter().position(|x| same_doc && x.id == g.id) {
Some(i) => Some(i),
None => {
let same = |n: &str| d.graphic_style(n).is_some_and(|x| x.same_look(&new));
let p = existing(&g.name, d.graphic_styles.iter().map(|x| x.name.as_str()), same_doc, same);
// Objects follow the style's id, not its name.
p.and_then(|p| d.graphic_style_index(if let Placed::Reused(n) = &p { n } else { &g.name }))
}
};
let i = match found {
Some(i) => i,
None => {
let name = unique_name(&g.name, |n| d.graphic_style(n).is_some());
out.placed(Res::GraphicStyle, &g.name, &Placed::added(&g.name, name.clone()));
let id = d.next_graphic_style_id();
d.graphic_styles.push(GraphicStyle { name, id, ..new });
d.graphic_styles.len() - 1
}
};
out.relink.styles.insert(g.id, d.graphic_style_id(i));
}
}
}
/// Where a loaded text style is in the document ([`import_text_styles`]).
pub(crate) struct LoadedStyle {
/// A paragraph style (else a character style).
pub para: bool,
/// Its name in the document.
pub name: String,
/// It was added (else the document's own was the same).
pub added: bool,
}
/// Add every character style of `src` (with `chars`) and every paragraph style (with `paras`) to
/// `d`, with the swatches, patterns and what their art uses (styles of the other kind included)
/// that the styles' fill and stroke lead to, as Paste adds those of pasted type
/// ([`Clipboard::import_into`]; a global or spot color whose name `d` gives another color is
/// `d`'s). Each style, symbol and pattern is placed after those it uses, so that a style follows
/// a pattern added under a free name and a pattern follows a style renamed on the way in. A style
/// `d` has under its name, or a numbered copy of it ("Body 2"), that is the same once relinked is
/// used; any other is added under a free name. → each style of `src` placed, in order, character
/// styles first. An error, as [`Clipboard::import_into`] gives.
pub(crate) fn import_text_styles(src: &Document, d: &mut Document, chars: bool, paras: bool) -> Result<Vec<LoadedStyle>> {
let library = OnceCell::new();
let used = Used::of_text_styles(&Defs::of(src, &library), chars, paras);
let clip = Clipboard::with_used(src, &used, &library, vec![]);
let before = [&d.char_styles, &d.para_styles].map(|l| l.iter().map(|s| s.name.clone()).collect::<BTreeSet<_>>());
let out = clip.import_used(d, &used, &SwatchChoices::default(), false, true)?;
let mut placed: Vec<LoadedStyle> = vec![];
for (para, r, list) in [(false, Res::CharStyle, &clip.char_styles), (true, Res::ParaStyle, &clip.para_styles)] {
for s in list {
let name = out.relink.renamed(r, &s.name);
let again = placed.iter().any(|p| p.para == para && p.name == name);
let added = !again && !before[usize::from(para)].contains(&name);
placed.push(LoadedStyle { para, name, added });
}
}
Ok(placed)
}
/// Swatch `w` in `d` ([`existing`]), else added under a free name (swatches and colour groups
/// share one namespace).
fn add_swatch(d: &mut Document, w: &Swatch, same_doc: bool) -> Placed {
let same = |n: &str| d.swatch(n).is_some_and(|t| *t == Swatch { name: n.to_string(), ..w.clone() });
existing(&w.name, d.swatches_iter().map(|t| t.name.as_str()), same_doc, same).unwrap_or_else(|| {
let name = d.free_swatch_name(&w.name);
d.swatches.push(Swatch { name: name.clone(), ..w.clone() });
Placed::added(&w.name, name)
})
}