mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 07:16:52 +00:00
fix(svg): glyphs keep only typed raster data URLs and no links
This commit is contained in:
committed by
bflatastic
parent
c972c15d0f
commit
0086c85bc7
+19
-6
@@ -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())?;
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user