* i18n: Traditional Chinese interface draws hanzi with an installed TC font
The zh-hant interface shared the Simplified Chinese font order, so its first
CJK face was the embedded Noto Sans CJK SC and traditional characters were
drawn with mainland glyph shapes (說, 資, 這, 為, 戶).
On the desktop, a Traditional Chinese interface now puts an installed
Traditional Chinese font before every CJK face: PingFang TC on macOS,
Microsoft JhengHei on Windows, Noto Sans CJK TC on Linux. Noto Sans CJK SC
stays after it as the fallback. Without an installed TC font, and on wasm,
the order is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Credits: add TobyWu666 to contributors/people.toml
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: find Traditional Chinese fonts at the root of a font folder
Linux user font folders (~/.fonts, ~/.local/share/fonts) often hold font
files directly; the Traditional Chinese candidates only named files in
distribution subfolders, so those fonts were missed (CodeRabbit review).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: skip the Traditional Chinese font test without embedded catalogs
Since the catalogs come from craft-i18n (#4072), a build without
CRAFT_I18N_DIR is English only, so the test now skips like the other
catalog tests. Also rustfmt hant_font_chars.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Each shaping call let harfrust compile a new shape plan (the face's
lookups for the run's script, direction, language and features), which
took about as long as shaping a short run. Each thread now keeps up to 64
plans (the oldest dropped to make room), found by face and harfrust's
ShapePlanKey::matches, and passes the plan to the shaper. A kept plan is
only used for the face it was made for: the face id (from one process-wide
counter, so no two faces share it) together with the font bytes' address,
length and face index. The plan is the one the shaper would have made for
the buffer, so the glyphs are the same; tests shape with kept and fresh
plans and compare, and check the per-thread bound.
PDF export of the export_bench document (259 pages), interleaved with main
(A/A within 1 %): median 421 ms -> 319 ms (-24.2 %), measured on a loaded
machine; on a quieter run earlier 294 ms -> 224 ms (-23.8 %). PNG export of
page 1 (which lays out the whole document): about -34 %. The PDF and PNG
bytes are identical to main's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shortcuts that give the same answers as the lookups they skip:
- is_rtl: nothing before U+0590 is right to left, so Latin, Greek and
Cyrillic text skips the bidi class search.
- strong_rtl and is_mark: ASCII letters are the only strong ASCII
characters, and no ASCII character is a mark.
- cluster_starts: in ASCII text every character starts a grapheme except LF
after CR, and no ASCII character joins a cluster.
- display lines: a glyph run's vectors and text are sized up front, and the
symbol-font check is made once per run instead of once per cluster.
- PDF glyph runs: the baseline (the y most glyphs share) is only worked
out for italic runs, the only ones that use it, and a run on one baseline
returns it without counting.
Tests check each shortcut against the full lookup (every code point for the
bidi classes, every ASCII pair for the clusters).
PDF export of the export_bench document, interleaved with main: median
293 ms -> 241 ms (-18 % with the previous commit). The PDF bytes are
identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Noto Sans Thai and its siblings have no Latin letters or digits, so on a
machine whose only Thai fonts are Noto no face covers the test run and
fallback_for_run rightly returns None. The guard now asks whether the Thai
substitute covers the whole run, which doesn't go through fallback_for_run.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
named_instances_and_collection_members_are_capped: 2 s -> 10 s. The face counts in the test
(one face for a million identical members, MAX_FILE_FACES from add_font) are what catch the old
code, which built a face per member; the time (under 0.1 s on a loaded machine, so 10 s is over
100x) only backs up "read once".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
The hinting breaker stays wall-clock: skrifa's interpreter budget is fixed,
counts instructions rather than the points each one moves, and reports no
work count, so there is nothing deterministic to bound. The comment now says
the result can depend on the machine by design, and that only faces needing
the interpreter for their contours are hinted at all. Cargo.toml notes that
brotli-decompressor is the crate and version wuff already uses: pure Rust,
forbid(unsafe_code) by default, wasm-ready, BSD-3-Clause/MIT.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
CodeRabbit on #3869: two concurrent install_font calls could pick the same
destination and temporary, and the leftover-unlink retry could remove the
other call's open temporary. write_unique now holds a process-wide lock.
In-process only (noted in a ponytail comment).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
Ultrareview finding 5: the slow-draw count only grew, so three draws slowed
by a busy machine at any point in a session switched hinting off until
restart, breaking hint-reliant faces (MingLiU, DFKai-SB). Hinting now stops
only when at least three draws were slow and they are at least one in 16 of
the face's hinted draws: a face slow throughout still stops at its third
glyph, and slow glyphs spread among fast ones are still bounded. Counting
interpreter work instead of wall-clock stays with #3827.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CodeRabbit review of finding 62's fix: one hinted draw slowed by a busy
machine (scheduler pause, page fault) switched hinting off for the face for
the rest of the process, leaving hint-reliant faces with timing-dependent
missing strokes. A strike counter now drops hinting only at the third slow
glyph.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Finding 62 (partial): skrifa 0.47 caps a hinting run at 1M instructions
but not the points each moves (SHZ, IUP... walk a whole zone), and a font
opts into the interpreter by its family name. Faces asking for a
twilight zone over 1,024 points or a stack over 4,096 values get no
interpreter; a glyph whose hinting buffer passes 256 KiB is drawn
unhinted; and (native) one hinted glyph over 50 ms turns hinting off for
its face. Residual: a face within the limits can still spend about
1M x zone size point moves on one glyph (and its prep program) before
the strike lands; the full fix is a point-work budget in skrifa or an
isolated worker with a deadline.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Finding 61: Recorder copied every gradient's stop list (and every clip
glyph's outline) per op, capped only by op count, so a compact COLR graph
reusing one long stop list could expand to billions of stops. Stops and
path elements now share MAX_PAYLOAD (65,536) per glyph, charged before
copying; past it the glyph falls back to its outline and no further clip
outlines are built. The render cache charges each glyph's real ops,
stops and path bytes (ops_bytes) instead of a flat 64 bytes per op.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Finding 59: enumerate_faces took a collection's member count straight from
its header (a TTC can list one table directory millions of times) and
font_faces read every fvar named instance (up to 65535, with a linear
style dedupe and a name-table lookup each). Every entry became a FontFace
kept for the session, and add_fonts then checked each new face against
every face in the database.
Now a file yields at most 256 faces (MAX_FILE_FACES; user fonts ask for
one more so they can still be refused as "more than 256 faces"), named
instances are read to the same cap, a face with the family and style of
an earlier one in the file is skipped (repeated collection offsets), and
add_fonts dedupes against an indexed set.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Finding 46: install_font logged the caller-supplied file name raw when the
font couldn't be stored, and the desktop log writes line breaks as they
come, so a name could forge extra log records. The name is now logged
with {:?} (escaped and quoted), and so are the font-derived family names in
the two glyph-outline warnings, which come from a document's own fonts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Findings 39 and 70.
39: wuff inflates each WOFF 1.0 table into a buffer sized from the
directory's origLength, before anything checks it against our 128 MB
cap, so a tiny file could ask for gigabytes. The directory's table sizes
are now summed first and a file over MAX_DECODED_BYTES is TooLarge.
70: wuff's WOFF2 glyf reconstruction allocates a glyph's points from its
nPoints counts and checks the 65536-point limit only afterwards. The
brotli stream is now decompressed by us (brotli-decompressor, already in
the tree through wuff) and every transformed glyf table's per-glyph point
totals are checked before wuff unpacks them; a glyph over the limit (which
wuff would refuse anyway) is refused up front.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SonarCloud's analysis of main at b4da59f0c lists 23 closures such as `|v| v.as_slice()` that hand their argument straight to one method. This passes the method itself: `Vec::as_slice`, `str::to_ascii_lowercase`, `f64::total_cmp` and the like, all forms the repo already uses elsewhere. Behaviour is unchanged.
Five stay. The four in crates/proof/src/es/mod.rs sit in code that #4191 replaces, and its replacement already uses `PoisonError::into_inner`. In crates/engine/src/io/safe_save.rs the path would be `<dyn std::error::Error + Send + Sync>::is::<Guided>`, which reads worse than `|inner| inner.is::<Guided>()`.
* chore: resolve SonarCloud maintainability issues
* fix: restore compiling refactors, revert behaviour changes, run cargo fmt
- fontdb::norm: keep the closure for filter (char::is_alphanumeric takes
self, not &self, so it does not fit Iterator::filter).
- story.rs: write_mark_rpr needs &mut self (rev_attrs, rpr_change).
- Revert the viewport meta change and the return 0 added to
workspace_version/sha256, which changed behaviour or masked failures.
- cargo fmt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Import Document in odt page_parts after dropping glob re-export
* fix: apply_kind_properties binds ContentControl; cell_ref keeps char closures
* fix(engine): pass case helpers by value (clippy needless borrows)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
Pin craft-fonts a71c20e3 in ci.yml, freebsd.yml and release.yml; web-craft-fonts so the web
build keeps the Geor face; the interface font definitions add it; the Thai run test
accepts the embedded Thai face that pin brings (#3561).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF
ROADMAP.md: status line once (this change first, then main's); progress-log and history rows union in date order
docs/layout-parity.md: status line once (this change first, then main's); history rows union in date order
Word for Mac spaces Symbol lines at 1.222 em (measured at 11 and 20 pt)
even with Apple's Symbol installed, whose own line is one em. Symbol's
metrics now apply when the resolved face is one em tall as well as when
the font is substituted (fonts::word::symbol_line).
fallback_for_run cached its answer under the face count read after the
search. A face loaded in between by another thread (its own search, or a
family a document names) made a miss look current: later lookups at the
new count got the stale miss (review on #3078). The search now returns
the count of the loaded faces its last choice was made from, and the
answer is cached under that; anything loaded since makes the next lookup
search again. No lock is added, so cached lookups from parallel layout
stay contention-free.