Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3c976d3
M12.AR3: shape and preserve Arabic PDF text before writing output
Mouhand-Kaddo Oct 24, 2026
8a96504
M12.AR5: preserve cell direction and RTL sheet geometry in PDF export
Mouhand-Kaddo Oct 13, 2026
d261391
Merge upstream main into fix/arabic-pdf-text
Mouhand-Kaddo Oct 27, 2026
88f1f5d
Merge upstream main into fix/arabic-pdf-reading-direction
Mouhand-Kaddo Oct 27, 2026
77819b0
Merge remote-tracking branch 'origin/main' into fix/arabic-pdf-text
Mouhand-Kaddo Nov 1, 2026
fcf4986
Merge origin/main into fix/arabic-pdf-reading-direction
Mouhand-Kaddo Nov 1, 2026
c9df883
Merge fix/arabic-pdf-text (#940) into fix/arabic-pdf-reading-direction
Mouhand-Kaddo Nov 1, 2026
71627ff
Merge origin/main into fix/arabic-pdf-text
Mouhand-Kaddo Nov 2, 2026
7d507b1
Merge fix/arabic-pdf-text (#940) into fix/arabic-pdf-reading-direction
Mouhand-Kaddo Nov 2, 2026
93934ea
Merge origin/main into fix/arabic-pdf-text
Mouhand-Kaddo Nov 3, 2026
7395ea3
Merge fix/arabic-pdf-text (#940) into fix/arabic-pdf-reading-direction
Mouhand-Kaddo Nov 3, 2026
3c3a36c
Merge origin/main into fix/arabic-pdf-text
Mouhand-Kaddo Nov 7, 2026
95ed0d5
M12.AR3: wrap shaped Arabic text by measured pieces after main's runn…
Mouhand-Kaddo Nov 7, 2026
02bc527
Merge fix/arabic-pdf-text (#940) into fix/arabic-pdf-reading-direction
Mouhand-Kaddo Nov 7, 2026
9ac5d0c
M12.AR3: fall back instead of failing PDF export when the Arabic font…
Mouhand-Kaddo Nov 9, 2026
6e92ec5
Merge origin/main into fix/arabic-pdf-text
Mouhand-Kaddo Nov 9, 2026
d328b7c
Merge origin/main into fix/arabic-pdf-text
Mouhand-Kaddo Nov 9, 2026
816a6c0
Merge fix/arabic-pdf-text (#940) into fix/arabic-pdf-reading-direction
Mouhand-Kaddo Nov 9, 2026
0d7334a
M12.AR5: read render_pdf's bytes in the RTL mirroring test
Mouhand-Kaddo Nov 9, 2026
44b0d6f
Merge origin/main into fix/arabic-pdf-text
Mouhand-Kaddo Nov 13, 2026
fd27066
Merge fix/arabic-pdf-text (#940) into fix/arabic-pdf-reading-direction
Mouhand-Kaddo Nov 13, 2026
1671616
M12.AR3: say what the no-font fallback draws; drop an unrelated test …
Mouhand-Kaddo Nov 13, 2026
6e1b194
Merge fix/arabic-pdf-text (#940) into fix/arabic-pdf-reading-direction
Mouhand-Kaddo Nov 13, 2026
62f7364
M12.AR5: explicit Left and Right keep their side on a right-to-left s…
Mouhand-Kaddo Nov 13, 2026
e45ab09
M12.AR3: the oversized Arabic text test passes without an Arabic face
Mouhand-Kaddo Nov 13, 2026
7cb8ca6
Merge branch 'fix/arabic-pdf-text' into fix/arabic-pdf-reading-direction
Mouhand-Kaddo Nov 13, 2026
eddc52f
M12.AR5: the merged oversized Arabic test returns () as on this branch
Mouhand-Kaddo Nov 13, 2026
911a5d2
M12.AR3: Arabic print tests skip on the missing-font flag, not a zero…
Mouhand-Kaddo Nov 13, 2026
dba1cd3
Merge branch 'fix/arabic-pdf-text' into fix/arabic-pdf-reading-direction
Mouhand-Kaddo Nov 13, 2026
b1b3c93
Merge fix/arabic-pdf-text (#940) into fix/arabic-pdf-reading-direction
Mouhand-Kaddo Nov 13, 2026
028d80a
M12.AR5: gaps §5 lists non-Latin PDF outside Arabic in both Missing e…
Mouhand-Kaddo Nov 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ATTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,7 @@ Generated-in-code art is original and has no file to list: the UI icon set (`cra
| `docs/images/pivot.png` | GridCraft contributors | screenshot of GridCraft rendered offscreen (`crates/ui-egui/examples/snapshot.rs`) showing an original sample workbook | MIT OR Apache-2.0 | README screenshot; no third-party content |
| `docs/images/format-chart.png` | GridCraft contributors | screenshot of GridCraft rendered offscreen (`crates/ui-egui/examples/snapshot.rs`) showing an original sample workbook | MIT OR Apache-2.0 | README screenshot; no third-party content |
| `docs/images/page-break.png` | GridCraft contributors | screenshot of GridCraft rendered offscreen (`crates/ui-egui/examples/snapshot.rs`) showing an original sample workbook | MIT OR Apache-2.0 | README screenshot; no third-party content |
| Core 14 AFM metric numbers in `crates/pdf/src/symbol.rs` (Symbol: character codes and advance widths) and `crates/pdf/src/metrics.rs` (Helvetica, Times and Courier advance widths); no font file | Adobe Systems Incorporated | Adobe's Core 14 AFM font metrics files (`Symbol.afm` 001.008 and the Helvetica, Times and Courier AFMs) | Adobe's Core 14 metrics notice: the files may be used, copied and distributed for any purpose and without charge, provided the notice is kept (quoted in `crates/pdf/src/metrics.rs`) | Numbers only, typed in from the public metrics. PDF export embeds no font: Symbol and the Times faces are base-14 fonts every PDF reader supplies, and text outside WinAnsiEncoding is set in a predefined, non-embedded Adobe-GB1 CID font |
| Core 14 AFM metric numbers in `crates/pdf/src/symbol.rs` (Symbol: character codes and advance widths) and `crates/pdf/src/metrics.rs` (Helvetica, Times and Courier advance widths); no font file | Adobe Systems Incorporated | Adobe's Core 14 AFM font metrics files (`Symbol.afm` 001.008 and the Helvetica, Times and Courier AFMs) | Adobe's Core 14 metrics notice: the files may be used, copied and distributed for any purpose and without charge, provided the notice is kept (quoted in `crates/pdf/src/metrics.rs`) | Numbers only, typed in from the public metrics. PDF export embeds none of these fonts: Symbol and the Times faces are base-14 fonts every PDF reader supplies, and non-Arabic text outside WinAnsiEncoding is set in a predefined, non-embedded Adobe-GB1 CID font |
| Unicode values of the Symbol encoding in `crates/pdf/src/symbol.rs` (the code-to-character map; numbers only, no file copied) | Unicode, Inc. | `symbol.txt`, "Adobe Symbol Encoding to Unicode", table version 1.0 (2011-07-12), at unicode.org/Public/MAPPINGS/VENDORS/ADOBE/ | Unicode, Inc.'s notice in the file: "Unicode, Inc. hereby grants the right to freely use the information supplied in this file in the creation of products supporting the Unicode Standard, and to make copies of this file in any form for internal or external distribution as long as this notice remains attached." The file is provided as-is, without warranties | Separate from Adobe's Core 14 notice (previous row), which covers the character codes and widths; the Unicode values come from this table |

Arabic PDF text uses Noto Sans Arabic from the optional [craft-fonts build input](https://github.com/storytold/craft-fonts), pinned by the release workflow. Author: The Noto Project Authors. Licence: SIL Open Font License 1.1. Native unbundled export uses an installed Noto Sans Arabic face. No new asset file is committed.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
| 2026-10-31 | Recently Used lists the functions most recently entered in a formula from any source (ribbon, control channel, MCP, scripts), kept with the preferences and returned by `formulas.recentlyUsed`; the Insert Function dialog takes Excel's title and a working category list (Most Recently Used, All, each category) |
| 2026-10-31 | Formulas › Function Library as a big Insert Function button, the AutoSum split button and three columns of labelled small buttons, each category book in its own colour; category menus end with Insert Function… on that category; More Functions cascades into the remaining categories; Home › Editing shares the AutoSum menu; the MR key tip opens Recently Used |
| 2026-10-31 | System and embedded CJK fallback text shares the Latin baseline in cells, editors and window titles (#1265, #2621). Symbol and emoji alignment, manual row heights and clipping are unchanged |
| 2026-10-26 | PDF preserves Context/LTR/RTL reading direction through wrapping and drawing; RTL sheet cells, borders, headings, objects and sparklines mirror consistently |
| 2026-10-26 | Arabic PDF export shapes licensed glyph outlines and preserves searchable logical text; characters the font lacks, or a missing Arabic font, fall back to the standard fonts instead of failing the export |
| 2026-10-26 | Arabic bidi display/editing preserve logical source text; per-cell reading direction is editable and survives undo, XLSX and conditional-format priority |
| 2026-10-26 | Arabic text shows in cells and the interface through a system, fontconfig or craft-fonts Arabic face; workbook fonts keep their script fallbacks |
| 2026-10-25 | AutoSum over a selection does what Excel does: a selected row gets one total to its right (in its last cell if that is empty), not a one-cell total under each cell; a block whose last column is empty gets each row's total there, one whose last row is empty each column's, and with both the corner gets the total of the block; otherwise totals go under each column as before |
Expand Down Expand Up @@ -285,6 +287,8 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
| 2026-10-31 | minor | Progress log: Recently Used and Insert Function categories |
| 2026-10-31 | minor | Progress log: Function Library |
| 2026-10-31 | minor | Progress log: shared CJK/Latin baselines in cells, editors and window titles |
| 2026-10-26 | minor | Progress log: PDF reading direction and RTL sheets |
| 2026-10-26 | minor | Progress log: Arabic PDF text |
| 2026-10-26 | minor | Progress log: Arabic bidi display and editing |
| 2026-10-26 | minor | Progress log: Arabic font fallback |
| 2026-10-25 | minor | Progress log: AutoSum over a selected row, and a selection's empty last row and column |
Expand Down
1 change: 1 addition & 0 deletions crates/engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ gridcraft-chart = { workspace = true }
gridcraft-pdf = { workspace = true }
gridcraft-math = { workspace = true }
image = { workspace = true }
unicode-segmentation = "=1.13.3"

[dev-dependencies]
gridcraft-xlsx = { workspace = true, features = ["test-support"] }
Expand Down
47 changes: 37 additions & 10 deletions crates/engine/src/cmd/print.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ use gridcraft_core::{CellRef, MAX_COLS, MAX_ROWS, RangeRef, Value};
#[cfg(test)]
use gridcraft_model::{Alignment, Image, SheetViewMode};
use gridcraft_model::{
Anchor, BorderLine, BorderStyle, Borders, Color, HAlign, Orientation, PageOrder, PatternType, PrintSettings, ShapeKind, Sheet, Style, StyleId,
StyleTable, Underline, VAlign, Visibility, Workbook,
Anchor, BorderLine, BorderStyle, Borders, Color, HAlign, Orientation, PageOrder, PatternType, PrintSettings, ReadingOrder, ShapeKind, Sheet,
Style, StyleId, StyleTable, Underline, VAlign, Visibility, Workbook,
};
use gridcraft_pdf::{Font, Page, PdfDoc, Point, Rect, Rgb};
use gridcraft_pdf::{Baseline, Font, Page, PdfDoc, Point, Rect, Rgb, TextDirection};
use serde_json::{Value as Json, json};

use super::{CommandSpec, Result, Session, bad, bool_param, cmd, edit, f64_param, has_doc, split_sheet, str_param, target_sheet, u32_param};
use super::{
CommandSpec, EngineError, Result, Session, bad, bool_param, cmd, edit, f64_param, has_doc, split_sheet, str_param, target_sheet, u32_param,
};

mod draw;
mod header_footer;
Expand Down Expand Up @@ -151,8 +153,17 @@ pub(crate) const PX_TO_PT: f32 = 0.75;
/// Most pages one export produces.
pub const MAX_PAGES: usize = 2000;

/// A rendered PDF and whether some of its Arabic text was set without an Arabic face.
pub struct RenderedPdf {
pub bytes: Vec<u8>,
pub pages: usize,
pub missing_arabic_font: bool,
}

const MISSING_ARABIC_FONT: &str = "Some Arabic text could not be drawn in this PDF because no Noto Sans Arabic face was found for it.";

/// Renders laid-out sheets to a PDF.
pub fn render_pdf(wb: &Workbook, layouts: &[SheetLayout], file: &str, path: &str) -> (Vec<u8>, usize) {
pub fn render_pdf(wb: &Workbook, layouts: &[SheetLayout], file: &str, path: &str) -> Result<RenderedPdf> {
let mut doc = PdfDoc::new();
doc.info.title = if wb.props.title.is_empty() { file.to_string() } else { wb.props.title.clone() };
doc.info.author = wb.props.author.clone();
Expand Down Expand Up @@ -188,7 +199,16 @@ pub fn render_pdf(wb: &Workbook, layouts: &[SheetLayout], file: &str, path: &str
let b = make_block(wb, lay.sheet, sh, *rs, *cs, (x, y), lay);
draw_block(pg, &b, lay.settings.gridlines, &mut cf);
draw_block_links(pg, &b, page_index, &mut pending_links);
printed_blocks.push(PrintedBlock { page: page_index, sheet: lay.sheet, rows: *rs, cols: *cs, x, y, k });
printed_blocks.push(PrintedBlock {
page: page_index,
sheet: lay.sheet,
rows: *rs,
cols: *cs,
x,
y,
k,
mirror_axis: sh.right_to_left.then_some(b.mirror_axis),
});
if lay.settings.headings {
draw_headings(pg, &b, rsi == 0, csi == 0, (ox, oy), (lay.heading_w * k, lay.heading_h * k));
}
Expand All @@ -204,6 +224,7 @@ pub fn render_pdf(wb: &Workbook, layouts: &[SheetLayout], file: &str, path: &str
}
if lay.settings.headings {
let fill = [0xF2, 0xF2, 0xF2];
let ox = if sh.right_to_left { lay.page_w - ox - lay.heading_w * k } else { ox };
pg.fill_rect(ox, oy, lay.heading_w * k, lay.heading_h * k, fill);
pg.rect_stroke(ox, oy, lay.heading_w * k, lay.heading_h * k, 0.3, [0xA0, 0xA0, 0xA0]);
}
Expand All @@ -227,18 +248,24 @@ pub fn render_pdf(wb: &Workbook, layouts: &[SheetLayout], file: &str, path: &str
page.link_page(Rect::new(lx, ly, w, h), target_page, Point::new(x, y));
}
}
let n = doc.page_count();
(doc.finish(), n)
let pages = doc.page_count();
let missing_arabic_font = doc.missing_arabic_font();
let bytes = doc.finish().map_err(|error| EngineError::Other(error.to_string()))?;
Ok(RenderedPdf { bytes, pages, missing_arabic_font })
}

// ---------------------------------------------------------------- commands

fn build_pdf(s: &Session, p: &Json, cmd: &str) -> Result<(Vec<u8>, usize)> {
fn build_pdf(s: &mut Session, p: &Json, cmd: &str) -> Result<(Vec<u8>, usize)> {
let opts = PrintOptions::from_params(p, cmd)?;
let d = s.doc()?;
let lays = layouts(&d.wb, &opts);
let path = d.path.clone().unwrap_or_default();
Ok(render_pdf(&d.wb, &lays, &d.display_title(), &path))
let pdf = render_pdf(&d.wb, &lays, &d.display_title(), &path)?;
if pdf.missing_arabic_font {
s.ui_requests.push(crate::UiRequest::Message(MISSING_ARABIC_FONT.into()));
}
Ok((pdf.bytes, pdf.pages))
}

pub(crate) fn export_pdf(s: &mut Session, p: &Json) -> Result<Json> {
Expand Down
Loading
Loading