Merge pull request #6576 from dotCooCoo/fix/pasted-svg-filter-size-limit

Effects: a pasted SVG filter at the size limit still draws under its new name
This commit is contained in:
BFlat
2026-11-11 19:00:06 +07:00
committed by GitHub
2 changed files with 41 additions and 3 deletions
+16 -3
View File
@@ -1219,7 +1219,9 @@ impl Clipboard {
/// 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
/// free name, so editing or deleting the document's filter leaves it alone.
/// free name, 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)> {
let mut found = BTreeSet::new();
for n in &self.nodes {
@@ -1244,9 +1246,20 @@ impl Clipboard {
let to = if same_doc {
(name.clone(), held.xml.clone())
} else {
let new = unique_name(&name, |n| taken.contains(n));
// Free names made from the starts of the name, longest first, then free letters and
// digits: 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').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())?;
Some((new, drawn))
});
let Some((new, drawn)) = renamed else { continue };
taken.insert(new.clone());
let drawn = vectorcraft_render::effects::svg_filter::with_id(&xml, &new).unwrap_or_else(|| xml.clone());
(new, drawn)
};
links.insert((name, xml), to);
+25
View File
@@ -259,6 +259,31 @@ fn pasted_art_keeps_a_filter_another_of_its_name_would_replace() {
assert_eq!(both, [("Shift".to_string(), SHIFT_MORE.to_string()), ("Shift".to_string(), SHIFT_MORE.to_string())]);
}
/// A pasted filter at the size limit, which "Shift 2" would make too long to draw, keeps its own
/// definition under a free name no longer than its own (a free letter for a one-letter name), and
/// its id is that name.
#[test]
fn pasted_art_with_a_filter_at_the_size_limit_still_draws_when_renamed() {
for (name, held, renamed) in [("Shift", SHIFT, "Shif"), ("S", r#"<filter id="S"><feOffset dx="3" dy="3"/></filter>"#, "a")] {
let mut s = session();
run(&mut s, "svgFilter.define", json!({"xml": held}));
let (open, close) = (format!(r#"<filter id="{name}"><feOffset dx="9" dy="9" result=""#), r#""/></filter>"#);
let big = format!("{open}{}{close}", "a".repeat(vectorcraft_doc::SvgFilter::MAX_XML - open.len() - close.len()));
assert!(vectorcraft_render::effects::svg_filter::check(&big).is_ok());
let svg = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><defs>{big}</defs><rect width="30" height="30" filter="url(#{name})"/></svg>"#
);
run(&mut s, "clipboard.importSvg", json!({"svg": svg}));
run(&mut s, "edit.pasteInPlace", json!({}));
let pasted = filter_effects(&s);
assert_eq!(pasted.len(), 1);
assert_eq!(pasted[0].0, renamed, "renamed: {}", pasted[0].0);
assert_eq!(vectorcraft_render::effects::svg_filter::check(&pasted[0].1), Ok(renamed.to_string()), "the pasted filter draws under its name");
assert!(pasted[0].1.contains("dx=\"9\""), "its own filter");
assert_eq!(names(&mut s), [name]);
}
}
/// Art in a symbol's definition that uses a filter follows its edits too.
#[test]
fn a_symbols_art_follows_a_filter_edit() {