* 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>
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.
A missing Khmer OS font drew in whichever loaded Khmer font covered the text, usually a
wider Khmer OS face, so lines wrapped and fixed-height text boxes hid their last line.
Word uses the system Khmer font; so do we now.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Two holes in the whole-run fallback search (review on #3078):
- The coverage index keeps one coverage per file, so a font collection
whose faces only together map a run matched it; the search loaded that
family and stopped, returning no face even when a later font covered
the run. It now goes on to the next file that maps the run.
- A single-character fallback may have loaded one file of a family (its
Italic, say). The run search then treated the family as loaded and
added only the file it reached first, so a regular run could be drawn
in Bold although the Regular file was installed. It now loads whatever
of the family isn't loaded yet.
Files loaded by family name are now recorded in the index too, so no
search loads a file a second time.
Tests: a split-coverage collection before a covering font; a family
whose Italic a character fallback loaded, reached at its Bold file first.
Both fail without the change (no face; Bold).
On Linux the run fallback loaded the script's preferred family (Noto Sans
Thai, which has no Latin letters) and stopped: every character had a face,
so an installed font mapping the whole run (IBM Plex Sans Thai) was never
tried, and a Thai + Latin run was split across two faces. Three Thai tests
failed on machines with Noto Sans Thai installed.
When no loaded face covers the run, the search now looks through the
coverage index for the first installed file mapping the whole run. It
loads that file's whole family, since path order can reach the Bold
file before the Regular. It also looks at the loaded faces again even
when it loaded nothing itself: a concurrent search may just have
loaded the font and taken its files off the index, and the miss would
otherwise be cached.
Regression test: a_run_no_preferred_face_covers_whole_loads_an_installed_font_that_does
(synthetic fonts, independent of what is installed).
* #2240: fonts Word embeds as subsets lay out the text set in them
Embedded fonts were registered under the family inside the font file. Word
embeds a subset ("embed only the characters used") under a made-up family
(___WRD_EMBED_SUB_46) and the text asks for it by the font table's name
(OBRBWW+Century Schoolbook), so it still fell back to a substitute. Each
embedded file now comes with its font table name, and its faces go by that
name when they don't already.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #2240: the black-circle fallback test passes its font's table name
main's new `circle_checks_face_coverage` test (#2582) called `add_document_fonts` with bare font
bytes; with this branch each embedded font comes with the name the document's font table gives
it, so the test passes "Times New Roman" alongside the face it renames to it.
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>
Old releases of heavy faces (the core-fonts Arial Black) claim weight 400 in
OS/2, so bold text in them was drawn with synthetic bold. Current releases say
900 and Word draws bold Arial Black unchanged. A static face whose file says less
than semibold now takes the weight its family's last word names: Black/Heavy 900,
ExtraBold/UltraBold 800. Files that already declare a heavy weight keep it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The system font scan (file_face_names) also catalogs faces with none of
the tables skrifa reads glyph outlines from (glyf, CFF, CFF2, VARC) and
none of the color bitmap tables the renderer draws (sbix, CBDT). On
macOS 27.0.1 the scan of /System/Library/Fonts catalogs "GB18030 Bitmap"
(Supplemental/NISC18030.ttf), and the font list shows it. The face
stores its glyphs as bdat bitmaps and maps 27,544 characters.
FontDb::outline returns an empty path for each of them, and the face has
no color glyph tables. The renderer draws none of them.
file_face_names now leaves out a face whose table directory has none of
those six tables. It reuses OUTLINE_TABLES from the interface CJK
fallback (#1250). VectorCraft's font scan checks the four outline tables
(storytold/vectorcraft#781). WordCraft's renderer also draws sbix and
CBDT bitmaps (#1504). Noto Color Emoji has CBDT and none of the outline
tables, and the scan keeps listing it.
A paragraph in a Thai font that isn't installed (TH Sarabun New, Angsana
New, Cordia New, the UPC families…) drew its Thai letters in one fallback
face and the Latin letters, digits and baht sign in a Latin substitute at
a visibly different size. Word substitutes one font (Cordia New) for the
whole run.
Thai families now have their own substitute lists (Cordia New and
Leelawadee UI on Windows, Thonburi on macOS, Noto Sans Thai and the TLWG
fonts on Linux; serif families prefer Angsana New, Sathu or Noto Serif
Thai), and the first listed face with Latin letters and digits as well
as Thai wins. When the substitute still lacks some of the run's Thai or
Latin text, the run moves to one face covering it through
FontDb::fallback_for_run, the generalised East Asian run fallback. Only
Thai fonts get this: a missing Latin font keeps Latin text in its own
substitute, a run with its own complex-script font keeps it for the Thai
letters, and a missing roman-classed Thai font is no longer drawn in
Cambria.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Word sizes a line by the fonts in it, and a bullet in Symbol (Word's
default, U+F0B7) has a taller ascent than Calibri or Times New Roman.
Without Symbol (Linux, macOS without Office, the web build) WordCraft
draws the bullet from another face and used that face's height, so every
bulleted line was shorter than Word's: 14.5 pt against 15.1 pt in
Calibri 11 pt at 1.08 spacing, 14.9 against 15.7 in Times New Roman 12 pt.
symbol_font_metrics gives Symbol's and Wingdings' ascent and descent as
Word's PDFs state them, and a run in one of them that isn't installed
takes them. The label's ascent now comes from its style, so the existing
rule (the label's extra ascent added unscaled) sees them.
Measured in Word 2016: Calibri 11 / Times 12 bullets 15.08 / 15.72 pt,
WordCraft now 15.07 / 15.75 pt. On 251 real documents against Word PDFs,
page breaks at the same word 45.4% -> 50.1%.
#1613 draws a missing font in Cambria when the font table classes it as
roman, as Word does. Times New Roman is roman in every font table, so
where it isn't installed (Linux, the web build) every Times document came
out in Caladea: lines 2.1 pt taller at 12 pt and different line breaks.
Word always has Times New Roman, so it never applies that rule to it.
The rule now leaves alone a font whose substitutes are chosen by its name
(a metric-compatible clone or the same design under another name: Times,
Arial, Courier New, Palatino, the symbol, Hebrew and Persian fonts).
Fonts with no clone of their own (Cambria, Georgia, brand fonts) still
follow it.
Checked on 251 real documents against PDFs from Word: exact page counts
201 -> 229, page breaks in the same place 45.4% -> 51.9%.
Inside a Flatpak, /usr/share/fonts and /usr/local/share/fonts are the runtime's and the home font
folders aren't exposed, so WordCraft listed only the runtime's fonts. Flatpak mounts the host's
system, local and user fonts at /run/host/fonts, /run/host/local-fonts and /run/host/user-fonts
for every app, and its main branch (after 1.19.2) also mounts ~/.fonts at
/run/host/user-fonts-legacy when the XDG user font folder exists too; scan those as well.
Folders that don't exist are skipped, as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fallback put Segoe UI Symbol before Segoe UI Emoji, and Symbol has
monochrome glyphs for some emoji (✅), so they drew in black and white
and tests_color_glyphs failed on Windows. Characters with default emoji
presentation now rank the colour emoji fonts first, both when loading a
system fallback and when choosing among loaded faces.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(web): add option to load system installed fonts in dropdown
Add explicit 'Load System Installed Fonts…' button in font dropdowns for web (Wasm).
- Dispatches font.loadSystem via transient user activation on click
- Calls window.queryLocalFonts() to enumerate and load installed fonts into FontDb
- Invalidates font family cache in FontPreviews upon new font installation
- Complete localization across all 18 languages
- Credit GhostMitra in contributors/people.toml
* fix(ui, engine): canvas focus after chrome click and checked file access in sniff
* Revert "fix(ui, engine): canvas focus after chrome click and checked file access in sniff"
This reverts commit b0c925bf79.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With no system fonts (a headless CLI), Arabic and Persian text drew as
empty boxes: the document font database only loaded the craft-fonts
Japanese and Chinese faces. Load every non-Japanese craft-fonts face.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Fonts: the interface CJK fallback skips faces without outlines
The interface CJK fallback has listed PingFangUI.ttc from
FontServices.framework as its last macOS candidate since #495. The skrifa
crate reads glyph outlines only from the glyf, CFF, CFF2 and VARC tables.
On macOS 27.0.1 the 32 faces in PingFangUI.ttc have none of them and keep
their outlines in Apple's hvgl table. pick_face counts coverage from each
face's cmap alone. The file's first face maps all 1,114 CJK characters of
the zh-hans and zh-hant catalogs. When no earlier candidate maps all of
them, find_ui_font returns that face for a Chinese interface built without
craft-fonts, and the interface would draw the characters it takes from
that face as blank space.
pick_face now skips a face with none of those four tables, and find_ui_font
goes on to the next candidate. VectorCraft's font scan has the same check
(storytold/vectorcraft#781).
* Contributors: add dotCooCoo (Robert Lee)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
#1052's test asserted that every Hebrew font falls back to David, but
only the Windows fallback list has it, so it failed on Linux and macOS.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fonts: Add platform-specific Hebrew font support
Add proper Hebrew font substitution and fallback for Windows, macOS, and Linux:
- Windows: Prioritize David/Miriam (Office) and Segoe UI fonts
- macOS: Prioritize Helvetica and Geeza Pro (system defaults)
- Linux: Use Noto Sans Hebrew and open-source fonts
Fixes Hebrew character display issue (showing rectangles) when editing
text in fonts without Hebrew glyphs by ensuring the system falls back
to Hebrew-capable fonts. Includes platform-optimized font lists in
both document font resolution (word.rs) and RTL text fallback search
(fontdb.rs).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Old Russian and Central European documents name fonts by their Windows 3.1
charset aliases: "Times New Roman Cyr", "Arial CYR", "Courier New CE",
"Arial (Hebrew)". Windows maps these to the font itself (FontSubstitutes);
we treated them as unknown families and fell back to the sans default.
resolve() now tries the base family, and substitutes for it when it isn't
installed (Times New Roman Cyr -> Liberation Serif, like Times New Roman).
Co-authored-by: Магда М.Г. <mail@mgmagda.ru>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Word's symbol fonts are proprietary and never bundled, so text set in them
showed the substitute's letters. New wordcraft_doc::symbol_fonts maps every
code (stored as U+F020-U+F0FF, e.g. w:sym, or as a plain 8-bit code) to its
Unicode equivalent: Symbol from Unicode's Adobe symbol.txt, the Wingdings
and Webdings sets from Unicode L2/12-368 (WG2 N4384), checked against UCD.
Layout shapes the mapped characters (fallback faces draw them); PDF text,
copy as text and TXT export read as the symbols; the stored text and docx
round trips are unchanged.
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #848: font list previews no longer load every family they show
The Font box drew each entry in its own face through the full layout
pipeline, which loaded the whole family into the font database for good
(faces are never unloaded). Every trip through the list kept tens of
megabytes. Entries are now drawn with FontDb::preview_face: a loaded
family's face is shared, an installed one is read into a face the
database doesn't keep and is freed when dropped, and render_text_line
draws it without the shared outline cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Font preview test tolerates other tests loading fallbacks; gaps rows 1–3 de-duplicated, #848 noted in row 3
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Builds with craft-fonts (releases, the FreeBSD job) put only its Japanese
faces in the document font database; Noto Sans CJK SC served the interface
alone. Simplified-only ideographs in a document then had no face unless the
system had one, and the run-level CJK pick from #567 found no face covering a
mixed run, which the FreeBSD job's
east_asian_run_uses_one_face_covering_the_whole_run caught. The Chinese craft
faces now follow the Japanese ones (Mincho stays first for Japanese text).
fallback_reads_each_installed_font_file_once_however_many_characters_miss
used U+30001.., part of which Noto Sans CJK SC maps (CJK Extension G); it now
uses Supplementary Private Use Area-A, which no font here maps.
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Fix CJK fallback mixing serif/sans faces within one run
Per-character fallback picked the first covering face in load order, so a
run without an East Asian font could draw JIS-covered ideographs in a
bundled Japanese Mincho and simplified-only ones in the SC sans, mixing
serif and sans glyphs even inside one word. Pick one face covering all
of the run's East Asian characters when available, keeping the
per-character fallback when none does.
* Review fixes for #567: cache the run-level CJK fallback
- fallback_for_east_asian is cached per (character set, excluded face,
bold, italic) and dropped when the loaded faces change or the system
fallback is switched, like the per-character cache, so relayout doesn't
redo the search; the cache is bounded in entries and in key size.
- loaded_fallback and loaded_covering share one fallback_order helper.
- Test for the cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>