Effects: a pasted SVG filter avoids brush effect names and is refused with no free name

Follows #6592. Paste now also skips the names that effects in the
document's brush art use when it picks a free name for a pasted SVG
filter. When no free name keeps the filter within the 64 KB limit (a
one-letter name at the limit with every free letter and digit taken),
the paste is refused instead of keeping the name of the document's
filter. Clipboard::import_into and import_text_styles return a Result,
and their callers pass the error on before the document changes.
This commit is contained in:
dotCooCoo
2026-11-11 06:08:58 -08:00
parent 015805322a
commit e0e361a834
11 changed files with 86 additions and 34 deletions
+1 -1
View File
@@ -496,7 +496,7 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
// nothing to apply is no edit, and a click that only applies brushes the document has is
// named for that.
let mut probe = (*st.doc).clone();
let imported = clip.import_into(&mut probe, &choices, false);
let imported = clip.import_into(&mut probe, &choices, false)?;
let doc_names: Vec<String> = picked.iter().map(|n| target(&imported.renamed, n)).collect();
let paths = if apply { brush_targets(&st.doc, &roots(s, p)?) } else { vec![] };
let paint = stroke_paint(s);
+22 -14
View File
@@ -28,7 +28,7 @@ use vectorcraft_doc::{
use super::super::brushsym::{set_natural_size, stored_natural_size};
use super::super::patterncmds::add_pattern;
use super::super::{Result, bad, unique_name};
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).
@@ -1055,8 +1055,9 @@ impl Clipboard {
}
/// Add the resources the objects use to `d` (inside the paste's undo step) and work out how
/// pasted copies relink to them.
pub(crate) fn import_into(&self, d: &mut Document, choices: &SwatchChoices, same_doc: bool) -> Imported {
/// 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)
}
@@ -1065,9 +1066,9 @@ impl Clipboard {
/// 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) -> Imported {
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);
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
@@ -1230,7 +1231,7 @@ impl Clipboard {
&& out.relink.styles.is_empty()
&& out.relink.colors.is_empty()
&& used.styles.is_empty());
out
Ok(out)
}
/// How the pasted SVG filter effects relink ([`Relink::filters`]). Each names the document's
@@ -1238,8 +1239,8 @@ impl Clipboard {
/// 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 or digit; the filter's id is always its name.
fn filter_links(&self, d: &Document, same_doc: bool) -> BTreeMap<(String, String), (String, String)> {
/// a free letter or digit; 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);
@@ -1272,6 +1273,9 @@ impl Clipboard {
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);
}
@@ -1288,13 +1292,17 @@ impl Clipboard {
.filter(|x| vectorcraft_render::effects::svg_filter::check(x).is_ok_and(|id| id == new))?;
Some((new, drawn))
});
let Some((new, drawn)) = renamed else { continue };
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);
}
links
Ok(links)
}
/// Image blobs: a different blob under a key the document uses gets a new key.
@@ -1409,13 +1417,13 @@ pub(crate) struct LoadedStyle {
/// 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.
pub(crate) fn import_text_styles(src: &Document, d: &mut Document, chars: bool, paras: bool) -> Vec<LoadedStyle> {
/// 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 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 {
@@ -1425,7 +1433,7 @@ pub(crate) fn import_text_styles(src: &Document, d: &mut Document, chars: bool,
placed.push(LoadedStyle { para, name, added });
}
}
placed
Ok(placed)
}
/// Swatch `w` in `d` ([`existing`]), else added under a free name (swatches and colour groups
+1 -1
View File
@@ -1332,7 +1332,7 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho
vec![Affine::IDENTITY]
};
let (ids, imported, artboards, into) = s.edit(mode.label(), |d, sel| {
let imported = clip.import_into(d, choices, same_doc);
let imported = clip.import_into(d, choices, same_doc)?;
// Draw Inside: the clip group of the object drawn into, made on the first paste.
let into = inside.map(|t| super::create::draw_inside_group(d, t)).transpose()?;
let artboards: Vec<usize> = boards
+2 -2
View File
@@ -1303,7 +1303,7 @@ fn use_pattern(s: &mut Session, data: &Value, stroke: bool) -> Result<String> {
let choices = SwatchChoices::default();
// The pattern's name in the document, worked out on a copy: nothing to add is no edit.
let mut probe = (*s.doc()?.doc).clone();
let imported = clip.import_into(&mut probe, &choices, false);
let imported = clip.import_into(&mut probe, &choices, false)?;
let renamed = imported.renamed.iter().find(|r| r["kind"] == "pattern" && r["from"] == name.as_str()).and_then(|r| r["to"].as_str());
let target = renamed.unwrap_or(&name).to_string();
let paint = vectorcraft_doc::pattern::pattern_paint(&target);
@@ -1315,7 +1315,7 @@ fn use_pattern(s: &mut Session, data: &Value, stroke: bool) -> Result<String> {
in_one_step(s, |s| {
if added {
s.edit("Add to Swatches", |d, _| {
clip.import_into(d, &choices, false);
clip.import_into(d, &choices, false)?;
if !has_swatch(d) {
let swatch = d.free_swatch_name(&target);
d.swatches.push(vectorcraft_color::Swatch { name: swatch, paint: paint.clone(), global: false, spot: false });
+4 -3
View File
@@ -10,6 +10,7 @@ use vectorcraft_doc::pattern::pattern_paint;
use vectorcraft_doc::{Appearance, Document, Node, NodeId, PatternDef, TileType};
use vectorcraft_geom::{PathData, Point, Rect, SubPath, shapes};
use super::Result;
use super::clipboard::Clipboard;
use super::clipboard::resources::SwatchChoices;
use super::swatchlib::LibraryInfo;
@@ -84,11 +85,11 @@ pub fn swatch_source(w: &Swatch) -> Option<Arc<Document>> {
/// Add pattern `name` of library `lib` to `d` with the resources its art uses, as pasting art
/// painted with it does: the same pattern there already is used, and a name another pattern or
/// swatch has gets a number → the pattern's name in `d`.
pub(crate) fn import(d: &mut Document, lib: &Document, name: &str) -> String {
pub(crate) fn import(d: &mut Document, lib: &Document, name: &str) -> Result<String> {
let user = Node::path(NodeId(0), shapes::rectangle(Rect::new(0.0, 0.0, 1.0, 1.0)), Appearance::basic(pattern_paint(name), Paint::None, 0.0));
let imported = Clipboard::gather(lib, vec![user]).import_into(d, &SwatchChoices::default(), false);
let imported = Clipboard::gather(lib, vec![user]).import_into(d, &SwatchChoices::default(), false)?;
let renamed = imported.renamed.iter().find(|r| r["kind"] == "pattern" && r["from"] == name).and_then(|r| r["to"].as_str());
renamed.unwrap_or(name).to_string()
Ok(renamed.unwrap_or(name).to_string())
}
/// The pattern of `d` that is pattern `name` of library `lib`: the same tile options and art under
+1 -1
View File
@@ -753,7 +753,7 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
if let (Paint::Pattern { pattern, .. }, Some(src)) = (&w.paint, &patterns) {
// The pattern comes with its swatch; a pattern the document has without one
// gets one.
let target = patternlib::import(d, src, pattern);
let target = patternlib::import(d, src, pattern)?;
let name = match patternlib::pattern_swatch(d, &target) {
Some(name) => name,
None => {
+2 -2
View File
@@ -405,7 +405,7 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
};
// Where the symbols land, worked out on a copy: nothing to add and nothing to place is no edit.
let mut probe = (*st.doc).clone();
let imported = clip.import_into(&mut probe, &choices, false);
let imported = clip.import_into(&mut probe, &choices, false)?;
let doc_names: Vec<String> = picked.iter().map(|n| target(&imported.renamed, n)).collect();
let centre = st.doc.artboards.first().map(|a| a.rect.center()).unwrap_or(Point::ZERO);
let at = if place { Some(Point::new(number_or(p, "x", centre.x, C)?, number_or(p, "y", centre.y, C)?)) } else { None };
@@ -413,7 +413,7 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
let mut out = json!({ "library": info.id });
if imported.added > 0 || place {
let placed = s.edit("Add to Symbols", |d, sel| {
let imported = clip.import_into(d, &choices, false);
let imported = clip.import_into(d, &choices, false)?;
// Every symbol brought in (nested ones too) takes its Symbol Options along.
for sym in &clip.symbols {
let to = target(&imported.renamed, &sym.name);
+2 -2
View File
@@ -976,12 +976,12 @@ fn load(s: &mut Session, p: &Value, kinds: &[Kind]) -> Result<Value> {
}
let (chars, paras) = (kinds.contains(&Kind::Char), kinds.contains(&Kind::Para));
let mut trial = (*s.doc()?.doc).clone();
let mut placed = import_text_styles(&src, &mut trial, chars, paras);
let mut placed = import_text_styles(&src, &mut trial, chars, paras)?;
// An undo step only when the document changes. The step takes the document the import made,
// unless the edit first ends a typing session, which can change the document it starts from.
if trial != *s.doc()?.doc {
if super::typing_in_progress(s) {
placed = s.edit(label, |d, _| Ok(import_text_styles(&src, d, chars, paras)))?;
placed = s.edit(label, |d, _| import_text_styles(&src, d, chars, paras))?;
} else {
s.edit(label, |d, _| {
*d = trial;
+7 -7
View File
@@ -1105,7 +1105,7 @@ fn self_referencing_pattern_is_not_added_again() {
let different = dst.execute("shape.rectangle", &json!({"x": 200, "y": 200, "width": 30, "height": 30})).unwrap()["id"].clone();
dst.execute("object.pattern.make", &json!({"name": "Dots", "edit": false, "ids": [different]})).unwrap();
let target = std::sync::Arc::make_mut(&mut dst.doc_mut().unwrap().doc);
super::super::clipboard::resources::import_text_styles(&source, target, true, true);
super::super::clipboard::resources::import_text_styles(&source, target, true, true).unwrap();
assert!(dst.doc().unwrap().doc.pattern("Dots 2").is_some());
assert!(
dst.doc().unwrap().doc.pattern("Dots 2").unwrap().art[0]
@@ -1116,7 +1116,7 @@ fn self_referencing_pattern_is_not_added_again() {
);
let before = dst.doc().unwrap().doc.clone();
let target = std::sync::Arc::make_mut(&mut dst.doc_mut().unwrap().doc);
let again = super::super::clipboard::resources::import_text_styles(&source, target, true, true);
let again = super::super::clipboard::resources::import_text_styles(&source, target, true, true).unwrap();
assert!(again.iter().all(|s| !s.added));
assert_eq!(dst.doc().unwrap().doc, before);
}
@@ -1148,13 +1148,13 @@ fn separate_self_referencing_patterns_are_not_added_again() {
dst.execute("object.pattern.make", &json!({"name": format!("Dots{k}"), "edit": false, "ids": [different]})).unwrap();
}
let target = std::sync::Arc::make_mut(&mut dst.doc_mut().unwrap().doc);
super::super::clipboard::resources::import_text_styles(&source, target, true, true);
super::super::clipboard::resources::import_text_styles(&source, target, true, true).unwrap();
for k in 0..9 {
assert!(dst.doc().unwrap().doc.pattern(&format!("Dots{k} 2")).is_some(), "{k}");
}
let before = dst.doc().unwrap().doc.clone();
let target = std::sync::Arc::make_mut(&mut dst.doc_mut().unwrap().doc);
let again = super::super::clipboard::resources::import_text_styles(&source, target, true, true);
let again = super::super::clipboard::resources::import_text_styles(&source, target, true, true).unwrap();
assert!(again.iter().all(|s| !s.added));
assert_eq!(dst.doc().unwrap().doc.patterns.len(), before.patterns.len());
assert_eq!(dst.doc().unwrap().doc, before);
@@ -1162,7 +1162,7 @@ fn separate_self_referencing_patterns_are_not_added_again() {
let target = std::sync::Arc::make_mut(&mut dst.doc_mut().unwrap().doc);
target.patterns.retain(|p| !p.name.starts_with("Dots8"));
let left = target.patterns.len();
let third = super::super::clipboard::resources::import_text_styles(&source, target, true, true);
let third = super::super::clipboard::resources::import_text_styles(&source, target, true, true).unwrap();
assert_eq!(dst.doc().unwrap().doc.patterns.len(), left + 1);
assert!(third.iter().filter(|s| s.name != "Patterned8" && !s.name.starts_with("Patterned8 ")).all(|s| !s.added));
}
@@ -1195,13 +1195,13 @@ fn a_long_loop_of_patterns_is_not_added_again() {
dst.execute("object.pattern.make", &json!({"name": format!("Dots{k}"), "edit": false, "ids": [different]})).unwrap();
}
let target = std::sync::Arc::make_mut(&mut dst.doc_mut().unwrap().doc);
super::super::clipboard::resources::import_text_styles(&source, target, true, true);
super::super::clipboard::resources::import_text_styles(&source, target, true, true).unwrap();
for k in 0..9 {
assert!(dst.doc().unwrap().doc.pattern(&format!("Dots{k} 2")).is_some(), "{k}");
}
let before = dst.doc().unwrap().doc.clone();
let target = std::sync::Arc::make_mut(&mut dst.doc_mut().unwrap().doc);
let again = super::super::clipboard::resources::import_text_styles(&source, target, true, true);
let again = super::super::clipboard::resources::import_text_styles(&source, target, true, true).unwrap();
assert!(again.iter().all(|s| !s.added));
assert_eq!(dst.doc().unwrap().doc.patterns.len(), before.patterns.len());
assert_eq!(dst.doc().unwrap().doc, before);