From 8f54d1cce4e85ce4158322db493f7886750af164 Mon Sep 17 00:00:00 2001 From: aiongg Date: Tue, 13 Oct 2026 19:05:28 +0000 Subject: [PATCH] Integration: #787 refuses bitmap-only fonts for embedding, and styles name the face's numeric weight --- crates/engine/src/cmd/export.rs | 32 +++++++++++++++++++++++++++++++- crates/epub/src/lib.rs | 12 +++--------- crates/fonts/src/fontdb.rs | 7 +++++++ 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/crates/engine/src/cmd/export.rs b/crates/engine/src/cmd/export.rs index f562295dc..5fca76420 100644 --- a/crates/engine/src/cmd/export.rs +++ b/crates/engine/src/cmd/export.rs @@ -156,7 +156,7 @@ fn embedded_fonts(d: &designcraft_doc::Document, warnings: &mut Vec) -> let face = db.face(&family, &style); if !face.family.eq_ignore_ascii_case(&family) { warnings.push(format!("font `{family} {style}` is not available; not embedded")); - } else if face.restricted_licence() { + } else if face.restricted_licence() || face.bitmap_only() { warnings.push(format!("font `{family} {style}`: its licence doesn't allow embedding; not embedded")); } else if face.data().starts_with(b"ttcf") { warnings.push(format!("font `{family} {style}` is part of a font collection; not embedded")); @@ -811,6 +811,36 @@ mod text_tests { assert!(html.contains("\"Source Sans 3\", sans-serif;")); } + /// A font whose licence allows embedding bitmaps only (fsType bit 9) is not packaged whole, + /// and a Semibold style asks for the Semibold face's weight, not Bold's. + #[test] + fn epub_embedding_follows_bitmap_only_licences_and_weights() { + use designcraft_fonts::testing::{font_with, with_fs_type}; + const BITMAP: &str = "DocFont Epub Bitmap Only"; + for fs_type in [0x0200, 0x0208] { + designcraft_fonts::FontDb::global() + .add_font(with_fs_type(font_with(&format!("{BITMAP} {fs_type:04x}"), &['W']).unwrap(), fs_type).unwrap()); + } + let mut s = Session::new(); + s.execute("file.new", &json!({"pages": 1})).unwrap(); + for fs_type in [0x0200, 0x0208] { + let family = format!("{BITMAP} {fs_type:04x}"); + s.execute("style.paragraph.create", &json!({"name": family, "chars": {"fontFamily": family, "fontStyle": "Regular"}})).unwrap(); + } + s.execute("style.paragraph.create", &json!({"name": "Semi", "chars": {"fontFamily": "Source Sans 3", "fontStyle": "Semibold"}})).unwrap(); + s.execute("frame.create", &json!({"rect": [72, 72, 400, 200], "content": "text", "text": "Words", "caret": false})).unwrap(); + let r = s.execute("file.exportEpub", &json!({"embedFonts": true})).unwrap(); + let mut z = unzip(r["base64"].as_str().unwrap()); + let css = read(&mut z, "OEBPS/style.css"); + assert!(!css.contains(&format!("@font-face {{ font-family: \"{BITMAP}")), "{css}"); + let warnings = r["warnings"].to_string(); + for fs_type in [0x0200, 0x0208] { + assert!(warnings.contains(&format!("{BITMAP} {fs_type:04x} Regular`: its licence doesn't allow embedding")), "{warnings}"); + } + let semi = css.lines().find(|l| l.contains(".semi {")).unwrap_or_default(); + assert!(semi.contains("font-weight: 600;"), "{css}"); + } + #[test] fn flattener_rasterises_spreads_with_transparency() { let mut s = Session::new(); diff --git a/crates/epub/src/lib.rs b/crates/epub/src/lib.rs index 265c23ea2..c8985a7aa 100644 --- a/crates/epub/src/lib.rs +++ b/crates/epub/src/lib.rs @@ -349,15 +349,9 @@ fn hex(doc: &Document, swatch: &str, tint: f32) -> Option { fn char_css(doc: &Document, c: &CharProps, base_size: f64, generics: &BTreeMap, out: &mut String) { let _ = write!(out, "font-family: \"{}\", {}; ", css_string(&c.font_family), generic_family(&c.font_family, generics)); - let st = c.font_style.to_lowercase(); - if st.contains("bold") { - out.push_str("font-weight: bold; "); - } else if st.contains("semibold") { - out.push_str("font-weight: 600; "); - } - if st.contains("italic") { - out.push_str("font-style: italic; "); - } + // The face's own weight, so an embedded Semibold or Light face is the one chosen. + let (weight, style) = css_weight_style(&c.font_style); + let _ = write!(out, "font-weight: {weight}; font-style: {style}; "); let _ = write!(out, "font-size: {:.3}em; ", c.size / base_size); if c.tracking != 0.0 { let _ = write!(out, "letter-spacing: {:.3}em; ", c.tracking / 1000.0); diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index 453605283..4aee90692 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -358,6 +358,13 @@ impl FontFace { let fs_type = self.skrifa().and_then(|f| f.os2().ok()).map_or(0, |t| t.fs_type()); fs_type & 0x0002 != 0 && fs_type & 0x000C == 0 } + + /// Does the font's licence allow embedding bitmaps only? OS/2 `fsType` bit 9: the outlines + /// may not be passed on, so the font file can't be packaged whole. + pub fn bitmap_only(&self) -> bool { + use skrifa::raw::TableProvider; + self.skrifa().and_then(|f| f.os2().ok()).is_some_and(|t| t.fs_type() & 0x0200 != 0) + } } #[cfg(not(target_arch = "wasm32"))]