Skip to content

Decide whether a standard-14 face constant should resolve to the face it names #451

Description

@DemchaAV

Where this stands

fontName(FontName.HELVETICA_BOLD) selects the Helvetica family. The face comes
from the style's decoration, so a style that names a face and sets no decoration
renders regular — and is measured with regular metrics. The same holds for all nine
standard-14 style variants (TIMES_BOLD, COURIER_OBLIQUE,
HELVETICA_BOLD_OBLIQUE, …).

Two pieces, each individually reasonable:

So the design is name selects the family, decoration selects the face, and
.fontName(HELVETICA_BOLD) expresses the family twice and the face never. The working
form is:

DocumentTextStyle.builder()
        .fontName(FontName.HELVETICA)
        .decoration(DocumentTextDecoration.BOLD)
        .size(13)
        .build();

What has already landed

#479 and #486 took Option 2 — make the no-op visible — and cleared the call sites.
Nothing in this repository renders the wrong weight any more.

  • The one library site that produced wrong output is fixed.
    ChartDefaults.DONUT_CENTER_TEXT_STYLE names HELVETICA and sets
    decoration(BOLD). The four other sites in the original inventory —
    HeadingBarStyle, TimelineBuilder, TimelineMarker, ModernProfessional — each
    already set the decoration beside the alias and were never wrong; the alias was
    redundant there, not broken.
  • The silence is gone. FontLibrary.warnOnceAboutFaceAlias logs one line per
    distinct face constant the library rewrites — per name, not per lookup, so a document
    that uses the form a thousand times says so once.
  • The rule is written down. docs/font-coverage.md, section "The name picks the
    family, the decoration picks the face"
    , and the fontName @param on
    DocumentTextStyle.
  • Today's answer is pinned.
    FontFaceResolutionTest
    asserts the resolved PDFont for the family + decoration matrix, for a face constant
    with no decoration, and for HELVETICA_BOLD with decoration(ITALIC) →
    Helvetica-Oblique.
  • The call sites are gone. fix(examples): render the weights the styles declare #486 rewrote the form in 138 places in examples. A grep
    for the face constants across every non-test main source now returns only the
    FONT_ALIASES table, the DefaultFonts family declarations and Javadoc — zero call
    sites.

What is still open

Whether the alias should become a real fallback: look the requested variant up
first, and fall back to the base family only when nothing is registered. The
standard-14 families already declare all four faces (DefaultFonts passes
"HELVETICA_BOLD" and friends to FontFamilyDefinition.standard14), so the bold
program is available — it is only unreachable.

Deciding it needs a precedence rule for the case the pinning test already names: a
style that names HELVETICA_BOLD and sets decoration(ITALIC). Today the decoration
wins and the result is Helvetica-Oblique. The candidates are decoration-wins
(unchanged), name-wins, or combine into bold-oblique. Whichever is chosen, it lands as
an edit to a named assertion rather than as a silent change of behaviour.

The cost argument has changed since this was filed. The original reason to defer was
blast radius: every layout snapshot and pixel baseline covering an alias site would
move, and 97 example sites used the form without a decoration. Those sites no longer
exist, so making the alias a real face now moves nothing in this repository. The change
is visible only to consumer code that writes the form — which is exactly where it would
read as a fix rather than a regression. It is still a behavioural change to published
API and wants a @since note and a CHANGELOG entry, but it no longer carries a
snapshot-churn bill.

The remaining alternative is to leave the rewrite as the rule and treat the warning plus
the two documented statements as the answer. That is a decision to record here and
close on, not a silent status quo.

Also update

docs/font-coverage.md and the DocumentTextStyle @param, both of which currently
state the rewrite as the rule, if the rule changes.
docs/architecture/backend-capability-matrix.md only if resolution ends up differing
per backend — the PPTX backend takes the face from the decoration since #450, so the two
agree today.

