From 4571cd58c13c3145a69871e9a35fd855bb72dec8 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:17:45 +0200 Subject: [PATCH] Print and PDF export use the Numeral setting (#3784) (#3824) --- crates/engine/src/cmd/file.rs | 10 +++++++-- crates/engine/src/cmd/view.rs | 4 ++-- crates/engine/src/io.rs | 2 ++ crates/engine/src/io_ext.rs | 9 ++++++-- crates/engine/src/session.rs | 1 + crates/engine/src/tests.rs | 39 +++++++++++++++++++++++++++++++++++ crates/pdf/src/lib.rs | 19 ++++++++++++++--- 7 files changed, 75 insertions(+), 9 deletions(-) diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 6494aa55d..cf59c833e 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -364,10 +364,16 @@ fn save_as(s: &mut Session, v: &Value) -> CmdResult { } /// How a copy is written: Print Markup from the command's `markup`, else the File › Print -/// setting (`file.printMarkup`); comments as Track Changes Options show them. +/// setting (`file.printMarkup`); comments as Track Changes Options show them; digits as the +/// Numeral setting shows them on screen. pub fn export_options(s: &Session, v: &Value) -> crate::io::ExportOptions { let markup = p::bool(v, "markup").unwrap_or(s.prefs.print_markup); - crate::io::ExportOptions { markup, comments: markup && s.prefs.markup.comments, word2003: p::str(v, "format") == Some(FORMAT_WORD_2003) } + crate::io::ExportOptions { + markup, + comments: markup && s.prefs.markup.comments, + word2003: p::str(v, "format") == Some(FORMAT_WORD_2003), + numeral: s.view.numeral.resolve(crate::session::system_locale()), + } } fn export_png(s: &mut Session, v: &Value) -> CmdResult { diff --git a/crates/engine/src/cmd/view.rs b/crates/engine/src/cmd/view.rs index bc46df315..98e2c5eb5 100644 --- a/crates/engine/src/cmd/view.rs +++ b/crates/engine/src/cmd/view.rs @@ -141,8 +141,8 @@ pub fn specs() -> Vec { .pure(), CommandSpec::new("view.state", "View State", "View", |s, _| serde_json::to_value(&s.view).map_err(|e| CmdError::Failed(e.to_string()))) .pure(), - // Word › File › Options › Advanced › Numeral: how digits look on screen (display - // only; the document text keeps the typed codepoints). + // Word › File › Options › Advanced › Numeral: how digits look on screen, in print, PDFs + // and page images (display only; the document text keeps the typed codepoints). CommandSpec::new("view.numeral", "Numerals", "File › Options › Advanced", |s, v| match v.get("value") { None => Ok(json!({"value": s.view.numeral})), Some(Value::String(name)) => { diff --git a/crates/engine/src/io.rs b/crates/engine/src/io.rs index 0c2593196..27e702053 100644 --- a/crates/engine/src/io.rs +++ b/crates/engine/src/io.rs @@ -59,6 +59,8 @@ pub struct ExportOptions { /// An `.xml` copy is a Word 2003 XML Document (WordprocessingML 2003) instead of a Word XML /// Document (Flat OPC): `format: "wordml2003"` on the save commands. pub word2003: bool, + /// How digits look in PDFs and page images (the Numeral setting, `System` resolved). + pub numeral: wordcraft_layout::NumeralMode, } /// A document password: wiped from memory when dropped, never printed (`Debug` shows `***`). diff --git a/crates/engine/src/io_ext.rs b/crates/engine/src/io_ext.rs index aea98a343..762ac423a 100644 --- a/crates/engine/src/io_ext.rs +++ b/crates/engine/src/io_ext.rs @@ -31,7 +31,12 @@ pub fn save(ext: &str, doc: &Document, opts: &crate::io::ExportOptions) -> Optio "xml" => Some(wordcraft_docx::write_flat(doc).map_err(|e| e.to_string())), "png" => Some(render_png(doc, 0, 2.0, opts)), "pdf" => { - let pdf = wordcraft_pdf::PdfOptions { include_markup: opts.markup, comments: opts.markup && opts.comments, ..Default::default() }; + let pdf = wordcraft_pdf::PdfOptions { + include_markup: opts.markup, + comments: opts.markup && opts.comments, + numeral: opts.numeral, + ..Default::default() + }; Some(wordcraft_pdf::export(doc, &pdf).map_err(|e| e.to_string())) } other => wordcraft_formats::export_with(other, doc, &wordcraft_formats::ExportOptions { comments: opts.markup && opts.comments }), @@ -41,7 +46,7 @@ pub fn save(ext: &str, doc: &Document, opts: &crate::io::ExportOptions) -> Optio /// Render page `page` as PNG at `scale` px/pt. With Print Markup's comments the page has its /// markup area with the comments in balloons (page images always show tracked changes). pub fn render_png(doc: &Document, page: usize, scale: f32, opts: &crate::io::ExportOptions) -> Result, String> { - let lo = wordcraft_layout::LayoutOptions { comment_balloons: opts.markup && opts.comments, ..Default::default() }; + let lo = wordcraft_layout::LayoutOptions { comment_balloons: opts.markup && opts.comments, numeral: opts.numeral, ..Default::default() }; let l = wordcraft_layout::layout(doc, &mut wordcraft_layout::LayoutCache::new(), &lo); let p = l.pages.get(page).ok_or_else(|| format!("no page {}", page + 1))?; let img = wordcraft_render::render_page(doc, p, scale, &Default::default()); diff --git a/crates/engine/src/session.rs b/crates/engine/src/session.rs index 68ec37f8e..a13489e6d 100644 --- a/crates/engine/src/session.rs +++ b/crates/engine/src/session.rs @@ -973,6 +973,7 @@ impl Session { let opts = LayoutOptions { view: ViewMode::Print, comment_balloons: markup && self.prefs.markup.comments, + numeral: self.view.numeral.resolve(SYSTEM_LOCALE.get().map(String::as_str)), filename: Some(Arc::from(fn_name.as_str())), filepath: Some(Arc::from(fn_path.as_str())), ..Default::default() diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 305e640d7..a91e71efe 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -5036,6 +5036,45 @@ fn numeral_mode_sets_gets_and_rejects_junk() { assert_eq!(text(&s), "Year 2024"); } +/// The Numeral setting also applies to print and PDF output, not just the screen layout. +#[test] +fn export_layout_follows_the_numeral_setting() { + use wordcraft_layout::Placed; + fn glyph_ids(s: &Session) -> Vec { + let mut out = Vec::new(); + for page in &s.export_layout().pages { + for it in &page.items { + if let Placed::Lines { para, .. } = it { + out.extend(para.glyphs.iter().map(|g| g.gid)); + } + } + } + out + } + let mut s = s(); + s.doc = wordcraft_doc::Document::from_text("Year 2024"); + let western = glyph_ids(&s); + assert!(!western.is_empty()); + run(&mut s, "view.numeral", json!({"value": "hindi"})); + assert_ne!(glyph_ids(&s), western, "Hindi digits reach the printed layout"); +} + +/// File › Export PDF, Save As PDF and page images take the Numeral setting too. +#[test] +fn pdf_and_png_copies_follow_the_numeral_setting() { + let mut s = s(); + s.doc = wordcraft_doc::Document::from_text("Year 2024"); + // A fixed creation date, so the PDF bytes depend only on the layout. + s.doc.core.created = "2024-01-01T00:00:00Z".into(); + let copy = |s: &Session, name: &str| crate::io::save_bytes_opts(name, &s.doc, &cmd::file::export_options(s, &json!({}))).unwrap(); + let (pdf, png) = (copy(&s, "x.pdf"), copy(&s, "x.png")); + assert_eq!(copy(&s, "x.pdf"), pdf); + run(&mut s, "view.numeral", json!({"value": "hindi"})); + assert_eq!(cmd::file::export_options(&s, &json!({})).numeral, wordcraft_layout::NumeralMode::Hindi); + assert_ne!(copy(&s, "x.pdf"), pdf, "Hindi digits reach the PDF"); + assert_ne!(copy(&s, "x.png"), png, "Hindi digits reach the page image"); +} + /// Issue #67: View › Zoom steps. Zoom In/Out leave a fit mode and step 10% from the current zoom /// (the UI keeps `view.zoom` equal to the shown zoom while a fit mode is on), and never get stuck /// short of the 10%–500% limits. diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 7836636a7..e6072e91d 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -50,7 +50,7 @@ use wordcraft_geom::Rect; use wordcraft_layout::display::Paint; use wordcraft_layout::display::{DisplayOptions, Draw, Stroke as LineStyle, page_display}; use wordcraft_layout::para::{ClKind, LineEnd}; -use wordcraft_layout::{DocLayout, LayoutCache, LayoutOptions, Page, Placed, layout}; +use wordcraft_layout::{DocLayout, LayoutCache, LayoutOptions, NumeralMode, Page, Placed, layout}; use wordcraft_metafile::{Picture, PlacedItem}; /// Largest page side we write (200", the PDF limit). @@ -95,11 +95,24 @@ pub struct PdfOptions { /// is scaled to fit and centred, the paper turned to the page's orientation (Scale to paper /// size, #2108). `None` keeps the document's page sizes. pub paper: Option<(f32, f32)>, + /// How [`export`] shows digits (the Numeral setting; callers resolve `System`, see + /// [`NumeralMode::resolve`]). [`export_layout`] keeps the layout it is given. + pub numeral: NumeralMode, } impl Default for PdfOptions { fn default() -> Self { - PdfOptions { pages: None, title: None, author: None, tagged: true, include_markup: false, comments: false, compress: true, paper: None } + PdfOptions { + pages: None, + title: None, + author: None, + tagged: true, + include_markup: false, + comments: false, + compress: true, + paper: None, + numeral: NumeralMode::Arabic, + } } } @@ -108,7 +121,7 @@ impl Default for PdfOptions { /// markup area with the comment balloons. pub fn export(doc: &Document, opts: &PdfOptions) -> Result, PdfError> { write::guard(|| { - let lo = LayoutOptions { hide_deleted: !opts.include_markup, comment_balloons: opts.comments, ..Default::default() }; + let lo = LayoutOptions { hide_deleted: !opts.include_markup, comment_balloons: opts.comments, numeral: opts.numeral, ..Default::default() }; let lay = layout(doc, &mut LayoutCache::new(), &lo); export_layout_inner(doc, &lay, opts) })