Files
wanderbooks/crates/ledger-import/src/fonts.rs
T
swaitsandClaude Opus 5.5 2c29d48ab9 P3: the IIF in plain ASCII with every change listed, and the CPA package as of one day (defects 7, 21)
The CPA package's general-journal.iif now holds every posted entry through
as_of (closing entries left out, D-36), written as plain ASCII with CR LF,
DOCNUM the entry id, QuickBooks Desktop's field limits and subaccount types
honoured, and every character written differently listed in the export's
warnings and the README (grouped by substitution, TINs masked). The 1099
worksheet stops at as_of too. PDFs print CJK, Arabic, Hebrew, Thai and
Devanagari (Noto fallbacks, OFL), and so does the viewer. has_full_tin reads
Unicode separators.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
2026-10-21 09:35:32 -06:00

112 lines
4.1 KiB
Rust

//! The fonts the books print in, bundled so nothing depends on what's
//! installed: Inter for the text, and a Noto fallback for each script
//! Inter lacks, so a vendor or an account named in Chinese, Japanese,
//! Korean, Arabic, Hebrew, Thai or Hindi prints as written, never as
//! empty boxes. Each font's provenance and license is in `fonts/`
//! (`Inter-LICENSE.txt`, `NotoSansCJK-LICENSE.txt`, `NotoSans-LICENSE.txt`;
//! all SIL OFL 1.1).
//!
//! ```text
//! GOAL: every name prints as the books keep it, in the PDFs and in the
//! viewer, from one copy of each font in the binary.
//! SOURCE: DECISION (lead, P3 review round 2, 2026-10-20: CJK, Arabic,
//! Hebrew, Thai and Devanagari; no emoji, a colour emoji font
//! being about 10 MB).
//! INVARIANTS:
//! 1. The PDFs (crate::pdf, crate::asset_register, crate::labels) set
//! their text in Inter, then try FALLBACKS in order, as each template
//! names them (`font: ("Inter", "Noto Sans CJK SC", ...)`).
//! 2. The viewer (ledger-gui) registers FALLBACKS, these same bytes,
//! after its own fonts: each font is in the binary once.
//! ```
/// Inter, the PDFs' text face, in the three weights the templates use.
pub static INTER: [&[u8]; 3] = [
include_bytes!("../fonts/Inter-Regular.ttf"),
include_bytes!("../fonts/Inter-SemiBold.ttf"),
include_bytes!("../fonts/Inter-Bold.ttf"),
];
/// The fallbacks for the scripts Inter lacks, each its family name and
/// its bytes, in the order tried: Noto Sans CJK SC (a subset: every
/// character from U+1100 on that the face draws and Inter doesn't: Hangul,
/// kana, the CJK ideographs, fullwidth forms), Noto Sans Arabic UI (its
/// letters keep their dots, so a PDF's text reads back as written:
/// `fonts/NotoSans-LICENSE.txt`), Noto Sans Hebrew, Noto Sans Thai and
/// Noto Sans Devanagari (each whole, its static Regular weight). Emoji
/// have none.
pub static FALLBACKS: [(&str, &[u8]); 5] = [
(
"Noto Sans CJK SC",
include_bytes!("../fonts/NotoSansCJKsc-Regular-subset.otf"),
),
(
"Noto Sans Arabic UI",
include_bytes!("../fonts/NotoSansArabicUI-Regular.ttf"),
),
(
"Noto Sans Hebrew",
include_bytes!("../fonts/NotoSansHebrew-Regular.ttf"),
),
(
"Noto Sans Thai",
include_bytes!("../fonts/NotoSansThai-Regular.ttf"),
),
(
"Noto Sans Devanagari",
include_bytes!("../fonts/NotoSansDevanagari-Regular.ttf"),
),
];
/// Every font the PDFs use (INVARIANT 1), as Typst takes them: the
/// bundled bytes themselves, not a copy of them.
pub(crate) fn typst_fonts() -> Vec<typst::foundations::Bytes> {
INTER
.iter()
.copied()
.chain(FALLBACKS.iter().map(|(_, bytes)| *bytes))
.map(typst::foundations::Bytes::new)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
/// INVARIANT 1: every template names Inter, then every fallback, in
/// FALLBACKS' order; and each font's family is the one it is named
/// by.
#[test]
fn every_template_names_every_fallback_in_order() {
let mut want = String::from("font: (\"Inter\"");
for (family, _) in &FALLBACKS {
want.push_str(&format!(", \"{family}\""));
}
want.push(')');
for (name, t) in [
("statement.typ", include_str!("../templates/statement.typ")),
(
"asset_register.typ",
include_str!("../templates/asset_register.typ"),
),
("labels.typ", include_str!("../templates/labels.typ")),
] {
assert!(t.contains(&want), "{name} names {want}");
}
let book = typst::text::FontBook::from_fonts(
typst_fonts()
.into_iter()
.flat_map(|b| typst::text::Font::iter(b).collect::<Vec<_>>())
.collect::<Vec<_>>()
.iter(),
);
for (family, _) in &FALLBACKS {
assert!(
book.contains_family(&family.to_lowercase()),
"{family} is a family the fonts hold"
);
}
assert!(book.contains_family("inter"));
}
}