mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 03:06:57 -06:00
file.place {path | name+dataBase64, link, template, replace, at, rect, page, crop} places a
file as one undo step without touching the clipboard. Raster images come in at 100% of the
resolution their file declares (PNG pHYs, JFIF/EXIF, TIFF, BMP; 72 ppi otherwise), linked to
their path by default; SVG becomes one group with its embedded images; a PDF/.ai page or a
native document's artboard becomes one clipped group. The images, symbols, patterns and
swatches the art uses join the document (renamed on conflict). Replace keeps the replaced
object's stacking place and transform; Template puts the art on a new locked template layer
below the current one. file.place.info and image.info report size, ppi and colour mode.
File > Place picks a file and opens the Place dialog (Link, Template, Replace); the web picks
through the inbox. The Control bar shows Linked File/Embedded, the file name, colour mode and
ppi of the selected image.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
232 lines
8.1 KiB
Rust
232 lines
8.1 KiB
Rust
//! Resources placed art brings along: the images, symbols, pattern swatches and colour swatches it
|
|
//! uses (also through symbol and pattern art and through tint swatches) join the document. A name
|
|
//! the document already uses for something different gets a free variant (`Star 2`,
|
|
//! `pdf-image-1 2`…) and the art's references follow; a colour linked to a swatch the document
|
|
//! defines differently keeps its colour, unlinked. Text styles are left as they are: the art carries
|
|
//! its own formatting.
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::sync::Arc;
|
|
|
|
use vectorcraft_color::Paint;
|
|
use vectorcraft_doc::{AppearanceItem, Document, Node, NodeKind};
|
|
|
|
use super::unique_name;
|
|
|
|
/// Visit `n` and every descendant (children and opacity-mask art) mutably.
|
|
pub(super) fn each_mut(n: &mut Node, f: &mut impl FnMut(&mut Node)) {
|
|
f(n);
|
|
if let Some(m) = &mut n.mask {
|
|
each_mut(Arc::make_mut(&mut m.art), f);
|
|
}
|
|
if let Some(ch) = n.children_mut() {
|
|
for c in ch {
|
|
each_mut(Arc::make_mut(c), f);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A reference from art to a document resource.
|
|
enum Ref<'a> {
|
|
Image(&'a mut String),
|
|
Symbol(&'a mut String),
|
|
Pattern(&'a mut String),
|
|
/// A colour's (or gradient's) swatch link; renaming it to `None` unlinks it.
|
|
Swatch(&'a mut Option<String>),
|
|
}
|
|
|
|
/// Every resource reference held by paint `p`.
|
|
fn paint_refs(p: &mut Paint, f: &mut impl FnMut(Ref)) {
|
|
match p {
|
|
Paint::Pattern { pattern, .. } => f(Ref::Pattern(pattern)),
|
|
Paint::Gradient(g) if g.swatch.is_some() => f(Ref::Swatch(&mut g.swatch)),
|
|
_ => {}
|
|
}
|
|
p.map_links(&mut |_, link, _| {
|
|
if link.is_some() {
|
|
f(Ref::Swatch(link));
|
|
}
|
|
false
|
|
});
|
|
}
|
|
|
|
/// Every resource reference in `n` and its descendants.
|
|
fn refs(n: &mut Node, f: &mut impl FnMut(Ref)) {
|
|
each_mut(n, &mut |n| {
|
|
for it in &mut n.appearance.items {
|
|
match it {
|
|
AppearanceItem::Fill(fl) => paint_refs(&mut fl.paint, f),
|
|
AppearanceItem::Stroke(st) => paint_refs(&mut st.paint, f),
|
|
}
|
|
}
|
|
match &mut n.kind {
|
|
NodeKind::Text(t) => {
|
|
for r in &mut t.runs {
|
|
paint_refs(&mut r.style.fill, f);
|
|
paint_refs(&mut r.style.stroke, f);
|
|
}
|
|
}
|
|
NodeKind::Image(im) => f(Ref::Image(&mut im.key)),
|
|
NodeKind::SymbolInstance { symbol, .. } => f(Ref::Symbol(symbol)),
|
|
_ => {}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Resource names in first-use order.
|
|
#[derive(Default)]
|
|
struct Used {
|
|
images: Vec<String>,
|
|
symbols: Vec<String>,
|
|
patterns: Vec<String>,
|
|
swatches: Vec<String>,
|
|
}
|
|
|
|
impl Used {
|
|
fn note(&mut self, r: Ref) {
|
|
let (list, name) = match r {
|
|
Ref::Image(k) => (&mut self.images, &*k),
|
|
Ref::Symbol(k) => (&mut self.symbols, &*k),
|
|
Ref::Pattern(k) => (&mut self.patterns, &*k),
|
|
Ref::Swatch(Some(k)) => (&mut self.swatches, &*k),
|
|
Ref::Swatch(None) => return,
|
|
};
|
|
if !list.contains(name) {
|
|
list.push(name.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// What each resource the art uses is called in the document (`None`: a swatch link to drop) and
|
|
/// whether its definition is copied in.
|
|
type Renames = BTreeMap<String, (Option<String>, bool)>;
|
|
|
|
/// Names in `dst` for the `used` definitions of `src` (`def`): the same name when `dst` has none
|
|
/// (copied) or an equal one (shared), else a free variant (`taken`, copied).
|
|
fn names<T: PartialEq>(
|
|
used: &[String],
|
|
def: impl Fn(&str) -> Option<T>,
|
|
existing: impl Fn(&str) -> Option<T>,
|
|
taken: impl Fn(&str) -> bool,
|
|
) -> Renames {
|
|
let mut out = Renames::new();
|
|
for name in used {
|
|
let Some(d) = def(name) else { continue };
|
|
let entry = match existing(name) {
|
|
None => (Some(name.clone()), true),
|
|
Some(e) if e == d => (Some(name.clone()), false),
|
|
Some(_) => (Some(unique_name(name, |c| taken(c) || out.values().any(|(n, _)| n.as_deref() == Some(c)))), true),
|
|
};
|
|
out.insert(name.clone(), entry);
|
|
}
|
|
out
|
|
}
|
|
|
|
/// The renamed resources whose definitions are copied: (name in `src`, name in `dst`).
|
|
fn copies(m: &Renames) -> Vec<(&str, &str)> {
|
|
m.iter().filter(|(_, (_, copy))| *copy).filter_map(|(old, (new, _))| Some((old.as_str(), new.as_deref()?))).collect()
|
|
}
|
|
|
|
/// Bring the resources `nodes` (art from `src`) use into `dst`, renaming on conflict, and point
|
|
/// `nodes` at the names they got.
|
|
pub(super) fn adopt(dst: &mut Document, src: &Document, nodes: &mut [Node]) {
|
|
let mut used = Used::default();
|
|
for n in nodes.iter_mut() {
|
|
refs(n, &mut |r| used.note(r));
|
|
}
|
|
// Symbol and pattern art and tint swatches can use more resources (the lists grow while they
|
|
// are walked).
|
|
let (mut si, mut pi, mut wi) = (0, 0, 0);
|
|
loop {
|
|
let mut art: Vec<Node> = vec![];
|
|
if let Some(name) = used.symbols.get(si) {
|
|
si += 1;
|
|
art.extend(src.symbols.iter().find(|s| s.name == *name).map(|s| (*s.art).clone()));
|
|
} else if let Some(name) = used.patterns.get(pi) {
|
|
pi += 1;
|
|
art.extend(src.pattern(name).into_iter().flat_map(|p| p.art.iter().map(|a| (**a).clone())));
|
|
} else if let Some(name) = used.swatches.get(wi) {
|
|
wi += 1;
|
|
if let Some(mut paint) = src.swatch(name).map(|w| w.paint.clone()) {
|
|
paint_refs(&mut paint, &mut |r| used.note(r));
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
for mut a in art {
|
|
refs(&mut a, &mut |r| used.note(r));
|
|
}
|
|
}
|
|
|
|
let images = names(&used.images, |k| src.images.get(k), |k| dst.images.get(k), |c| dst.images.contains_key(c));
|
|
let symbols = names(
|
|
&used.symbols,
|
|
|n| src.symbols.iter().find(|s| s.name == n),
|
|
|n| dst.symbols.iter().find(|s| s.name == n),
|
|
|c| dst.symbols.iter().any(|s| s.name == c),
|
|
);
|
|
let patterns = names(&used.patterns, |n| src.pattern(n), |n| dst.pattern(n), |c| dst.pattern(c).is_some());
|
|
// A swatch the document defines differently isn't renamed (that would add look-alike swatches):
|
|
// the colours linked to it keep their look, unlinked.
|
|
let swatches: Renames = used
|
|
.swatches
|
|
.iter()
|
|
.filter_map(|n| {
|
|
let w = src.swatch(n)?;
|
|
let entry = match dst.swatch(n) {
|
|
None => (Some(n.clone()), true),
|
|
Some(e) if e == w => (Some(n.clone()), false),
|
|
Some(_) => (None, false),
|
|
};
|
|
Some((n.clone(), entry))
|
|
})
|
|
.collect();
|
|
|
|
let mut rename = |r: Ref| {
|
|
let (map, k) = match r {
|
|
Ref::Image(k) => (&images, k),
|
|
Ref::Symbol(k) => (&symbols, k),
|
|
Ref::Pattern(k) => (&patterns, k),
|
|
Ref::Swatch(link) => {
|
|
if let Some((new, _)) = link.as_deref().and_then(|k| swatches.get(k)) {
|
|
*link = new.clone();
|
|
}
|
|
return;
|
|
}
|
|
};
|
|
if let Some((Some(new), _)) = map.get(k.as_str()) {
|
|
*k = new.clone();
|
|
}
|
|
};
|
|
for n in nodes.iter_mut() {
|
|
refs(n, &mut rename);
|
|
}
|
|
for (old, new) in copies(&images) {
|
|
if let Some(blob) = src.images.get(old) {
|
|
dst.images.insert(new.to_string(), blob.clone());
|
|
}
|
|
}
|
|
for (old, new) in copies(&symbols) {
|
|
if let Some(mut def) = src.symbols.iter().find(|s| s.name == old).cloned() {
|
|
def.name = new.to_string();
|
|
refs(Arc::make_mut(&mut def.art), &mut rename);
|
|
dst.symbols.push(def);
|
|
}
|
|
}
|
|
for (old, new) in copies(&patterns) {
|
|
if let Some(mut def) = src.pattern(old).cloned() {
|
|
def.name = new.to_string();
|
|
for a in &mut def.art {
|
|
refs(Arc::make_mut(a), &mut rename);
|
|
}
|
|
dst.patterns.push(def);
|
|
}
|
|
}
|
|
for (old, _) in copies(&swatches) {
|
|
if let Some(mut w) = src.swatch(old).cloned() {
|
|
paint_refs(&mut w.paint, &mut rename);
|
|
dst.swatches.push(w);
|
|
}
|
|
}
|
|
}
|