collective

Browse / github.com/storytold/craft-libs / patch

krilla: a glyph is an SVG glyph only when the font has a document for it

krilla: a glyph is an SVG glyph only when the font has a document for it ## What and why Krilla's `has_svg_data` (`vendor/krilla/src/text/glyph/svg.rs`) asked only whether the font has an `SVG ` table: `svg().map(|t| t.glyph_data(g).is_ok()).is_ok()` is true for any glyph once the table exists, and `glyph_data` is `Ok(None)` for a glyph without a document anyway. So every glyph of a font with SVG glyphs went into a Type 3 font instead of the embedded font. A glyph now counts as an SVG glyph only when the table has a document for it. Recorded in `README.craft.md` and `SOURCE.json`. ## How it was tested - `crates/pdf/tests/svg.rs` builds a two-glyph font whose `S

Status
open
Score
0.00
Adopters
0 other operators
Author
lehni (imported from pull request #250, open)
Base
1d041f5c030aded6ad12e5b1636abdda20405bc8
Size
+101 −5 in 8 files
Review
pending
Shared
2026-10-28
Id
b6697c0217b538953d511eb6

How it went

No other operator has reported using it yet.

Diff

raw · Cargo.lock NOTICE crates/pdf/Cargo.toml crates/pdf/README.md crates/pdf/tests/svg.rs vendor/krilla/README.craft.md vendor/krilla/SOURCE.json vendor/krilla/src/text/glyph/svg.rs

diff --git a/Cargo.lock b/Cargo.lock--- a/Cargo.lock+++ b/Cargo.lock@@ -818,6 +818,7 @@ dependencies = [  "thiserror 2.0.21",  "ttf-parser",  "wasm-bindgen-test",+ "write-fonts", ]  [[package]]diff --git a/NOTICE b/NOTICE--- a/NOTICE+++ b/NOTICE@@ -91,7 +91,7 @@ The optional writer embeds Compact ICC Profiles by Clinton Ingram See licenses/pdf-writer/LICENSE-ICC.txt and ATTRIBUTION.md for sources and hashes. The writer adds no font files. Its four existing ICC profiles are retained under vendor/krilla/icc. -Krilla is vendored with a native unwind-cleanup patch; see vendor/krilla/README.craft.md+Krilla is vendored with a native unwind-cleanup patch and an SVG glyph check fix; see vendor/krilla/README.craft.md and vendor/krilla/SOURCE.json. Its upstream MIT/Apache-2.0 licences and CC0 profiles are unchanged.  Optional PDF readerdiff --git a/crates/pdf/Cargo.toml b/crates/pdf/Cargo.toml--- a/crates/pdf/Cargo.toml+++ b/crates/pdf/Cargo.toml@@ -33,6 +33,7 @@ hayro-interpret = { version = "=0.7.0", default-features = false } hayro-cmap = "=0.1.0" kurbo = "0.13" ttf-parser = "=0.25.1"+write-fonts = "=0.48.1"  [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3"diff --git a/crates/pdf/README.md b/crates/pdf/README.md--- a/crates/pdf/README.md+++ b/crates/pdf/README.md@@ -103,7 +103,7 @@ that supply shaped glyphs do not need that shaper. Adapters that call `draw_text DeckCraft, also need `write-simple-text` when adopting this revision. `write-pdf` adds embedded-PDF support independently of text shaping. -The writer pins Krilla 0.8.2, with a local unwind-cleanup patch documented in+The writer pins Krilla 0.8.2, with a local unwind-cleanup patch and an SVG glyph check fix documented in [`vendor/krilla/README.craft.md`](../../vendor/krilla/README.craft.md). Successful serialization uses the same backend version as before extraction. It provides: diff --git a/crates/pdf/tests/svg.rs b/crates/pdf/tests/svg.rsnew file mode 100644--- /dev/null+++ b/crates/pdf/tests/svg.rs@@ -0,0 +1,91 @@+//! OpenType SVG glyphs, drawn from a minimal synthetic font built in the test.+#![cfg(feature = "write")]+#![allow(clippy::unwrap_used, clippy::panic)]++use backend::geom::Point;+use backend::page::PageSettings;+use backend::text::{GlyphId, KrillaGlyph};+use craft_pdf::write::{self, Metadata, WriteError, Writer, backend};+use kurbo::{Rect, Shape};+use write_fonts::FontBuilder;+use write_fonts::tables::glyf::{GlyfLocaBuilder, Glyph, SimpleGlyph};+use write_fonts::tables::head::{Flags, Head, MacStyle};+use write_fonts::tables::hhea::Hhea;+use write_fonts::tables::hmtx::{Hmtx, LongMetric};+use write_fonts::tables::loca::LocaFormat;+use write_fonts::tables::maxp::Maxp;+use write_fonts::types::{FWord, Fixed, LongDateTime, Tag, UfWord};++/// The glyph the font's `SVG ` table has a document for.+const SVG: u16 = 1;+/// A glyph with an outline only.+const PLAIN: u16 = 2;++/// A font of two squares whose `SVG ` table holds a document for [`SVG`] alone.+fn svg_font() -> Vec<u8> {+    let square = Glyph::Simple(SimpleGlyph::from_bezpath(&Rect::new(0.0, 0.0, 500.0, 700.0).to_path(0.1)).unwrap());+    let glyphs = [Glyph::Empty, square.clone(), square];+    let mut glyf_loca = GlyfLocaBuilder::new();+    for glyph in &glyphs {+        glyf_loca.add_glyph(glyph).unwrap();+    }+    let (glyf, loca, loca_format) = glyf_loca.build();+    let index_to_loc_format = if loca_format == LocaFormat::Long { 1 } else { 0 };+    let date = LongDateTime::new(0);+    let head = Head::new(Fixed::ONE, 0, Flags::empty(), 1000, date, date, 0, 0, 500, 700, MacStyle::empty(), 8, index_to_loc_format);+    let num_glyphs = glyphs.len() as u16;+    let hhea = Hhea {+        ascender: FWord::new(800),+        descender: FWord::new(-200),+        advance_width_max: UfWord::new(600),+        number_of_h_metrics: num_glyphs,+        ..Default::default()+    };+    let hmtx = Hmtx::new(glyphs.iter().map(|_| LongMetric::new(600, 0)).collect(), vec![]);++    // The `SVG ` table: its header (version, the document list's offset, reserved), then the+    // document list with one record (first and last glyph, the document's offset from the list+    // and length) and the document.+    let doc = format!(r#"<svg xmlns="http://www.w3.org/2000/svg"><rect id="glyph{SVG}" width="500" height="700"/></svg>"#);+    let mut svg = [0u16.to_be_bytes().as_slice(), &10u32.to_be_bytes(), &0u32.to_be_bytes()].concat();+    svg.extend(1u16.to_be_bytes());+    svg.extend([SVG.to_be_bytes(), SVG.to_be_bytes()].concat());+    svg.extend([14u32.to_be_bytes(), (doc.len() as u32).to_be_bytes()].concat());+    svg.extend(doc.as_bytes());++    let mut builder = FontBuilder::new();+    builder.add_table(&head).unwrap();+    builder.add_table(&hhea).unwrap();+    builder.add_table(&Maxp::new(num_glyphs)).unwrap();+    builder.add_table(&hmtx).unwrap();+    builder.add_table(&glyf).unwrap();+    builder.add_table(&loca).unwrap();+    builder.add_raw(Tag::new(b"SVG "), svg);+    builder.build()+}++/// Export one glyph of the font, uncompressed, behind the writer's unwind guard.+fn export(glyph: u16) -> Result<String, WriteError> {+    write::guard(|| {+        let font = write::font(&svg_font(), 0, None)?;+        let settings = backend::SerializeSettings { compress_content_streams: false, ..Default::default() };+        let mut writer = Writer::new(settings, Metadata::default());+        let mut page = writer.document_mut().start_page_with(PageSettings::from_wh(100.0, 100.0).unwrap());+        let mut surface = page.surface();+        let glyphs = [KrillaGlyph::new(GlyphId::new(u32::from(glyph)), 0.6, 0.0, 0.0, 0.0, 0..1, None)];+        surface.draw_glyphs(Point::from_xy(10.0, 80.0), &glyphs, font, "x", 50.0, false);+        surface.finish();+        page.finish();+        Ok(String::from_utf8_lossy(&writer.finish()?).into_owned())+    })+}++#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]+#[cfg_attr(not(target_arch = "wasm32"), test)]+fn only_glyphs_with_an_svg_document_go_into_a_type3_font() {+    let pdf = export(SVG).unwrap();+    assert!(pdf.contains("/Type3"), "the SVG glyph is a Type 3 glyph");+    let pdf = export(PLAIN).unwrap();+    assert!(!pdf.contains("/Type3"), "a glyph without a document stays in the font");+    assert!(pdf.contains("/CIDFontType2"), "as a TrueType glyph");+}diff --git a/vendor/krilla/README.craft.md b/vendor/krilla/README.craft.md--- a/vendor/krilla/README.craft.md+++ b/vendor/krilla/README.craft.md@@ -32,3 +32,7 @@ A PNG whose header implies more than 1 GiB of decoded output is refused by fallibly, so a small file (or a bitmap glyph in a font) cannot request an allocation that ends the process. The colour and alpha buffers that PNG, GIF and WebP decoding split an image into are reserved fallibly too, and a failure there is an image error.++A glyph counts as an SVG glyph only when the font's SVG table has a document for it.+Upstream's check asked only whether the font has the table, so every glyph of such a font+went into a Type 3 font instead of the embedded font.diff --git a/vendor/krilla/SOURCE.json b/vendor/krilla/SOURCE.json--- a/vendor/krilla/SOURCE.json+++ b/vendor/krilla/SOURCE.json@@ -352,7 +352,7 @@     {       "path": "src/text/glyph/svg.rs",       "upstream_sha256": "67d0bc7e67db7685de44810fb4a0b6eca73b45a2d0a1b3aed88630bab3787894",-      "sha256": "67d0bc7e67db7685de44810fb4a0b6eca73b45a2d0a1b3aed88630bab3787894"+      "sha256": "d1a712bb36500c04393c428d73532bc79dc784a9aea9ec1729f9a5ab920edbe0"     },     {       "path": "src/text/group.rs",diff --git a/vendor/krilla/src/text/glyph/svg.rs b/vendor/krilla/src/text/glyph/svg.rs--- a/vendor/krilla/src/text/glyph/svg.rs+++ b/vendor/krilla/src/text/glyph/svg.rs@@ -8,8 +8,7 @@ use crate::text::GlyphId; pub(crate) fn has_svg_data(font: &Font, glyph: GlyphId) -> bool {     font.font_ref()         .svg()-        .map(|svg_table| svg_table.glyph_data(glyph.to_skrifa()).is_ok())-        .is_ok()+        .is_ok_and(|svg_table| svg_table.glyph_data(glyph.to_skrifa()).is_ok_and(|data| data.is_some())) }  /// Draw an SVG-based glyph on a surface.