Repository navigation
Conversation
… tier A document set in Arial converts in 0.57 s with Arial installed and 44 s with it hidden from fontconfig, with peak RSS rising from 50 MB to 297 MB (issue nerdy-pro#188). The cost is not the per-glyph fallback pass — its memo is sound and it never asks the host when the substitute covers the text. It is the catalogue's deep tier: the first request for a name the host lacks as a family reaches step 4 of the resolver, which builds tier 2 by instantiating every face on the host and reading four tables from each. On CoreText that is 44 ms. On fontconfig/FreeType every `new_typeface` is an `FT_Open_Face` with an mmap, `enumerate` walks every font on the host per family, and the manager retains up to 1024 opened faces — seconds and hundreds of megabytes, proportional to the host's font count, not the document. Only after that scan did step 7 consult the table that answers "Arial → Liberation Sans". Three changes, each pinned by a test watched failing first: * Step 3b: a substitution-table key whose own family the host lacks is answered from the table before any metadata is read. The guard keeps the metadata path for keys whose base family is present — "Segoe UI Light" with Segoe UI installed still looks for the real Light face. What the shortcut gives up is a face carrying the key only as a typographic or localized alias (Arial Narrow for "Arial"); for a request by family name the metric substitute is the better answer, and the step table says so. * `family_faces` remembers what the manager said about a name it did not enumerate, both ways. `preload` asks four style variants of every family, so each absent family was probed four times — and a *matchable* one gained a duplicate slot per probe, which the deep tier then read as well. * `host_default` is fetched once per effective style; every unresolvable family reached `legacy_make_typeface` four times. Reproduced in miniature here: a copy of a five-page document in which every family resolves spends 14–23 ms in the registry; the same document with only its body font absent builds the deep tier (848 faces, 370–480 ms) and doubles wall time. The Linux magnitude remains inferred from Skia's fontconfig port; the reporter's `RUST_LOG=debug` "catalogue deep tier" line is the confirmation, and the resolver now also logs which request first reached the metadata tier. Refs nerdy-pro#188
`collect_font_families` added every §20.1.4.1.16 script font a theme lists — thirty-odd families in an ordinary Word theme (宋体, Nyala, Tunga, Vrinda, Shruti, Microsoft Himalaya…) — and `preload` resolved four style variants of each. Nothing can request one: `resolve_theme_font_ref` maps only the latin, east-asian and complex-script slots, so every one of those resolutions served no run. On a host without them every one was an absent family, and on fontconfig the first absent family builds the font metadata tier (issue nerdy-pro#188), so every themed document paid that scan — with the body font installed or not. Collect the three slots and leave the script fonts alone. A theme font a run does use still resolves on first measurement through `FontCache::get`, so output is unchanged; what changes is that a document no longer warns about fonts it never asked for. On this host the registry phase of a five-page themed document goes from building the tier to not building it. Refs nerdy-pro#188
ikashapov
added a commit
to ikashapov/dxpdf
that referenced
this pull request
Oct 14, 2026
One squashed commit carrying the tree of integration/2026-10-14, which was previously a chain of ten merges ending at ac5787e (kept locally as archive/integration-2026-10-14-merges). Base: upstream main 3a5c7ca. Integrated, each at the head of its open upstream pull request: - nerdy-pro#252 fix-layout-defects-228-232 (a40d5ce) — issues nerdy-pro#228–nerdy-pro#232: bookmark-only paragraphs, nested-table width, list instances over one abstract numbering, table width resolution (pct and tcW autofit), column balancing and separators - nerdy-pro#183 issue-150-wmf-svg (6e5aa99) — WMF/EMF and SVG images - nerdy-pro#184 issue-151-pgborders-docgrid (27204b3) — page borders, document grid - nerdy-pro#186 issue-153-shaped-clusters (65739f6) — shaped text clusters - nerdy-pro#187 issue-154-tracked-changes (6982b34) — tracked changes and comments - nerdy-pro#185 issue-155-smartart-charts (3683223) — SmartArt and charts - nerdy-pro#256 issue-253-del-instr-text (0656a52) — w:delInstrText tolerated - nerdy-pro#257 issue-250-image-hyperlinks (cb5ce2b) — a:hlinkClick on pictures - nerdy-pro#258 issue-188-font-absent-cost (9a63bf0) — no deep catalogue tier before the substitution table, no theme script-font preload - nerdy-pro#259 issue-158-auto-hyphenation (9bfbae2) — w:autoHyphenation Beyond the union of those branches, the integration itself changed: - README keeps one row per feature after the merges; the matrix tally is recomputed from the rows. - nerdy-pro#253's tests assert that a deleted field code is never drawn and never run text, because nerdy-pro#154 keeps w:del runs instead of dropping them at parse. - nerdy-pro#250's push_link_over sits after nerdy-pro#151's resolve_line_metrics; nerdy-pro#158's HyphenationCtx.measure and rejoin_hyphenated take &dyn SplitMeasure (nerdy-pro#153). Verified on this exact tree: cargo fmt, clippy and doc clean; cargo test 2977 passed / 0 failed (skipping the capi library freshness guards); --no-default-features and --features capi build; the twelve-document harness passes 12/12.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Answer a curated font substitution before building the metadata tier, and stop preloading theme script fonts
Refs #188 — the conversion-time half. The GIL half landed earlier (
py.detachinsrc/lib.rs).What was slow
A document set in Arial converts in 0.57 s with Arial installed and 44 s with it hidden from fontconfig; peak RSS 50 MB → 297 MB. The cost is not the per-glyph fallback pass — its
(face, codepoint)memo is sound and it never asks the host when the substitute covers the text. It is the catalogue's deep tier: the first request for a name the host lacks as a family reaches step 4 of the resolver, which builds tier 2 by instantiating every face on the host and reading four tables from each. On CoreText that is 44 ms. On fontconfig/FreeType everynew_typefaceis anFT_Open_Facewith an mmap,enumeratewalks every font on the host per family, and the manager retains up to 1024 opened faces — seconds and hundreds of megabytes, proportional to the host's font count rather than to the document. Only after that scan did step 7 consult the table that answers "Arial → Liberation Sans".Two things made every Word document pay, not just Arial ones:
preloadresolved four style variants of every family the document names, andcollect_font_familiesadded every per-script font a theme lists — thirty-odd families (宋体, Nyala, Tunga, Vrinda…) that no run can ever request, every one absent on a Linux desktop.Changes
family_facesremembers what the manager said about a name it did not enumerate, both ways — each absent family was probed four times, and a matchable unenumerated one gained a duplicate slot per probe that the deep tier then read as well.host_defaultis fetched once per effective style instead of once per unresolvable family per variant.resolve_theme_font_refmaps only the latin / east-asian / complex-script slots, so none of them served a run; a theme font a run does use still resolves lazily throughFontCache::get. Output is unchanged; the document stops warning about fonts it never asked for.catalog.rs's module doc states that its cost table was measured on CoreText and what the same operations cost on fontconfig.Evidence
Reproduced in miniature on macOS (debug build, relative numbers): a copy of a five-page document in which every family resolves spends 14–23 ms in the registry; the same document with only its body font absent builds the deep tier (848 faces, 370–480 ms) and doubles wall time, with layout, subsetting and painting unchanged and zero fallback lookups. The Linux magnitude is inferred from Skia's fontconfig port (
SkFontMgr_fontconfig.cpp:createTypefaceFromFcPattern,FontAccessible, the 1024-facefTFCache); the reporter'sRUST_LOG=debugrun before and after — the[font] catalogue deep tier: N faces …line — is the confirmation this PR asks for.A document naming an absent family that is not a substitution-table key still builds the tier once; bounding that (the typeface-cache limit during the scan) needs a Linux measurement first and is deliberately not in this PR.
Tests
Each watched fail first, with a mutation check:
a_table_key_whose_family_is_absent_substitutes_without_the_metadata_tier— "Calibri" with only Carlito installed →Substitution, anddeep_is_built()stays false (restoring the old order builds it).a_table_key_whose_base_family_is_present_still_reaches_the_metadata_tier— "Segoe UI Light" with Segoe UI →ParsedFaceName, tier built (dropping the guard turns it into a substitution).an_unenumerable_name_is_probed_once,the_host_default_is_fetched_once_per_style— the two memos, via counters.theme_script_fonts_are_not_preloaded.FaceCatalog::from_faces_without_deep_tieris the test-support constructor that makes "did this request reach the tier" observable at all;from_facespre-builds it.