fix(svg): glyphs keep only typed raster data URLs and no links

This commit is contained in:
Dylan M. Taylor
2026-11-03 23:29:09 +07:00
committed by bflatastic
parent c972c15d0f
commit 0086c85bc7
2 changed files with 38 additions and 9 deletions
+19 -6
View File
@@ -99,10 +99,18 @@ fn glyph(svg: &str, id: &str, color: &str) -> Option<Glyph> {
// Edits: byte ranges to cut and what to put in their place, in order.
let mut edits: Vec<(Range<usize>, String)> = vec![];
// Cut with the left-out elements: images of SVG documents, as the table asks (a glyph draws no
// documents inside it). Their link is `href`, else `xlink:href`, as the importer reads it.
// documents inside it). Their link is `href`, else `xlink:href`, as the importer reads it. Any
// data URL not typed as a raster goes too: the importer reads an untyped one (`text/plain`) as
// SVG, SVGZ as well, when its bytes aren't a raster.
let svg_image = |n: &roxmltree::Node| {
let href = n.attribute("href").or_else(|| n.attribute(("http://www.w3.org/1999/xlink", "href")));
n.tag_name().name() == "image" && href.and_then(|h| h.trim_start().get(..14)).is_some_and(|v| v.eq_ignore_ascii_case("data:image/svg"))
let data = href.map(|h| h.trim_start()).filter(|h| h.get(..5).is_some_and(|v| v.eq_ignore_ascii_case("data:")));
let raster = |h: &str| {
["data:image/png", "data:image/jpeg", "data:image/jpg", "data:image/gif", "data:image/webp"]
.iter()
.any(|t| h.get(..t.len()).is_some_and(|v| v.eq_ignore_ascii_case(t)) && h[t.len()..].starts_with([';', ',']))
};
n.tag_name().name() == "image" && data.is_some_and(|h| !raster(h))
};
for n in root.descendants().filter(|n| n.is_element() && (LEFT_OUT.contains(&n.tag_name().name()) || svg_image(n))) {
edits.push((n.range(), String::new()));
@@ -172,13 +180,18 @@ fn edited(svg: &str, range: Range<usize>, edits: &[(Range<usize>, String)]) -> O
Some(out)
}
/// `n` as art of its own, made from `doc` (the glyph's document): no ids (they aren't a document's),
/// its members' neither, and its images holding the pixels `doc` keeps for them, named by their
/// content. A linked image, or one whose pixels `doc` doesn't hold, is left out (`None` when `n`
/// is one).
/// `n` as art of its own, made from `doc` (the glyph's document): no ids (they aren't a document's)
/// or links, its members' neither, and its images holding the pixels `doc` keeps for them, named
/// by their content. A linked image, or one whose pixels `doc` doesn't hold, is left out (`None`
/// when `n` is one).
fn own_art(n: &Node, doc: &Document) -> Option<Node> {
let mut n = n.clone();
n.id = NodeId(u64::MAX);
// A glyph is drawn, not followed: an `<a>`'s link (a `javascript:` one too) would be written
// as a link again when its text is exported as outlines.
if n.url().is_some() {
n.edit_attrs(|a| a.url.clear());
}
if let NodeKind::Image(im) = &mut n.kind {
// A glyph's images are embedded: a link (to no file of the font's) shows nothing.
let blob = doc.images.get(&im.key).filter(|b| im.link.is_none() && !b.bytes.is_empty())?;
+19 -3
View File
@@ -210,8 +210,8 @@ fn images_are_in_the_art_with_their_pixels() {
assert!(b.x0.abs() < 1e-6 && (b.width() - 500.0).abs() < 1e-6 && (b.y0 + 500.0).abs() < 1e-6, "in font units: {b:?}");
}
/// An image whose data is an SVG document is left out, as the `SVG ` table asks: a glyph draws no
/// documents inside it.
/// An image whose data is an SVG document, or a data URL not typed as a raster, is left out, as
/// the `SVG ` table asks: a glyph draws no documents inside it.
#[test]
fn svg_images_are_left_out() {
let inner = r##"<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect width="10" height="10" fill="#f00"/></svg>"##;
@@ -220,9 +220,13 @@ fn svg_images_are_left_out() {
<path fill="#00f" d="M0 0h500v-500z"/>
<image width="500" height="500" xlink:href="data:image/svg+xml;base64,{}"/>
<image width="500" height="500" href=" DATA:image/SVG+xml;utf8,{}"/>
<image width="500" height="500" href="data:text/plain;base64,{}"/>
<image width="500" height="500" href="data:;base64,{}"/>
</g></svg>"##,
vectorcraft_svg::base64_encode(inner.as_bytes()),
vectorcraft_svg::xml_escape(&inner.replace('#', "%23"))
vectorcraft_svg::xml_escape(&inner.replace('#', "%23")),
vectorcraft_svg::base64_encode(inner.as_bytes()),
vectorcraft_svg::base64_encode(inner.as_bytes())
);
let fills = fills(&art(&doc, 1).unwrap());
assert!(fills.contains(&Paint::solid(Color::rgb8(0, 0, 255))), "the glyph's own art");
@@ -255,3 +259,15 @@ fn an_images_plain_href_decides_whether_it_is_an_svg_image() {
assert_eq!(images(image("png", "svg+xml")), 1, "the PNG stays");
assert_eq!(images(image("svg+xml", "png")), 0, "the document goes");
}
/// A glyph's `<a>` draws its art but keeps no link: outlined text must not export a
/// `javascript:` link from a font.
#[test]
fn links_in_a_glyph_are_left_out() {
let doc = r##"<svg xmlns="http://www.w3.org/2000/svg"><g id="glyph1"><a href="javascript:alert(1)"><path fill="#00f" d="M0 0h500v-500z"/></a></g></svg>"##;
let art = art(doc, 1).unwrap();
assert!(fills(&art).contains(&Paint::solid(Color::rgb8(0, 0, 255))));
let mut urls = vec![];
art.walk(&mut |n| urls.extend(n.url().map(str::to_string)));
assert!(urls.is_empty(), "{urls:?}");
}