normal_track read the trak size table at a file-controlled u32 offset with unchecked additions,
which wrap on 32-bit targets such as wasm32 (a debug panic, or reads at the wrong offset). Its
reads are now checked, and an incomplete normal track is dropped entirely, never returned in part.
index_bytes was already bounded by fill(), which checks each range before the flagged additions
run; this makes those bounds explicit, still skipping damaged faces and reading later faces' name
tables without their outlines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
Fonts name a family by its typographic name and by a legacy name per weight; fonts without typographic names are installed under the legacy ones only. FontDb::resolve finds either from the other, and matches names across case, spaces, hyphens and underscores; every lookup (import, paste, REST import, rendering, missing fonts, the font report) uses it. A weight-number style goes by that weight, and .fig and REST imports wait for the installed fonts to be indexed.
Where the browser lists them (window.queryLocalFonts, Chromium), the font menu offers Use fonts installed on this computer; the fonts join the font database by name and each style's bytes are read only when text uses it, then auto-sizing text refits. Rust only (js-sys/web-sys). Fixes#1051.
A missing family the suite's font store has (Google Fonts, Noto) gets a
Download button. The first one asks the user to allow downloading fonts,
then craft-libs' craft-font-store installs pinned, checksum-verified files
into the Craft apps' shared font folder on a thread of its own. The font
scan reads that folder; once installed the text fits the real font.
font.download / font.downloads drive the store as its agent: refused while
downloading is off, never able to turn it on. Desktop only.
`system-ui`, `-apple-system`, `BlinkMacSystemFont` and
`.AppleSystemUIFont` join the names `apple_alias` resolves to the
installed System Font, as SF Pro and SF UI already do. SVG import keeps
a family the SVG lists when a font answers to its name, so on a Mac
`font-family="-apple-system, system-ui, sans-serif"` keeps
`-apple-system` and draws in System Font; elsewhere the generic
stand-ins apply as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review on #1309 pointed out that core-text 20.1's get_descriptors retains
the array it gets under the create rule a second time, so every scan kept
one array of font descriptors alive. Checking that turned up a larger leak:
Core Text autoreleases objects while it builds the collection, and the scan
runs where no autorelease pool is active (the app's background scan
thread, the CLI, the MCP server), so nothing ever drained them.
Measured with `figcraft-cli run` of N `text.rescanFonts` steps on macOS 15
(peak memory footprint):
| | 10 rescans | 200 rescans |
|---|---|---|
| main, without this PR | 221 MB | 220 MB |
| this PR before the fix | 228 MB | 796 MB |
| with the fix | 226 MB | 225 MB |
A standalone Swift loop over the same Core Text calls grows from 9 MB to
759 MB over 200 scans without a pool and stays at 20 MB inside one, so the
pool is what matters; the core-text bump removes the over-retain on top.
- The query runs inside objc2::rc::autoreleasepool. objc2 0.6 is already
in the lockfile (winit's), and the function is safe, so the workspace's
unsafe_code = "forbid" still holds.
- core-text goes to =22.0.0, the first release that takes ownership of the
array correctly. It brings core-foundation 0.10, core-graphics 0.25 and
core-graphics-types 0.2 beside the older versions other crates use. 22.0
moved framework linking behind a `link` feature that is on by default,
so with default features off it is named explicitly.
A rescan takes about 0.2 s instead of 0.08 s on that Mac (266 more font
files are indexed); it only runs when asked for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 7f6a3a4e46)
Font managers such as RightFont, FontBase and Typeface keep fonts in their
own library folders and activate them through Core Text, without copying
them into /Library/Fonts or ~/Library/Fonts. Every other Mac app lists
these fonts; FigCraft walks only the font folders, so it didn't, and a
file using one of them drew in a fallback font.
On macOS the scan now also asks Core Text for the file of every available
font and indexes the files that lie outside the walked folders. All three
scans (the first, the on-demand one and text.rescanFonts) share this, so a
font activated while the app runs is found by a rescan like a newly
installed one.
Core Text needs unsafe FFI, which the workspace forbids, so the query goes
through the safe wrappers of the core-text crate, pinned at =20.1.0. That
version builds on the core-foundation 0.9, core-graphics 0.23 and
foreign-types 0.5 the lockfile already has: core-text is the only new
package. The call sits inside catch_unwind, so anything unexpected from
Core Text leaves the scan with the font folders alone. Other platforms are
unchanged.
On a Mac with RightFont, 15 families from its library are now listed
(Geist, JetBrains Mono, Clash Display and others) that weren't before. A
full font listing went from 0.06 s to 0.20 s on that machine.
Tests: the filter that keeps files outside the font folders once, indexing
a font file found outside them, and (macOS only) that Core Text lists the
system's font files. cargo test --workspace, cargo clippy -p figcraft-text
--all-targets -D warnings and cargo fmt --check pass on macOS 15. CI
builds on Linux only, so the macOS path is checked locally.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5699e7abe6)
Existing text keeps the named instance's optical size, so documents render as before; the
Sketch import stores the size as the text's opsz value instead.
Conflicts resolved keeping both sides: `.sketch` stays in `is_document`
(now via `OsStr::to_str`); ROADMAP and the parity, roadmap and
file-format docs keep main's headers and rows with the `.sketch` notes
and history rows added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Release builds embed some families with only a few styles (Poppins
Regular, Italic and Bold), and the first font scan dropped every
installed face of such a family, so an installed Poppins SemiBold or
Medium was never offered. The first scan now keeps the embedded faces,
which still win for the styles they cover, and offers the installed
styles they lack beside them, as a rescan already did.
Closes#1246
* chore: resolve SonarCloud maintainability issues
- Define SHA256_FMT constant in packaging script to remove duplicated string literal (shelldre:S1192)
- Remove redundant 'static lifetime annotations from const items (rust:S8863)
- Replace closures with method and function pointers (rust:S1612)
- Extract helper functions in synthetic document generation, bezier polyline flattening, and component restoration to reduce cognitive complexity (rust:S3776)
* fix(packaging): use verify_sha256 helper function to avoid shellcheck SC2059
* fix(engine): remove duplicate closing brace in large_document
* fix(engine): take &mut Document in restore_id_map; keep 'static on associated consts
restore_id_map allocates ids, so it needs a mutable document. Elided
lifetimes in associated consts trip elided_lifetimes_in_associated_constant,
so those annotations are restored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Assign positional parameters to locals in sha256 verification
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
Thai typed into a layer whose font has no Thai glyphs (Inter, which new
text starts in) was drawn as missing-glyph boxes, on the desktop and on
the web, even with a Thai face embedded or installed: FALLBACK_FAMILIES
listed Latin, CJK, Hangul, Arabic and symbol families but no Thai one.
Noto Sans Thai, Noto Sans Thai UI, Leelawadee UI, Thonburi and Tahoma go
after Noto Sans Arabic; they are only consulted for characters the
layer's own font lacks. Tests: the list has a Thai face for each
platform, and with one installed or embedded every Thai cluster in a
Latin layer, marks included, is drawn from it (skips without one).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signs such as U+2316 and U+2912 are in no text face. The fallback list
had the macOS and Windows symbol fonts (Apple Symbols, Segoe UI Symbol)
but nothing a Linux install has, so they drew as empty boxes there even
with Noto Sans Symbols 2 or Noto Sans Math installed. Adds Noto Sans
Symbols, Noto Sans Symbols 2 (and the Symbols2 spelling some packages
use) and Noto Sans Math after them.
- --no-system-fonts holds for .sketch imports too: the CLI opts the
process out, so the import's font matching doesn't scan.
- Shared text styles get the installed family and platform tracking
their linked layers get.
- Each text run writes its letter spacing, line height and font axes,
neutral when it has none, instead of taking the first run's.
- The unsupported-drop hint and its catalogs list .sketch.
- A test sends damaged .sketch data through file.open and file.import.
- Stack items are matched to their source layers through a map.
- Docs: .sketch is a convert input; the Carbon kit's Sketch 47 edition
is refused (the 68 and 83 ones open).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The font database knows each face's PostScript name; the Sketch import
finds the installed face a file's font names (CourierNewPSMT is Courier
New Regular) and reads the family from the name only when it isn't
installed. Text without a fixed line height takes the line Sketch draws:
ascent and descent rounded to whole pixels, the line gap left out, a
fifth more for classic Mac fonts, the baseline where Sketch puts it
(measured on sketchtool renders of 80 fonts).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compared with Sketch's renders of a Sketch 64 file. Resized instances lay
out their layers the way Sketch does (measured: pins share the change in
proportion, a fixed size keeps its centre's share, and files before format
110 just scale); masters' backgrounds show in instances only when Sketch
includes them; swaps to a missing symbol keep the original; artboards
don't draw a style they carry. Apple's system fonts are found under the
names designs use (SF Pro, SF UI Text and Display, SF Compact), the
optical size follows the text size for named styles too, and imported
text keeps the platform's trak tracking as letter spacing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- text.rescanFonts offers a loaded family the files it hasn't read (styles
installed since) and refits every family that gained faces; `added` still
lists only the families that were unknown
- text.refit refuses an `ids` that isn't a list instead of refitting the
whole document
- text.loadFont with a file already loaded returns its families without
keeping its bytes or adding its faces again
- doc comments on the functions this change touches
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The system fonts are indexed once per process, so a font installed while
FigCraft (or `figcraft-cli mcp`) runs stayed missing until a restart.
- text.rescanFonts walks the font folders again and offers the families
installed since (loaded families keep their faces) → {added}
- text.loadFont {path | data} adds a font file without installing it → {families}
- both clear the layout cache so text lays out again in the new face
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Current macOS keeps PingFang and other on-demand fonts in its asset
store, which the font scan never walked, so Chinese text showed as
boxes. The scan now also walks the com_apple_MobileAsset_Font* folders
of /System/Library/AssetsV2 (and the older /System/Library/Assets) with
the same depth, size and file checks, and Hiragino Sans GB and Heiti SC
(preinstalled) join the Chinese UI faces and the text fallbacks.
Tested with a folder laid out the same way; not run on a Mac.
From review:
- The check ran after any file.open/openRecent/import result, so a
pasted or dropped image, reopening a file that was already open, or
Clear recent brought the window back. It now runs only for results
with a `summary` (a file opened, or pages imported), and never in the
browser.
- Text > Missing fonts... before the startup scan finished listed
installed fonts as missing. It now says it is checking the installed
fonts and lists them once that is done
(`figcraft_text::installed_fonts_known`, also true when system fonts
are opted out, where checks used to wait forever).
- ROADMAP progress log and file-format-parity status line.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Text in a family that isn't installed was drawn in a fallback font
without a word. Now:
- `text.missingFonts` lists the families a document names that no
installed or built-in font provides: text layers and their styled
ranges, text styles, and string variables bound to a font family
(every mode, through aliases), with the number of text layers.
- The desktop app shows a Missing fonts window after the user opens a
file that has any: once installed fonts are indexed (before that every
one of them would count as missing) and after any import report is
closed. Files named on the command line count. Programmatic opens and
the browser build never open it by themselves. Text > Missing fonts...
shows it for the file in front; Escape closes it.
- `figcraft-cli info` prints `missing font` lines (`missingFonts` with
--json).
- file-format-parity: the row claimed REST import warnings that no code
produced.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fonts activated through Adobe Fonts rendered in the fallback font without
a warning: Creative Cloud keeps them in its own folder as hidden files
named by number, which the font scan never read. The macOS scan now
includes ~/Library/Application Support/Adobe/CoreSync/plugins/livetype,
and files without an extension are indexed when they start with a font
signature. Fonts are read in place.
Closes#298
Text in the default Inter (or any Latin font) drew the missing-glyph box for every Hangul
syllable on macOS and Windows: no Korean family was on the fallback list, so the Korean
fonts those systems ship were never tried, and there is no default-font setting to work
around it. Apple SD Gothic Neo (macOS), Malgun Gothic (Windows) and Noto Sans CJK KR /
Noto Sans KR (Linux) now come after the Chinese and Japanese entries, so Han characters
resolve as before.
Fallback faces also follow the run's style: each fallback family gives its face nearest the
run's style instead of always its Regular, so Hangul (or kana) in a bold run comes out bold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MAPjgJo17KasfbzMTbV65
Require resolved font entries to be regular files before indexing them. Preserve normal font symlinks and cover named pipes in a bounded child-process regression.
Assisted-by: OpenAI Codex (GPT-6)
Signed-off-by: IanHollow <72767437+IanHollow@users.noreply.github.com>
"Placeholder rectangles" was a claim about Apple Color Emoji's glyf
outlines that no source I can show supports: the font's sbix chapter
documents that most emoji are bitmaps while the flags are drawn from
outlines, and it is Noto Color Emoji that has no outlines at all. The
comments, the ROADMAP row and the parity row now say that instead.
No behaviour change: comments and docs only.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
The placement formula is Skia's own fontations port
(src/ports/fontations/src/bitmap.rs) reading the sbix spec's
lower-left-corner rule; the earlier comment named CoreText, which that
source does not mention. Apple Color Emoji measures ~180 MB on current
macOS (the 190 MB figure is a hosted copy of the file), so the comments
say ~180 MB.
No behaviour change: comments only.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Colour emoji came out as the placeholder rectangles their outlines carry
(Apple Color Emoji) or as .notdef when the face has no outline at all:
nothing read sbix, CBDT/EBDT or COLR. A glyph now asks its face for
colour ink before its outline: an embedded bitmap strike decoded to
premultiplied RGBA and placed by the strike's bearings the way Skia (and
CoreText) places it, scaled to the size the run asks for, or a COLR v0
glyph's layers filled from the font's palette.
Faces that carry only colour tables are indexed again: Apple Color Emoji
is no longer skipped for being 190 MB (the scan still reads headers and
name tables only), and "Segoe UI Emoji" and "Noto Color Emoji" join
"Apple Color Emoji" as colour fallbacks, read only for a text that
nothing else covers. The png crate decodes the strikes; skrifa keeps
shaping, metrics and outlines.
Left for later: COLR v1 paint graphs, alpha-only strikes and the SVG and
PDF exports (text is outlines there).
Tests: 8 cases in the text crate (placement, PNG and raw-BGRA decoding,
scaling, COLR layers and bitmaps of the machine's own colour font) and a
render case gated on that font; the font-free ones run everywhere.
Verified on Windows with Segoe UI Emoji (COLR) and with Noto Color Emoji
(CBDT) read from a downloaded file; the sbix path runs through the same
code and needs a macOS run to be seen.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Headless runs (`figcraft-cli export`, `render`, `convert`, `run`, and the headless MCP server) never indexed the installed fonts, so a text layer in an installed family (Segoe UI, Inter installed per user, ...) was measured and drawn with a fallback face, while the desktop app used the real one. Auto layout sizes from MCP agents therefore differed from what users see in the app.
- `figcraft_text::scan_system_fonts_on_demand()`: the font database indexes the system fonts the first time it is used (first text layout, font lookup or font listing). `figcraft-cli` turns it on at startup for every subcommand, so a run that touches no text never pays for the scan. The desktop app keeps its background scan.
- `scan_system_fonts()` now runs once per process (a `OnceLock`): racing callers wait for the one directory walk instead of each walking the folders.
- Opt-out for reproducible renders: `--no-system-fonts` on any subcommand, or `FIGCRAFT_NO_SYSTEM_FONTS=1` (honoured by the desktop app too). Text then uses only the built-in fonts, as before.
- The scan is cheaper: it reads each file's headers, table directories and `name` tables instead of whole files (about 460 MB on this Windows machine), and takes file metadata from the directory listing instead of a lookup per file. This also speeds up the desktop app's background scan.
- Browser/wasm build unchanged (the scan is a no-op there).
- Docs: `docs/cli.md` (new Fonts section) and `docs/mcp.md` (Fonts under the backends table); CLI usage text mentions the flag.
- **Japanese (`ja`) and Korean (`ko`) draft catalogs** (`crates/ui-egui/locales/ja.toml`, `ko.toml`): every UI string (1,351 keys), command label (519) and menu wording (29), machine-drafted from our English (Japanese: concise, no desu/masu endings, full-width punctuation where Japanese UI text uses it; Korean: no hamnida endings, instructions in -seyo). Placeholders identical to English, `|context` keys translated per context, plural entries give only `other`. Marked `status = "draft - needs native review"` and registered as drafts (not in `COMPLETE`), like fr/de/pt-BR.
- **Plurals:** `plural_one` returns false for ja and ko (always "other").
- **Language picker and detection:** Preferences > Language lists the two languages by their own names; System picks `ja*` / `ko*` from the OS language list; `pref.language` doc lists `ja|ko` (engine spec and docs/mcp.md).
- **CJK UI fonts** (`crates/ui-egui/src/cjk.rs`): egui's fonts and Inter have no CJK glyphs. After the existing system font scan, the desktop app works out which CJK scripts (kana, Hangul, Han) the UI shows: the UI language's, the language picker's while Preferences is open, typed/IME text, and the open file's names and comments (re-read when the tab's revision changes, not mid-drag, and never again once all three scripts have a face). For each needed script it appends one face to egui's fallback list, the UI language's script first: Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux/BSD Noto Sans CJK, Source Han Sans, Nanum, WenQuanYi; the craft-fonts build's embedded BIZ UDPGothic is tried first (free). Han without kana outside a Japanese/Korean UI reads as Chinese (Chinese faces first). English users load no CJK face. No font files are added to the repo.
- **Read once, share the bytes:** new `FontDb::find_one` reads only the one face a fallback needs instead of every file of the family (Yu Gothic UI is 4 files of ~14 MB); `theme::font_data` hands egui the font database's shared bytes instead of copying them (also used for the existing Hebrew/Arabic fallbacks). The scan's existing 64 MB per-file cap applies. Any failure degrades to the previous behaviour (tofu at worst), never a panic.
- **Web:** no system fonts there. `crates/text/build.rs` now embeds craft-fonts' Noto Sans CJK into the wasm build when `FIGCRAFT_EMBED_CJK` is set (it covers kana, Hangul and Han; +16 MB) and reruns when that variable changes. Without it, `cjk::can_show` keeps ja/ko out of the picker and out of System detection, so Japanese and Korean browser users keep the English UI instead of boxes. Nothing is fetched from the network.
- **Docs:** localization-parity (rows, CJK UI fonts section, totals, revision), translating (catalog list, CJK conventions, fonts, plural note), target-app-parity, ROADMAP, gaps (gap 5), roadmap (focus 4), mcp.md. Localization 30% -> 35% (graded low: unreviewed drafts), 40-65 h -> 32-55 h; ready for real work ~52% -> ~53% (52.9 -> 53.1 by the dimension table); full parity ~284-554 h -> ~276-544 h; beta ~162-292 h -> ~155-280 h.
Right-to-left and mixed-direction names in the panels: layers, pages,
Assets, tab titles, the file header and canvas frame titles show in
visual order (Unicode bidi, joined Arabic letters, mirrored brackets),
elided at their end (the left end of a right-to-left name). egui shapes
each font run itself, so shown names are laid out a cluster at a time.
Rename fields keep egui's editing and draw the shown text over it: the
caret, selection, clicks, drags and arrow keys follow what's on screen.
System fonts are added for scripts the built-in UI fonts lack.
The Typography section's text style browser: list and grid views with
each style in its own font, search, folders, this file's and enabled
libraries' styles, apply, detach, edit the typography, create from the
selected chars, hover to preview on the canvas, and "Mixed" when the
chars use several styles. Text styles now link chars (style_id on text
styles), so a range can take a style; editing what a style sets
unlinks the chars edited; files from before link on open.
Live text previews (text.preview / text.endPreview): a hovered text
style, font style or named instance and a dragged axis slider show on
the canvas without an undo step; release commits one step, Escape
cancels. ui.input sends raw window input over the control channel.
Visual QA: list markers keep the paragraph's font, renames start with
the name selected and centred, Max lines shows while truncating.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>