Browse / github.com/storytold/pdfcraft / patch
M7: Thai document font from craft-fonts' Thai manifest (Sarabun)
M7: Thai document font from craft-fonts' Thai manifest (Sarabun) ## What this changes Refs #1384 (its first item: an open-licence document font for Thai, Sarabun). It's the PdfCraft side of storytold/craft-fonts#53, which adds Sarabun (OFL-1.1) to craft-fonts in a separate, opt-in `fonts/thai-manifest.txt`. Thai text written into a PDF by **Add Text** (and `page_add_text`) uses **Sarabun Regular**, a looped Thai face suited to documents (the Thai document-style face), when the craft-fonts checkout has that manifest. Otherwise it falls back to **Noto Sans Thai**, the loopless interface face, which is today's behaviour. The interface keeps Noto Sans Thai.
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- bomu-bomu (imported from pull request #4921, open)
- Base
1dd2737b47a9c1c9562f0af563053d1bf510e4bf- Size
- +187 −16 in 11 files
- Review
- pending
- Shared
- 2026-10-28
- Id
87dd55e6d9f88071f81877a7
How it went
No other operator has reported using it yet.
Diff
raw · ATTRIBUTION.md ATTRIBUTION.toml ROADMAP.md contributors/people.toml crates/edit/src/tests.rs crates/fonts/README.md crates/fonts/build.rs crates/fonts/src/craft.rs crates/ui-egui/tests/it/content.rs docs/roadmap.md parity/acrobat-features.toml
@@ -344,4 +344,4 @@ Not in this repository and never downloaded by it: compiled in only when the bui | Input | Option | Title | Author | Licence | Source | Attribution | Used for | |---|---|---|---|---|---|---|---|-| `craft-fonts` | `CRAFT_FONTS_DIR` | Japanese fonts: BIZ UDPGothic (Regular, Bold), Shippori Mincho, BIZ UDMincho; native Simplified Chinese font: LXGW WenKai Lite; Arabic font: Noto Sans Arabic | The BIZ UDGothic and BIZ UDMincho Project Authors (Morisawa Inc.), The Shippori Mincho Project Authors (FONTDASU), LXGW and the original Klee Project Authors / Fontworks (LXGW WenKai Lite), The Noto Project Authors (Noto Sans Arabic) | OFL-1.1 | https://github.com/storytold/craft-fonts (pinned by commit in .github/workflows/release.yml and .github/workflows/ci.yml); LXGW WenKai Lite from https://github.com/lxgw/LxgwWenKai-Lite/releases/tag/v1.530 (commit b712c83d98acf977bacfb7fb010207802808bda9) | https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | Japanese fallback in the interface (BIZ UDPGothic first) and Japanese glyphs in edited PDF text (Shippori Mincho); Simplified Chinese interface fallback on native builds only (LXGW WenKai Lite Regular, font SHA-256 f799a538a3355877e597a0ae04050e897932c9ae7689eb9227c2953c5dd549ea, OFL SHA-256 0333a85fe077b6849f8e417acc44e8bd879442ceb681e271acf93e2d57fd4e2e, read from fonts/simplified-chinese-manifest.txt and never embedded on wasm); Arabic interface text, file names and document titles (Noto Sans Arabic); the web build embeds BIZ UDPGothic Regular and the pinned small Arabic, Hebrew, Thai and Devanagari faces; the selected craft-fonts revision contains no Telugu face. Other non-Japanese CJK faces retain the general 4 MiB cap; the reviewed native-only LXGW fallback has a separate 16 MiB cap. Open-source OFL releases are permitted under current AGENTS.md §1.1 |+| `craft-fonts` | `CRAFT_FONTS_DIR` | Japanese fonts: BIZ UDPGothic (Regular, Bold), Shippori Mincho, BIZ UDMincho; native Simplified Chinese font: LXGW WenKai Lite; Arabic font: Noto Sans Arabic; native Thai document font: Sarabun | The BIZ UDGothic and BIZ UDMincho Project Authors (Morisawa Inc.), The Shippori Mincho Project Authors (FONTDASU), LXGW and the original Klee Project Authors / Fontworks (LXGW WenKai Lite), The Noto Project Authors (Noto Sans Arabic), The Sarabun Project Authors (Cadson Demak; Sarabun) | OFL-1.1 | https://github.com/storytold/craft-fonts (pinned by commit in .github/workflows/release.yml and .github/workflows/ci.yml); LXGW WenKai Lite from https://github.com/lxgw/LxgwWenKai-Lite/releases/tag/v1.530 (commit b712c83d98acf977bacfb7fb010207802808bda9) | https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | Japanese fallback in the interface (BIZ UDPGothic first) and Japanese glyphs in edited PDF text (Shippori Mincho); Simplified Chinese interface fallback on native builds only (LXGW WenKai Lite Regular, font SHA-256 f799a538a3355877e597a0ae04050e897932c9ae7689eb9227c2953c5dd549ea, OFL SHA-256 0333a85fe077b6849f8e417acc44e8bd879442ceb681e271acf93e2d57fd4e2e, read from fonts/simplified-chinese-manifest.txt and never embedded on wasm); Arabic interface text, file names and document titles (Noto Sans Arabic); Thai text written into PDFs on native builds only (Sarabun Regular, read from fonts/thai-manifest.txt when the checkout has it, never embedded on wasm and never an interface font; otherwise the Noto Sans Thai interface face); the web build embeds BIZ UDPGothic Regular and the pinned small Arabic, Hebrew, Thai and Devanagari faces; the selected craft-fonts revision contains no Telugu face. Other non-Japanese CJK faces retain the general 4 MiB cap; the reviewed native-only LXGW fallback has a separate 16 MiB cap. Open-source OFL releases are permitted under current AGENTS.md §1.1 |@@ -3384,13 +3384,13 @@ sha256 = "e484866d4cce403175bd8d00b128feb08ab42e208de30e42cd9889d8f1735a6e" [[build_input]] name = "craft-fonts" option = "CRAFT_FONTS_DIR"-title = "Japanese fonts: BIZ UDPGothic (Regular, Bold), Shippori Mincho, BIZ UDMincho; native Simplified Chinese font: LXGW WenKai Lite; Arabic font: Noto Sans Arabic"-author = "The BIZ UDGothic and BIZ UDMincho Project Authors (Morisawa Inc.), The Shippori Mincho Project Authors (FONTDASU), LXGW and the original Klee Project Authors / Fontworks (LXGW WenKai Lite), The Noto Project Authors (Noto Sans Arabic)"+title = "Japanese fonts: BIZ UDPGothic (Regular, Bold), Shippori Mincho, BIZ UDMincho; native Simplified Chinese font: LXGW WenKai Lite; Arabic font: Noto Sans Arabic; native Thai document font: Sarabun"+author = "The BIZ UDGothic and BIZ UDMincho Project Authors (Morisawa Inc.), The Shippori Mincho Project Authors (FONTDASU), LXGW and the original Klee Project Authors / Fontworks (LXGW WenKai Lite), The Noto Project Authors (Noto Sans Arabic), The Sarabun Project Authors (Cadson Demak; Sarabun)" source = "https://github.com/storytold/craft-fonts (pinned by commit in .github/workflows/release.yml and .github/workflows/ci.yml); LXGW WenKai Lite from https://github.com/lxgw/LxgwWenKai-Lite/releases/tag/v1.530 (commit b712c83d98acf977bacfb7fb010207802808bda9)" licence = "OFL-1.1" attribution = "https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md" kind = "font"-usage = "Japanese fallback in the interface (BIZ UDPGothic first) and Japanese glyphs in edited PDF text (Shippori Mincho); Simplified Chinese interface fallback on native builds only (LXGW WenKai Lite Regular, font SHA-256 f799a538a3355877e597a0ae04050e897932c9ae7689eb9227c2953c5dd549ea, OFL SHA-256 0333a85fe077b6849f8e417acc44e8bd879442ceb681e271acf93e2d57fd4e2e, read from fonts/simplified-chinese-manifest.txt and never embedded on wasm); Arabic interface text, file names and document titles (Noto Sans Arabic); the web build embeds BIZ UDPGothic Regular and the pinned small Arabic, Hebrew, Thai and Devanagari faces; the selected craft-fonts revision contains no Telugu face. Other non-Japanese CJK faces retain the general 4 MiB cap; the reviewed native-only LXGW fallback has a separate 16 MiB cap. Open-source OFL releases are permitted under current AGENTS.md §1.1"+usage = "Japanese fallback in the interface (BIZ UDPGothic first) and Japanese glyphs in edited PDF text (Shippori Mincho); Simplified Chinese interface fallback on native builds only (LXGW WenKai Lite Regular, font SHA-256 f799a538a3355877e597a0ae04050e897932c9ae7689eb9227c2953c5dd549ea, OFL SHA-256 0333a85fe077b6849f8e417acc44e8bd879442ceb681e271acf93e2d57fd4e2e, read from fonts/simplified-chinese-manifest.txt and never embedded on wasm); Arabic interface text, file names and document titles (Noto Sans Arabic); Thai text written into PDFs on native builds only (Sarabun Regular, read from fonts/thai-manifest.txt when the checkout has it, never embedded on wasm and never an interface font; otherwise the Noto Sans Thai interface face); the web build embeds BIZ UDPGothic Regular and the pinned small Arabic, Hebrew, Thai and Devanagari faces; the selected craft-fonts revision contains no Telugu face. Other non-Japanese CJK faces retain the general 4 MiB cap; the reviewed native-only LXGW fallback has a separate 16 MiB cap. Open-source OFL releases are permitted under current AGENTS.md §1.1" [[asset]] path = "crates/sign/tests/data/x509-rsa-sha1.pdf"@@ -199,6 +199,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md ## Progress log +- **2026-10-27 (M7, refs #1384):** Thai text written by Add Text uses the Regular face of craft-fonts' opt-in Thai document manifest (`fonts/thai-manifest.txt`, Sarabun, looped Thai) on native builds whose checkout has it, and the Noto Sans Thai interface face otherwise, as before. The document faces are kept apart from the interface fonts, the manifest stays optional under `CRAFT_FONTS_REQUIRED`, and the web build is unchanged. The pinned craft-fonts revision has no such manifest yet, so release builds are unchanged until it is bumped. Estimates unchanged. - **2026-10-24 (M10, #4158 partial):** Word, HTML, RTF and PowerPoint export keep a block that continues another on its line (an italic case name, a link, a superscript) as a run in its own style inside that paragraph, with the source's word space, instead of a paragraph of its own. Style changes that wrap onto the next line still split paragraphs (the reading blocks keep style boundaries). Estimates unchanged. - **2026-10-24 (M3, window move on Wayland, #3970 follow-up):** After a move or resize by PdfCraft's own title bar or edges, the button release given back to egui goes before the pointer leaving the window, so hover no longer sticks at the old place until the pointer moves. Found by CodeRabbit in review of #3970; a unit test covers it. Estimates unchanged. - **2026-10-24 (M14, #4253):** The annotation quick bar remembers Keep tool selected across restarts, including unpinning. Old and malformed settings keep safe defaults; the opt-in UI control channel reads and changes the same preference. Estimates unchanged.@@ -20,6 +20,11 @@ [people.YouseifUS] sync_github_name = true +[people.bomu-bomu]+real_name = "Teerayut Hiruntaraporn"+display_name = "Teerayut Hiruntaraporn"+sync_github_name = false+ [people.brian-moon] real_name = "Il Min Brian Moon" display_name = "Il Min Brian Moon"@@ -1207,6 +1207,38 @@ fn arabic_font_names(doc: &Document, page: usize) -> Vec<String> { fonts.as_dict().unwrap().iter().map(|(k, _)| String::from_utf8_lossy(k).into_owned()).filter(|k| k.starts_with("PCAr")).collect() } +/// The `/FontDescriptor /FontFamily` of each shaped-text (Type 3) font on `page`.+fn shaped_font_families(doc: &Document, page: usize) -> Vec<String> {+ let p = &pdfcraft_model::pages(doc)[page];+ let res = doc.resolve(p.dict.get(b"Resources").unwrap());+ let fonts = doc.resolve(res.as_dict().unwrap().get(b"Font").unwrap());+ let fonts = fonts.as_dict().unwrap();+ arabic_font_names(doc, page)+ .iter()+ .map(|name| {+ let font = doc.resolve(fonts.get(name.as_bytes()).unwrap());+ let descriptor = doc.resolve(font.as_dict().unwrap().get(b"FontDescriptor").unwrap());+ descriptor.as_dict().unwrap().get(b"FontFamily").unwrap().as_string().unwrap().to_text()+ })+ .collect()+}++/// Thai Add Text is drawn from the document Thai face, whichever the build selected: Sarabun when+/// craft-fonts' Thai document manifest is embedded, else the interface face (Noto Sans Thai), as+/// before. Which face is selected is pinned in the fonts crate+/// (`craft::tests::document_manifest_faces_are_never_interface_fallbacks`), which can see the+/// embedded document faces; a manifest file on disk may have been skipped.+#[test]+fn added_thai_text_is_drawn_from_the_document_thai_face() {+ let Some(face) = pdfcraft_fonts::document_thai_font() else { return };+ let mut doc = fixture();+ let t = AddedText { rect: [72.0, 600.0, 400.0, 700.0], text: "สวัสดี น้ำ".into(), size: 14.0, ..AddedText::default() };+ add_content(&mut doc, 0, &Content::Text(t)).unwrap();+ let families = shaped_font_families(&doc, 0);+ assert!(!families.is_empty());+ assert!(families.iter().all(|f| f == face.family), "{families:?}, expected {}", face.family);+}+ #[test] fn arabic_items_keep_their_own_fonts_through_saves_updates_and_deletes() { let mut doc = fixture();@@ -55,10 +55,17 @@ set). Without that variable `CRAFT_FONTS` is empty and everything below copes: - `document_japanese_fonts_for_style`: every `Jpan` face in that order of preference, so the editor can use the first one that has all of the replacement's glyphs (the faces differ in coverage, e.g. of Cyrillic).-- `ui_thai_fonts` / `document_thai_font` / `shape_thai`: the `Thai` faces for the interface- and shaped Thai Add Text content. Harfrust positions stacked vowel/tone marks and decomposes- Sara Am; cluster ToUnicode entries preserve the original text. The shared Type 3 writer- embeds outlines, so reopening does not depend on installed fonts.+- `ui_thai_fonts`: the `Thai` faces of the main manifest (Noto Sans Thai, loopless) for the+ interface.+- `document_thai_font` / `shape_thai`: the face for Thai text written into PDFs and its shaper.+ It is the Regular face of craft-fonts' opt-in Thai document manifest (`fonts/thai-manifest.txt`,+ Sarabun, looped Thai) when the checkout has one, else the interface face, as before that+ manifest existed. Harfrust positions stacked vowel/tone marks and decomposes Sara Am; cluster+ ToUnicode entries preserve the original text. The shared Type 3 writer embeds outlines, so+ reopening does not depend on installed fonts. Only the Regular face is used so far.+ The faces of that document manifest (native builds only) live in an internal+ `CRAFT_DOCUMENT_FONTS`. They are never interface fallbacks, so they are kept out of+ `CRAFT_FONTS` and every `ui_*` list. - `SHIPPORI_MINCHO`: Shippori Mincho's bytes, or `None`. Generated Japanese Type 3 fonts record the fallback face's family and style in an indirect@@ -5,6 +5,9 @@ //! limits (Cloudflare Pages: 25 MiB per file). Native builds also read the optional //! `fonts/simplified-chinese-manifest.txt` (LXGW WenKai Lite); it is never embedded on wasm. //! `CRAFT_FONTS_REQUIRED` makes both the checkout and that native manifest required.+//! Native builds also read the opt-in Thai document manifest `fonts/thai-manifest.txt` (Sarabun)+//! into a separate `CRAFT_DOCUMENT_FONTS`: content faces, never interface fallbacks. It stays+//! optional even when required, since craft-fonts revisions before it have none. use std::fmt::Write as _; use std::path::PathBuf;@@ -18,16 +21,27 @@ fn main() { println!("cargo::rerun-if-env-changed=CRAFT_FONTS_DIR"); println!("cargo::rerun-if-env-changed=CRAFT_FONTS_REQUIRED"); let mut src = String::from("pub static CRAFT_FONTS: &[CraftFont] = &[\n");+ let mut documents = String::new(); let required = std::env::var_os("CRAFT_FONTS_REQUIRED").is_some(); if let Some(dir) = std::env::var_os("CRAFT_FONTS_DIR").map(PathBuf::from) { match craft_fonts(&dir, required) {- Ok(entries) => src.push_str(&entries),+ Ok(entries) => {+ src.push_str(&entries);+ let wasm = std::env::var("CARGO_CFG_TARGET_ARCH").is_ok_and(|a| a == "wasm32");+ match document_fonts(&dir, required, wasm) {+ Ok(entries) => documents = entries,+ Err(e) => println!("cargo::error=CRAFT_FONTS_DIR={}: {e}", dir.display()),+ }+ } Err(e) if required => println!("cargo::error=CRAFT_FONTS_DIR={}: {e}", dir.display()), Err(e) => println!("cargo::warning=building without craft-fonts: CRAFT_FONTS_DIR={}: {e}", dir.display()), } } else if required { println!("cargo::error=CRAFT_FONTS_REQUIRED is set but CRAFT_FONTS_DIR is missing"); }+ src.push_str("];\n/// Faces of craft-fonts' opt-in document manifests, for text written into PDFs only.\n");+ src.push_str("pub static CRAFT_DOCUMENT_FONTS: &[CraftFont] = &[\n");+ src.push_str(&documents); src.push_str("];\n"); let out = PathBuf::from(std::env::var_os("OUT_DIR").unwrap_or_default()).join("craft_fonts.rs"); if let Err(e) = std::fs::write(&out, src) {@@ -62,6 +76,35 @@ fn craft_fonts(dir: &std::path::Path, required: bool) -> Result<String, String> Ok(out) } +/// The faces of the opt-in Thai document manifest (`fonts/thai-manifest.txt`, Sarabun): for text+/// written into PDFs only, never interface fallbacks, so they go into `CRAFT_DOCUMENT_FONTS`, not+/// `CRAFT_FONTS`. Native only (the web build stays within its size limits). A missing manifest is+/// normal even when `required` (craft-fonts revisions before it have none); a broken one fails a+/// required build and is skipped with a warning otherwise.+fn document_fonts(dir: &std::path::Path, required: bool, wasm: bool) -> Result<String, String> {+ if wasm {+ return Ok(String::new());+ }+ let thai = dir.join("fonts/thai-manifest.txt");+ println!("cargo::rerun-if-changed={}", thai.display());+ match thai.try_exists() {+ Ok(false) => Ok(String::new()),+ Ok(true) => match manifest(dir, &thai, false, false) {+ Ok(entries) => Ok(entries),+ Err(e) if required => Err(e),+ Err(e) => {+ println!("cargo::warning=skipping optional Thai document fonts: {e}");+ Ok(String::new())+ }+ },+ Err(e) if required => Err(format!("{}: {e}", thai.display())),+ Err(e) => {+ println!("cargo::warning=skipping optional Thai document fonts: {}: {e}", thai.display());+ Ok(String::new())+ }+ }+}+ /// One `CraftFont { .. }` initialiser per manifest line. /// /// `strict_cap` is set for the required native Simplified Chinese manifest: a face that does not@@ -156,4 +199,27 @@ mod manifest_tests { assert!(manifest(&root, &path, false, true).is_err(), "a missing Hans asset cannot satisfy required input"); std::fs::remove_dir_all(root).unwrap(); }++ /// The Thai document manifest is optional even for a required build (the pinned craft-fonts+ /// predates it), native-only, and never breaks the build unless it is present and broken+ /// while `CRAFT_FONTS_REQUIRED` is set.+ #[test]+ fn document_manifest_is_optional_and_native_only() {+ let root = std::env::temp_dir().join(format!("pdfcraft-document-fonts-{}", std::process::id()));+ std::fs::create_dir_all(root.join("fonts")).unwrap();+ for required in [false, true] {+ assert_eq!(document_fonts(&root, required, false), Ok(String::new()), "missing manifest, required={required}");+ }+ std::fs::write(root.join("face.ttf"), b"synthetic manifest fixture").unwrap();+ let path = root.join("fonts/thai-manifest.txt");+ std::fs::write(&path, "# comment\nFixture | Regular | face.ttf | Thai,Latn\n").unwrap();+ for required in [false, true] {+ assert!(document_fonts(&root, required, false).unwrap().contains("family: \"Fixture\""), "required={required}");+ assert_eq!(document_fonts(&root, required, true), Ok(String::new()), "never on wasm, required={required}");+ }+ std::fs::write(&path, "not a manifest line\n").unwrap();+ assert_eq!(document_fonts(&root, false, false), Ok(String::new()), "a broken optional manifest is skipped");+ assert!(document_fonts(&root, true, false).is_err(), "a broken manifest fails a required build");+ std::fs::remove_dir_all(root).unwrap();+ } }@@ -2,7 +2,9 @@ //! //! `build.rs` embeds every font in craft-fonts' manifest when PdfCraft is built with //! `CRAFT_FONTS_DIR=<checkout>`; otherwise [`CRAFT_FONTS`] is empty and everything here returns-//! nothing. Callers must work either way.+//! nothing. Callers must work either way. Native builds also embed craft-fonts' opt-in Thai+//! document manifest, when the checkout has one, as [`CRAFT_DOCUMENT_FONTS`]: faces for text+//! written into PDFs, never interface fallbacks. /// A font from the optional craft-fonts build input (empty unless built with `CRAFT_FONTS_DIR`). pub struct CraftFont {@@ -66,13 +68,24 @@ pub fn ui_telugu_fonts() -> Vec<&'static CraftFont> { CRAFT_FONTS.iter().filter(|f| f.covers("Telu")).collect() } -/// Thai faces shared by the interface and PDF text writer.+/// The `Thai` faces of the main manifest for interface text, in manifest order. The PDF text+/// writer uses [`document_thai_font`], which prefers the document manifest's face. pub fn ui_thai_fonts() -> Vec<&'static CraftFont> { CRAFT_FONTS.iter().filter(|f| f.covers("Thai")).collect() } +/// The face for Thai text written into PDFs: the Regular face of craft-fonts' Thai document+/// manifest (Sarabun, looped Thai) when the build has it, else the interface Thai face (Noto Sans+/// Thai, loopless), else any document Thai face. `None` without a Thai face in the build. pub fn document_thai_font() -> Option<&'static CraftFont> {- ui_thai_fonts().into_iter().next()+ thai_document_face(CRAFT_DOCUMENT_FONTS, CRAFT_FONTS)+}++/// [`document_thai_font`] over the given document-manifest and main-manifest faces. Plain text+/// prefers the interface face to a document face of another style (no bold for regular text).+fn thai_document_face<'a>(document: &'a [CraftFont], ui: &'a [CraftFont]) -> Option<&'a CraftFont> {+ let thai = |faces: &'a [CraftFont]| faces.iter().filter(|f| f.covers("Thai"));+ thai(document).find(|f| f.style == "Regular").or_else(|| thai(ui).next()).or_else(|| thai(document).next()) } /// The `Deva` craft-fonts faces for Devanagari interface text (the Hindi catalog, file names,@@ -297,6 +310,44 @@ mod tests { assert_eq!(ui_arabic_fonts().len(), CRAFT_FONTS.iter().filter(|f| f.covers("Arab")).count()); } + #[test]+ fn thai_document_text_prefers_the_document_manifest_face() {+ static THAI_LATN: &[&str] = &["Thai", "Latn"];+ let document = [+ CraftFont { family: "FakeLooped", style: "Bold", scripts: THAI_LATN, bytes: b"looped-bold" },+ CraftFont { family: "FakeLooped", style: "Regular", scripts: THAI_LATN, bytes: b"looped" },+ CraftFont { family: "FakeLatin", style: "Regular", scripts: &["Latn"], bytes: b"not-thai" },+ ];+ let ui = [+ CraftFont { family: "BIZ UDPGothic", style: "Regular", scripts: &["Jpan"], bytes: b"not-thai" },+ CraftFont { family: "FakeLoopless", style: "Regular", scripts: &["Thai"], bytes: b"loopless" },+ ];+ // The document manifest's Regular face, whatever its position.+ assert_eq!(thai_document_face(&document, &ui).map(|f| f.bytes), Some(&b"looped"[..]));+ // Without a document Regular face, the interface face rather than a bold one for plain text.+ assert_eq!(thai_document_face(&document[..1], &ui).map(|f| f.bytes), Some(&b"loopless"[..]));+ // Without the document manifest (today's pinned craft-fonts), the interface face as before.+ assert_eq!(thai_document_face(&[], &ui).map(|f| f.bytes), Some(&b"loopless"[..]));+ // Some document Thai face rather than none; never a face of another script.+ assert_eq!(thai_document_face(&document[..1], &ui[..1]).map(|f| f.bytes), Some(&b"looped-bold"[..]));+ assert!(thai_document_face(&document[2..], &ui[..1]).is_none());+ assert!(thai_document_face(&[], &[]).is_none());+ }++ /// Document-manifest faces are for text written into PDFs only: the interface keeps its own.+ #[test]+ fn document_manifest_faces_are_never_interface_fallbacks() {+ assert!(ui_thai_fonts().iter().all(|f| !CRAFT_DOCUMENT_FONTS.iter().any(|d| std::ptr::eq(d, *f))));+ assert_eq!(ui_thai_fonts().len(), CRAFT_FONTS.iter().filter(|f| f.covers("Thai")).count());+ let any_thai = CRAFT_FONTS.iter().chain(CRAFT_DOCUMENT_FONTS).any(|f| f.covers("Thai"));+ assert_eq!(document_thai_font().is_some(), any_thai);+ if let Some(face) = CRAFT_DOCUMENT_FONTS.iter().find(|f| f.covers("Thai") && f.style == "Regular") {+ assert!(document_thai_font().is_some_and(|f| std::ptr::eq(f, face)));+ }+ #[cfg(target_arch = "wasm32")]+ assert!(CRAFT_DOCUMENT_FONTS.is_empty());+ }+ #[test] fn hebrew_document_faces_follow_the_weight() { static HEBR_LATN: &[&str] = &["Hebr", "Latn"];@@ -399,9 +399,17 @@ fn typing_thai_text_keeps_marks_and_commits_to_page() { if pdfcraft_fonts::document_thai_font().is_none() { return; }+ // The box being typed in draws with the interface Thai face; a document-only face (Sarabun,+ // from craft-fonts' Thai document manifest) is never an interface font. let fonts = pdfcraft_ui_egui::theme::font_definitions();- let face = pdfcraft_fonts::document_thai_font().unwrap();- assert!(fonts.families[&egui::FontFamily::Proportional].contains(&face.name()));+ let proportional = &fonts.families[&egui::FontFamily::Proportional];+ if let Some(ui) = pdfcraft_fonts::ui_thai_fonts().first() {+ assert!(proportional.contains(&ui.name()));+ }+ let document = pdfcraft_fonts::document_thai_font().unwrap();+ if !pdfcraft_fonts::ui_thai_fonts().iter().any(|f| std::ptr::eq(*f, document)) {+ assert!(!proportional.contains(&document.name()), "{} is a document-only face", document.name());+ } let mut h = harness(); assert!(h.state_mut().execute("edit.text")); h.run_steps(2);@@ -1,5 +1,6 @@ # PdfCraft roadmap: milestones and what's next +> **Last reviewed:** 2026-10-27 · **Last updated:** 2026-10-27 · **Change:** minor (M7, refs #1384: Thai Add Text uses craft-fonts' opt-in Thai document face, Sarabun, when the checkout has `fonts/thai-manifest.txt`; Noto Sans Thai otherwise and in the interface; estimates unchanged) > **Last reviewed:** 2026-10-25 | **Last updated:** 2026-10-25 | **Change:** minor (M14 localization current state: Arabic logical catalog, explicit RTL label direction and mirrored stock chrome; native/browser RTL workflows unqualified. Native release Hans face via LXGW WenKai Lite; estimates unchanged) > **Last reviewed:** 2026-10-23 · **Last updated:** 2026-10-23 · **Change:** minor (M7, #2636 partial: pasting added text boxes and images in the engine and through `content_copy`; estimates unchanged) > **Last reviewed:** 2026-10-23 · **Last updated:** 2026-10-23 · **Change:** minor (M11: image optimization accounts for page-local UserUnit; estimates unchanged)@@ -2022,8 +2022,8 @@ milestone = "M7" status = "shipped" commands = ["edit.text"] tools = ["page_add_text", "content_update"]-evidence = ["crates/ui-egui/tests/it/content.rs::typing_moving_styling_and_deleting_added_content", "crates/ui-egui/tests/it/content.rs::clicking_elsewhere_keeps_the_text_and_starts_a_new_box", "crates/automation/tests/it/automation.rs::adding_content_through_tools", "crates/edit/src/tests.rs::added_text_and_images_are_page_content_that_stays_editable", "crates/engine/src/tests.rs::added_text_and_images_render_and_stay_editable", "crates/edit/src/tests.rs::added_arabic_text_is_shaped_in_display_order_and_stays_searchable", "crates/automation/tests/it/automation.rs::thai_text_through_tools_saves_reopens_extracts_and_renders", "crates/ui-egui/tests/it/content.rs::typing_thai_text_keeps_marks_and_commits_to_page", "crates/automation/tests/it/automation.rs::added_arabic_text_keeps_copy_search_and_editor_order_after_retyping_and_reopen", "crates/edit/src/tests.rs::added_hebrew_text_is_drawn_right_to_left_and_stays_searchable", "crates/automation/tests/it/automation.rs::hebrew_text_through_tools_saves_reopens_extracts_and_renders"]-notes = "page content, editable later (retype, move, restyle); standard fonts, Windows-1252 text. Arabic text is shaped with the craft-fonts Arab face (Type 3 outlines with ToUnicode) and laid out in bidirectional display order; per-line ActualText keeps copy, search and editor text in typing order through retyping and save/reopen; it is drawn regular whatever the bold/italic setting, and typing in the box is still left to right (misc.rtl-ui). Hebrew (#128) takes the same path with the craft-fonts Hebr face (Noto Sans Hebrew): right-to-left display order, points and cantillation kept on their letter as one glyph, ToUnicode and per-line ActualText; regular style only, and right-to-left scripts other than Arabic and Hebrew are not covered. Thai Add Text uses the craft-fonts Thai face, with shaped clusters and ToUnicode; Thai is regular style, wrapping preserves clusters but is not dictionary-based, and forms/Fill and Sign are not covered by this change. Each click on the page keeps the box being typed and starts a new one; Done and Discard beside the box"+evidence = ["crates/ui-egui/tests/it/content.rs::typing_moving_styling_and_deleting_added_content", "crates/ui-egui/tests/it/content.rs::clicking_elsewhere_keeps_the_text_and_starts_a_new_box", "crates/automation/tests/it/automation.rs::adding_content_through_tools", "crates/edit/src/tests.rs::added_text_and_images_are_page_content_that_stays_editable", "crates/engine/src/tests.rs::added_text_and_images_render_and_stay_editable", "crates/edit/src/tests.rs::added_arabic_text_is_shaped_in_display_order_and_stays_searchable", "crates/automation/tests/it/automation.rs::thai_text_through_tools_saves_reopens_extracts_and_renders", "crates/ui-egui/tests/it/content.rs::typing_thai_text_keeps_marks_and_commits_to_page", "crates/edit/src/tests.rs::added_thai_text_is_drawn_from_the_document_thai_face", "crates/automation/tests/it/automation.rs::added_arabic_text_keeps_copy_search_and_editor_order_after_retyping_and_reopen", "crates/edit/src/tests.rs::added_hebrew_text_is_drawn_right_to_left_and_stays_searchable", "crates/automation/tests/it/automation.rs::hebrew_text_through_tools_saves_reopens_extracts_and_renders"]+notes = "page content, editable later (retype, move, restyle); standard fonts, Windows-1252 text. Arabic text is shaped with the craft-fonts Arab face (Type 3 outlines with ToUnicode) and laid out in bidirectional display order; per-line ActualText keeps copy, search and editor text in typing order through retyping and save/reopen; it is drawn regular whatever the bold/italic setting, and typing in the box is still left to right (misc.rtl-ui). Hebrew (#128) takes the same path with the craft-fonts Hebr face (Noto Sans Hebrew): right-to-left display order, points and cantillation kept on their letter as one glyph, ToUnicode and per-line ActualText; regular style only, and right-to-left scripts other than Arabic and Hebrew are not covered. Thai Add Text uses the craft-fonts Thai document face, with shaped clusters and ToUnicode: Sarabun (looped Thai) on native builds whose craft-fonts checkout has the opt-in fonts/thai-manifest.txt, otherwise the Noto Sans Thai interface face (the pinned craft-fonts revision has no Thai document manifest yet, so release builds still use Noto Sans Thai); Thai is regular style, wrapping preserves clusters but is not dictionary-based, and forms/Fill and Sign are not covered by this change. Each click on the page keeps the box being typed and starts a new one; Done and Discard beside the box" [[feature]] id = "edit.find-replace"