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
This commit is contained in:
48 files changed
+5661
-317
No files matched your search
@@ -18,10 +18,17 @@ typst = "0.15.1"
|
||||
serde_json = "1"
|
||||
typst-layout = "0.15.1"
|
||||
qrcode = { version = "0.14.1", default-features = false, features = ["svg"] }
|
||||
# BSD-3-Clause: the IIF's transliteration to ASCII (iif.rs, rules.md B-34).
|
||||
# Pinned exactly: a new table would change what a CPA's file says.
|
||||
deunicode = { version = "=1.6.0", default-features = false }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1"
|
||||
# The viewer's PDF reader (ledger-gui's hayro): reads back the text a PDF
|
||||
# draws, glyph by glyph, in the tests.
|
||||
hayro-interpret = "0.8"
|
||||
kurbo = "0.13"
|
||||
rqrr = { version = "0.11.0", default-features = false }
|
||||
@@ -0,0 +1,144 @@
|
||||
Noto Sans Arabic UI, Noto Sans Hebrew, Noto Sans Thai and Noto Sans
|
||||
Devanagari, Regular (the PDFs' and the viewer's fallbacks for Arabic,
|
||||
Hebrew, Thai and Devanagari text, which Inter lacks), unmodified:
|
||||
|
||||
NotoSansArabicUI-Regular.ttf Version 2.011 190,944 bytes
|
||||
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/arabic)
|
||||
SHA-256 f4fe1866a64cba1e8b8d08a67cd29070f919edf778ca5de983c1f2a690fc953b
|
||||
NotoSansHebrew-Regular.ttf Version 3.001 16,836 bytes
|
||||
Copyright 2024 The Noto Project Authors (https://github.com/notofonts/hebrew)
|
||||
SHA-256 04272f5600d0ec816d31d0df73b23aa8d3501ea359ebe820da31c11ffcf00853
|
||||
NotoSansThai-Regular.ttf Version 2.002 20,960 bytes
|
||||
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/thai)
|
||||
SHA-256 d4303fe9c63ebb72759ca8b6d2040c8ae81689f7d08d7b91c656154382b49313
|
||||
NotoSansDevanagari-Regular.ttf Version 2.007 184,228 bytes
|
||||
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/devanagari)
|
||||
SHA-256 9c7d935139ea6a1e6ad9dbac4f6d27ece1e04bca8123c8888d00a0f9df4724cd
|
||||
|
||||
Noto is a trademark of Google LLC. (These fonts name no Reserved Font
|
||||
Name.)
|
||||
|
||||
Each is the static, unhinted Regular weight as the Noto project
|
||||
publishes it (https://github.com/notofonts/notofonts.github.io), at the
|
||||
last commit that changed it, retrieved 2026-10-20:
|
||||
|
||||
fonts/NotoSansArabicUI/unhinted/ttf/NotoSansArabicUI-Regular.ttf
|
||||
commit 1ed0aaad710107ac0d6663ece26a4bb82ce6cff8
|
||||
fonts/NotoSansHebrew/unhinted/ttf/NotoSansHebrew-Regular.ttf
|
||||
commit 40ae54ff25c9e0ad997f62fde48297dd9dfe51c8
|
||||
fonts/NotoSansThai/unhinted/ttf/NotoSansThai-Regular.ttf
|
||||
commit 6da9d59fd18f429cb8dbfa87e87669204f77ddab
|
||||
fonts/NotoSansDevanagari/unhinted/ttf/NotoSansDevanagari-Regular.ttf
|
||||
commit 92d2ea744479d3379927f1fb5f8b0945dd0349b7
|
||||
|
||||
https://raw.githubusercontent.com/notofonts/notofonts.github.io/<commit>/<path>
|
||||
|
||||
Arabic in its UI form, not Noto Sans Arabic itself (2.013): the same
|
||||
design and project, but 2.013 draws a letter's dots as marks of their
|
||||
own, which the PDFs' typesetter (Typst 0.15) writes as text runs of
|
||||
their own, each carrying the letter again, so a PDF's text (copied,
|
||||
searched, read aloud) would say "شش" for "ش" and "سلام" as "شلام"; the
|
||||
UI form, whose letters keep their dots and whose height fits a Latin
|
||||
line, reads back as written.
|
||||
|
||||
Unhinted, not hinted: the outlines are the same, the hinting tables
|
||||
(about 180 KB across the four) serve only Windows' old rasterizer, and
|
||||
neither the PDFs nor the viewer use them. Kept whole: Arabic and
|
||||
Devanagari need their shaping tables to join and order their letters.
|
||||
|
||||
Emoji are not bundled: a colour emoji font is about 10 MB, so an emoji
|
||||
in a name prints as an empty box in the PDFs (the viewer draws egui's
|
||||
own small emoji set).
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
@@ -0,0 +1,125 @@
|
||||
Noto Sans CJK SC Regular, version 2.004, subset:
|
||||
NotoSansCJKsc-Regular-subset.otf (the PDFs' fallback for Chinese,
|
||||
Japanese and Korean text, and the other characters below that Inter
|
||||
lacks).
|
||||
|
||||
Copyright (c) 2014-2021 Adobe (http://www.adobe.com/).
|
||||
Noto is a trademark of Google Inc.
|
||||
(Noto Sans CJK names no Reserved Font Name.)
|
||||
|
||||
Made from Noto Sans CJK 2.004 (https://github.com/notofonts/noto-cjk),
|
||||
NotoSansCJK-Regular.ttc as Arch Linux packages it (noto-fonts-cjk
|
||||
20240730-1; SHA-256
|
||||
b76b0433203017ca80401b2ee0dd69350349871c4b19d504c34dbdd80541690a),
|
||||
face 2 (Noto Sans CJK SC), with fontTools 4.65.0, 2026-10-20:
|
||||
|
||||
every code point at or above U+1100 that face maps and Inter Regular
|
||||
(Inter-Regular.ttf here) doesn't: 44,128 code points, among them all
|
||||
of Hangul, kana, bopomofo, CJK punctuation and fullwidth forms, and
|
||||
every CJK ideograph the face draws (20,976 of the basic block, 6,582
|
||||
of Extension A, the compatibility ideographs, and 2,310 from
|
||||
Extensions B to G), written as U+XXXX-YYYY ranges to cjk.txt, then
|
||||
|
||||
pyftsubset NotoSansCJK-Regular.ttc --font-number=2 \
|
||||
--unicodes-file=cjk.txt --no-hinting --layout-features='' \
|
||||
--drop-tables+=BASE,VORG,vhea,vmtx,GPOS,GSUB \
|
||||
--output-file=NotoSansCJKsc-Regular-subset.otf
|
||||
|
||||
SHA-256 of the result:
|
||||
ebf572d736e30b54bf5db087b15f323c16c465052aec788d9cc1d8db41884929
|
||||
|
||||
The subset is a Modified Version under the license below, which it
|
||||
keeps.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -46,7 +46,7 @@
|
||||
//! SOURCE (7-9): docs/spec/rules.md B-25;
|
||||
//! docs/spec/sources/irs-partnership-basis.md.
|
||||
|
||||
use crate::pdf::{INTER, PdfError};
|
||||
use crate::pdf::PdfError;
|
||||
use crate::report::{ReportMeta, amount_parts, format_amount, long_date};
|
||||
use crate::xlsx::{Formats, XlsxError, footer, header, number};
|
||||
use ledger_core::TaxBasisRule;
|
||||
@@ -512,7 +512,7 @@ fn engine() -> &'static TypstEngine<TypstTemplateMainFile> {
|
||||
ENGINE.get_or_init(|| {
|
||||
TypstEngine::builder()
|
||||
.main_file(TEMPLATE)
|
||||
.fonts(INTER)
|
||||
.fonts(crate::fonts::typst_fonts())
|
||||
.build()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
//! 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"));
|
||||
}
|
||||
}
|
||||
+900
-93
File diff suppressed because it is too large.
Load diff
@@ -3,7 +3,7 @@
|
||||
//! Each label carries the company name, the asset tag (its headline), what
|
||||
//! the asset is, its serial number when recorded, and a QR code holding
|
||||
//! exactly the tag. Labels are typeset by `templates/labels.typ` with the
|
||||
//! same bundled Inter fonts as [`crate::pdf`]; the labels reach Typst as
|
||||
//! same bundled fonts as [`crate::pdf`]; the labels reach Typst as
|
||||
//! JSON data (`sys.inputs.data`), never as markup. The sheet geometry comes
|
||||
//! from the label maker's own specification, vendored in
|
||||
//! `docs/spec/sources/avery-5160.md`.
|
||||
@@ -38,7 +38,7 @@
|
||||
//! - A partly used sheet with 7 labels left: skip 23.
|
||||
//! ```
|
||||
|
||||
use crate::pdf::{INTER, PdfError};
|
||||
use crate::pdf::PdfError;
|
||||
use qrcode::render::svg;
|
||||
use qrcode::{EcLevel, QrCode};
|
||||
use std::sync::OnceLock;
|
||||
@@ -145,7 +145,7 @@ fn engine() -> &'static TypstEngine<TypstTemplateMainFile> {
|
||||
ENGINE.get_or_init(|| {
|
||||
TypstEngine::builder()
|
||||
.main_file(TEMPLATE)
|
||||
.fonts(INTER)
|
||||
.fonts(crate::fonts::typst_fonts())
|
||||
.build()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@
|
||||
//! - [`pdf`]: generate PDF financial statements (CPA handoff).
|
||||
//! - [`asset_register`]: the fixed asset register as XLSX and PDF.
|
||||
//! - [`labels`]: print asset label sheets with QR codes.
|
||||
//! - [`fonts`]: the bundled fonts the PDFs and the viewer share.
|
||||
//!
|
||||
//! All money stays `Decimal`; no floats in money paths.
|
||||
|
||||
pub mod asset_register;
|
||||
pub mod bank_csv;
|
||||
pub mod chart_csv;
|
||||
pub mod fonts;
|
||||
pub mod iif;
|
||||
pub mod labels;
|
||||
pub mod pdf;
|
||||
@@ -24,7 +26,10 @@ pub mod xlsx;
|
||||
pub use asset_register::{asset_register_pdf, asset_register_xlsx};
|
||||
pub use bank_csv::{BankCsvError, BankTransaction, DateOrder, parse_bank_csv, parse_bank_csv_with};
|
||||
pub use chart_csv::{ChartRow, parse_chart_csv};
|
||||
pub use iif::{IifAccount, IifError, QbAccountType, iif_accounts, journal_to_iif};
|
||||
pub use iif::{
|
||||
Iif, IifAccount, IifChange, IifError, IifField, LeftOut, MEMO_MAX, NAME_MAX, NUMBER_MAX,
|
||||
QbAccountType, iif_accounts, journal_to_iif,
|
||||
};
|
||||
pub use labels::{AssetLabel, LabelSheet, asset_labels_pdf};
|
||||
pub use pdf::{PdfError, statement_pdf};
|
||||
pub use report::{Member, ReportMeta, Row, RowKind, Statement, YearEarnings, format_amount};
|
||||
|
||||
+121
-10
@@ -4,10 +4,14 @@
|
||||
//! rows to `templates/statement.typ` as JSON data (`sys.inputs.data`) and
|
||||
//! renders the PDF. Amounts arrive already split by
|
||||
//! [`crate::report::amount_parts`] — Typst never does arithmetic on money,
|
||||
//! and labels are data, never markup. The font (Inter, SIL OFL 1.1; see
|
||||
//! `fonts/Inter-LICENSE.txt`) is bundled, so output doesn't depend on
|
||||
//! what's installed, and it is deterministic: the same statement and
|
||||
//! report metadata render to the same bytes.
|
||||
//! and labels are data, never markup. The fonts are bundled, so output
|
||||
//! doesn't depend on what's installed, and it is deterministic: the same
|
||||
//! statement and report metadata render to the same bytes. Inter sets
|
||||
//! the text; the Noto fallbacks ([`crate::fonts`]) draw what Inter can't:
|
||||
//! Chinese, Japanese, Korean, Arabic, Hebrew, Thai and Devanagari, so a
|
||||
//! vendor or an account named in them prints as written, right-to-left
|
||||
//! where it is written so, never as empty boxes. (Emoji have no font
|
||||
//! here: an emoji prints as an empty box.)
|
||||
|
||||
use crate::report::{ReportMeta, RowKind, Statement, amount_parts};
|
||||
use std::sync::OnceLock;
|
||||
@@ -42,11 +46,6 @@ pub enum PdfError {
|
||||
}
|
||||
|
||||
static TEMPLATE: &str = include_str!("../templates/statement.typ");
|
||||
pub(crate) static INTER: [&[u8]; 3] = [
|
||||
include_bytes!("../fonts/Inter-Regular.ttf"),
|
||||
include_bytes!("../fonts/Inter-SemiBold.ttf"),
|
||||
include_bytes!("../fonts/Inter-Bold.ttf"),
|
||||
];
|
||||
|
||||
/// The engine, built once: parsing the bundled fonts is the expensive part.
|
||||
fn engine() -> &'static TypstEngine<TypstTemplateMainFile> {
|
||||
@@ -54,7 +53,7 @@ fn engine() -> &'static TypstEngine<TypstTemplateMainFile> {
|
||||
ENGINE.get_or_init(|| {
|
||||
TypstEngine::builder()
|
||||
.main_file(TEMPLATE)
|
||||
.fonts(INTER)
|
||||
.fonts(crate::fonts::typst_fonts())
|
||||
.build()
|
||||
})
|
||||
}
|
||||
@@ -124,6 +123,10 @@ fn typeset(st: &Statement, meta: &ReportMeta) -> Result<PagedDocument, PdfError>
|
||||
/// ("#panic()", "*", "$", "[") prints as text and NEVER runs.
|
||||
/// 3. Every row reaches the page: template_data carries each row's label,
|
||||
/// kind, and amounts split by amount_parts, in order.
|
||||
/// 4. Every character prints as itself when a bundled font has it:
|
||||
/// Chinese, Japanese, Korean, Arabic, Hebrew, Thai and Devanagari
|
||||
/// from the Noto fallbacks (crate::fonts), Arabic and Hebrew right to
|
||||
/// left, never an empty box.
|
||||
/// ```
|
||||
pub fn statement_pdf(st: &Statement, meta: &ReportMeta) -> Result<Vec<u8>, PdfError> {
|
||||
let doc = typeset(st, meta)?;
|
||||
@@ -221,4 +224,112 @@ mod tests {
|
||||
assert!(doc.pages().len() > 1, "200 rows should span pages");
|
||||
assert!(statement_pdf(&st, &meta()).unwrap().starts_with(b"%PDF"));
|
||||
}
|
||||
|
||||
/// Each text run on the pages: its text, its font's family, and its
|
||||
/// glyphs' ids in that font (0 is the font's empty box, "notdef").
|
||||
fn runs(doc: &PagedDocument) -> Vec<(String, String, Vec<u16>)> {
|
||||
use typst::layout::{Frame, FrameItem};
|
||||
fn walk(frame: &Frame, out: &mut Vec<(String, String, Vec<u16>)>) {
|
||||
for (_, item) in frame.items() {
|
||||
match item {
|
||||
FrameItem::Group(g) => walk(&g.frame, out),
|
||||
FrameItem::Text(t) => out.push((
|
||||
t.text.to_string(),
|
||||
t.font.font().info().family.clone(),
|
||||
t.glyphs.iter().map(|g| g.id).collect(),
|
||||
)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for p in doc.pages() {
|
||||
walk(&p.frame, &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// INVARIANT 4: a company, an account and a vendor named in Chinese,
|
||||
/// Japanese and Korean print in the CJK fallback, every glyph a real
|
||||
/// one; the rest stays Inter. (With Inter alone, every one of these
|
||||
/// was glyph 0, an empty box.)
|
||||
#[test]
|
||||
fn chinese_japanese_and_korean_print_as_written() {
|
||||
let mut m = meta();
|
||||
m.company = "北京 Trading 株式会社".to_string();
|
||||
let mut st = profit_and_loss(&pl());
|
||||
st.rows.push(Row {
|
||||
kind: RowKind::Line,
|
||||
label: "6100 東京 서울 ありがとう ABC".into(),
|
||||
amounts: vec![Some(Decimal::ONE)],
|
||||
currency: false,
|
||||
rule_above: false,
|
||||
});
|
||||
let runs = runs(&typeset(&st, &m).unwrap());
|
||||
let mut seen = String::new();
|
||||
for (text, family, glyphs) in &runs {
|
||||
assert!(
|
||||
!glyphs.contains(&0),
|
||||
"{text:?} has an empty box in {family}"
|
||||
);
|
||||
let cjk = text.chars().any(|c| {
|
||||
matches!(u32::from(c), 0x1100..=0x11FF | 0x3000..=0x9FFF | 0xAC00..=0xD7AF | 0xFF00..=0xFFEF)
|
||||
});
|
||||
let ascii = text.is_ascii();
|
||||
if cjk {
|
||||
assert_eq!(family, "Noto Sans CJK SC", "{text:?}");
|
||||
seen.push_str(text);
|
||||
} else if ascii {
|
||||
assert_eq!(family, "Inter", "{text:?}");
|
||||
}
|
||||
}
|
||||
for want in ["北京", "株式会社", "東京", "서울", "ありがとう", "ABC"] {
|
||||
assert!(seen.contains(want), "{want} printed: {runs:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// INVARIANT 4 for the other bundled scripts: Arabic, Hebrew, Thai and
|
||||
/// Devanagari each print in their own Noto fallback, every glyph a
|
||||
/// real one (the right-to-left reading is checked on the PDF itself,
|
||||
/// `tests/spec_import.rs` `pdf_prints_every_bundled_script_as_written`).
|
||||
#[test]
|
||||
fn arabic_hebrew_thai_and_devanagari_print_as_written() {
|
||||
let mut m = meta();
|
||||
m.company = "شركة Example".to_string();
|
||||
let mut st = profit_and_loss(&pl());
|
||||
for label in ["مكتب", "שלום", "สวัสดี", "नमस्ते"] {
|
||||
st.rows.push(Row {
|
||||
kind: RowKind::Line,
|
||||
label: label.into(),
|
||||
amounts: vec![Some(Decimal::ONE)],
|
||||
currency: false,
|
||||
rule_above: false,
|
||||
});
|
||||
}
|
||||
let runs = runs(&typeset(&st, &m).unwrap());
|
||||
let family_of = |c: char| match u32::from(c) {
|
||||
0x0600..=0x06FF => Some("Noto Sans Arabic UI"),
|
||||
0x0590..=0x05FF => Some("Noto Sans Hebrew"),
|
||||
0x0E00..=0x0E7F => Some("Noto Sans Thai"),
|
||||
0x0900..=0x097F => Some("Noto Sans Devanagari"),
|
||||
_ => None,
|
||||
};
|
||||
for (text, family, glyphs) in &runs {
|
||||
assert!(
|
||||
!glyphs.contains(&0),
|
||||
"{text:?} has an empty box in {family}"
|
||||
);
|
||||
if let Some(want) = text.chars().find_map(family_of) {
|
||||
assert_eq!(family, want, "{text:?}");
|
||||
}
|
||||
}
|
||||
// Every letter printed in its script's font.
|
||||
for c in "شركةمكتبשלוםสวัสดีनमस्ते".chars() {
|
||||
let want = family_of(c).unwrap();
|
||||
assert!(
|
||||
runs.iter().any(|(t, f, _)| f == want && t.contains(c)),
|
||||
"{c} printed in {want}: {runs:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
#let firm = 0.6pt + ink
|
||||
|
||||
#set document(title: d.company + " — " + d.title, author: d.company)
|
||||
#set text(font: "Inter", size: 8pt, fill: ink, number-width: "tabular", hyphenate: false)
|
||||
#set text(font: ("Inter", "Noto Sans CJK SC", "Noto Sans Arabic UI", "Noto Sans Hebrew", "Noto Sans Thai", "Noto Sans Devanagari"), size: 8pt, fill: ink, number-width: "tabular", hyphenate: false)
|
||||
#set page(
|
||||
paper: "us-letter",
|
||||
flipped: true,
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
#let quiet = luma(90)
|
||||
|
||||
#set document(title: d.company + " — Asset labels", author: d.company)
|
||||
#set text(font: "Inter", fill: ink, number-width: "tabular", hyphenate: false)
|
||||
#set text(font: ("Inter", "Noto Sans CJK SC", "Noto Sans Arabic UI", "Noto Sans Hebrew", "Noto Sans Thai", "Noto Sans Devanagari"), fill: ink, number-width: "tabular", hyphenate: false)
|
||||
#set par(leading: 0.28em, spacing: 0pt)
|
||||
#set page(width: g.page_width * pt1, height: g.page_height * pt1, margin: 0pt)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
#let firm = 0.6pt + ink
|
||||
|
||||
#set document(title: d.company + " — " + d.title, author: d.company)
|
||||
#set text(font: "Inter", size: if dense { 7pt } else { 9pt }, fill: ink, number-width: "tabular")
|
||||
#set text(font: ("Inter", "Noto Sans CJK SC", "Noto Sans Arabic UI", "Noto Sans Hebrew", "Noto Sans Thai", "Noto Sans Devanagari"), size: if dense { 7pt } else { 9pt }, fill: ink, number-width: "tabular")
|
||||
#set page(
|
||||
paper: "us-letter",
|
||||
flipped: wide,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user