mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 17:44:02 +00:00
Merge pull request #6592 from dotCooCoo/fix/pasted-svg-filter-free-name
Effects: a pasted SVG filter is not renamed to a name the document's effects use
This commit is contained in:
@@ -1101,8 +1101,9 @@ pub fn with_id(xml: &str, id: &str) -> Option<String> {
|
||||
let f = filter_element(&doc).ok()?;
|
||||
let at = f.attributes().find(|a| a.name() == "id" && a.namespace().is_none())?.range_value();
|
||||
let (start, end) = (at.start.checked_sub(head)?, at.end.checked_sub(head)?);
|
||||
// The value may sit in single quotes: those are escaped too.
|
||||
let id = escaped(id).replace('\'', "'");
|
||||
// The value may sit in single quotes: those are escaped too. A tab, line feed or carriage
|
||||
// return is written as a character reference, which attribute-value normalization keeps.
|
||||
let id = escaped(id).replace('\'', "'").replace('\t', "	").replace('\n', " ").replace('\r', " ");
|
||||
Some(format!("{}{}{}", xml.get(..start)?, id, xml.get(end..)?))
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,23 @@ fn node_filters(n: &Node, out: &mut BTreeSet<(String, String)>) {
|
||||
});
|
||||
}
|
||||
|
||||
/// 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, .. })
|
||||
@@ -1219,7 +1236,7 @@ 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. When that name would
|
||||
/// 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)> {
|
||||
@@ -1240,12 +1257,25 @@ impl Clipboard {
|
||||
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 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: the first under which the filter still draws.
|
||||
let mut ends: Vec<usize> = name.char_indices().map(|(i, _)| i).skip(1).collect();
|
||||
@@ -1255,7 +1285,7 @@ impl Clipboard {
|
||||
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())?;
|
||||
.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 };
|
||||
|
||||
@@ -284,6 +284,44 @@ fn pasted_art_with_a_filter_at_the_size_limit_still_draws_when_renamed() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A pasted filter is not renamed to a name an effect in the document already gives, even one whose
|
||||
/// filter was deleted, so defining a filter of that name later changes only that effect.
|
||||
#[test]
|
||||
fn pasted_art_takes_no_name_the_documents_effects_use() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 10.0);
|
||||
run(&mut s, "svgFilter.define", json!({"xml": r#"<filter id="Shift 2"><feOffset dx="1" dy="1"/></filter>"#}));
|
||||
run(&mut s, "svgFilter.apply", json!({"name": "Shift 2", "ids": [a.0]}));
|
||||
run(&mut s, "svgFilter.delete", json!({"name": "Shift 2"}));
|
||||
run(&mut s, "svgFilter.define", json!({"xml": SHIFT}));
|
||||
let svg = format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><defs>{SHIFT_MORE}</defs><rect width="30" height="30" filter="url(#Shift)"/></svg>"#
|
||||
);
|
||||
run(&mut s, "clipboard.importSvg", json!({"svg": svg}));
|
||||
run(&mut s, "edit.pasteInPlace", json!({}));
|
||||
let names: Vec<String> = filter_effects(&s).into_iter().map(|(n, _)| n).collect();
|
||||
assert_eq!(names, ["Shift 2", "Shift 3"]);
|
||||
}
|
||||
|
||||
/// A pasted filter whose name holds a tab, a line feed or a carriage return keeps those characters
|
||||
/// in its id under its new name.
|
||||
#[test]
|
||||
fn a_renamed_paste_keeps_whitespace_characters_in_its_id() {
|
||||
for c in ["	", " ", " "] {
|
||||
let mut s = session();
|
||||
run(&mut s, "svgFilter.define", json!({"xml": format!(r#"<filter id="S{c}t"><feOffset dx="3" dy="3"/></filter>"#)}));
|
||||
let svg = format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><defs><filter id="S{c}t"><feOffset dx="9" dy="9"/></filter></defs><rect width="30" height="30" filter="url(#S{c}t)"/></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_ne!(pasted[0].0, names(&mut s)[0], "renamed");
|
||||
assert_eq!(vectorcraft_render::effects::svg_filter::check(&pasted[0].1), Ok(pasted[0].0.clone()), "{c}: its id is its name");
|
||||
}
|
||||
}
|
||||
|
||||
/// Art in a symbol's definition that uses a filter follows its edits too.
|
||||
#[test]
|
||||
fn a_symbols_art_follows_a_filter_edit() {
|
||||
|
||||
Reference in New Issue
Block a user