Activity

  1. DemchaAV commented on Jul 31, 2026

    @DemchaAV
    OwnerAuthor

    Working on this in #479. Two corrections to the inventory above, both measured by rendering the whole example catalogue and comparing every page.

    Four of the five library sites are already correct. HeadingBarStyle:21, TimelineBuilder:69, TimelineMarker:74 and ModernProfessional:134 each set .decoration(BOLD) beside the alias, so they render bold today. The alias is redundant there, not broken. ChartDefaults:101 is the one that sets no decoration, and it is the only place in the library where the form produces the wrong output.

    So the blast radius is narrower than "every chart axis label, heading bar, timeline marker and those preset headings": fixing ChartDefaults changes three documents — the engine deck (page 4), the feature catalogue (page 3) and the chart showcase (page 5). The axis labels are unaffected; they name HELVETICA already.

    A second source looked bigger and turned out to be a dead declaration. Four Typography factories (modernProfessional, editorialBlue, invoiceModern, proposalModern) declare a face constant as headlineFont, which reads like a systematic version of the same bug. It is not: those presets style their headlines themselves and never reach BrandTheme.headlineStyle(). Patching that style to BOLD changes nine documents — classic-serif, minimal-underlined, boxed-sections, centered-headline, blue-banner — none of them the four themes, and all of them intentionally regular. Rewriting the four tokens to their base family moves nothing.

    #479 takes neither Option 1 nor Option 3. It fixes the one real site, rewrites the sixteen redundant ones to name their family (render-neutral, verified by the same sweep), and adds the Option 2 signal: one log line per distinct face constant FontLibrary rewrites — per name, not per lookup.

    Option 1 stays open and is now cheaper to decide. FontFaceResolutionTest pins today's answer against the resolved PDFont, including HELVETICA_BOLD with decoration(ITALIC) → Helvetica-Oblique. Making the alias a real face becomes an edit to a named assertion rather than a silent change, and the precedence rule that case needs can be settled then. The cost is unchanged: 97 example sites use the form without a decoration.

  2. changed the title [-]fontName(HELVETICA_BOLD) selects the family, not the weight — bold text renders regular[/-] [+]Decide whether a standard-14 face constant should resolve to the face it names[/+] on Aug 7, 2026
  3. added
    enhancementNew feature or request
    and removed
    bugSomething isn't working
    on Aug 7, 2026
  4. DemchaAV commented on Aug 12, 2026

    @DemchaAV
    OwnerAuthor

    Not touched by the RTL/fonts work that just landed (#535/#534/#536/#538/#539/#540 —
    FontLibrary.java has zero commits from it, the FONT_ALIASES rewrite and the warning are
    exactly as this issue describes them). But that work supplies an argument for the open
    decision, so noting it here rather than leaving it to be rediscovered.

    The custom-font path already implements the fallback this issue is weighing, and it is
    now load-bearing. FontFamilyDefinition.Builder.build():

    FontBinarySource resolvedBold   = bold   != null ? bold   : regular;
    FontBinarySource resolvedItalic = italic != null ? italic : regular;
    FontBinarySource resolvedBoldItalic = boldItalic != null ? boldItalic
            : (bold != null ? bold : (italic != null ? italic : regular));

    Requested face if registered, nearest otherwise — with a ladder that keeps the weight when
    the slant is unavailable. The families added in this cycle lean on it for real: David Libre
    ships no italic, Noto Sans Georgian and Armenian are single variable-font instances, and
    Gothic A1 has a drawn bold but no italic. BundledFamiliesTest pins each collapse.

    So the library currently answers the same question two ways: a custom family resolves
    requested, else nearest, while a standard-14 face constant is rewritten to its base
    before any lookup, which is why the bold program that DefaultFonts registers via
    FontFamilyDefinition.standard14("HELVETICA", "HELVETICA_BOLD", …) exists but is
    unreachable. Option 1 would make the two paths agree rather than introduce a new behaviour.

    It also settles the precedence question this issue flags, in the direction the custom path
    already went: there, the explicitly registered face wins and the fallback only fires on
    absence. Applied to HELVETICA_BOLD + decoration(ITALIC), the consistent reading is that
    the decoration is the face selector and the constant contributes nothing once the family is
    known — i.e. today's Helvetica-Oblique, which FontFaceResolutionTest already pins. That
    keeps one rule for both paths instead of a special case for the standard 14.

    Still open, still a decision — just with one fewer unknown.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions