diff --git a/crates/svg/src/glyph.rs b/crates/svg/src/glyph.rs index 9c0ac5d5a..c019f9b0a 100644 --- a/crates/svg/src/glyph.rs +++ b/crates/svg/src/glyph.rs @@ -99,10 +99,18 @@ fn glyph(svg: &str, id: &str, color: &str) -> Option { // Edits: byte ranges to cut and what to put in their place, in order. let mut edits: Vec<(Range, 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, edits: &[(Range, 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 { let mut n = n.clone(); n.id = NodeId(u64::MAX); + // A glyph is drawn, not followed: an ``'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())?; diff --git a/crates/svg/tests/it/svg_glyphs.rs b/crates/svg/tests/it/svg_glyphs.rs index 1bf175367..20e534241 100644 --- a/crates/svg/tests/it/svg_glyphs.rs +++ b/crates/svg/tests/it/svg_glyphs.rs @@ -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##""##; @@ -220,9 +220,13 @@ fn svg_images_are_left_out() { + + "##, 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 `` 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##""##; + 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:?}"); +}