Skip to content

LT-22638: Show OpenType feature names and multi-valued variants - #1029

Draft
jasonleenaylor wants to merge 1 commit into
mainfrom
LT-22638
Draft

LT-22638: Show OpenType feature names and multi-valued variants#1029
jasonleenaylor wants to merge 1 commit into
mainfrom
LT-22638

Conversation

@jasonleenaylor

@jasonleenaylor jasonleenaylor commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes LT-22638. Makes OpenType font
features usable in Font Options: features show human-readable names from the
font (or a registered-feature catalog), and character variants with multiple
named alternates are selectable instead of a single On/Off toggle — parity with
the existing Graphite experience, with no change to the dropdown menu, so both
the Writing System and Styles font dialogs inherit it.

What changed

  • New OpenTypeFontFeatureInfoReader (FwUtils) — parses GSUB/GPOS feature
    lists, featureParams, and the name table via a table-source delegate (GDI
    GetFontData in the app, font-file bytes in tests). Bounds-checked; malformed
    fonts degrade to tag-only records. Adapted from Paratext's OpenTypeFeatures.Ttf.
  • New OpenTypeFeatureCatalog (FwUtils) — classifies registered features
    (hidden / default-on) and supplies English names, seeded from Paratext and
    audited against the OpenType registry (dlig visible, aalt hidden, kern
    default-on).
  • Rewrote the OpenType provider in FontFeaturesButton — character variants
    with named options become "None + option" submenus stored as cvNN=k; labels
    resolve font-supplied → resx → catalog → numbered fallback; default-on features
    initialize enabled; hidden features filtered.
  • resx — added None / numbered-set fallbacks; removed dead labels (aalt,
    ccmp) and entries orphaned by the rewrite (ss01ss05, ValueOff/ValueOn).
  • DocsDocs/opentype-font-features.md updated.

Storage, rendering, and CSS/Word export are untouched — the renderer-neutral
tag=value string already carried multi-values.

Testing

  • Reader verified against real Charis SIL 6.200 (cv43 "Capital Eng" + 3
    options, ss01 "Single-story a and g", GPOS mark/mkmk) and Scheherazade
    New
    (cv70 "Damma").
  • Synthetic malformed-table robustness tests.
  • Provider tests: multi-value cv, binary fallback, default-on init, label
    priority, hidden filtering, cv43=2 round-trip, resx↔catalog consistency.
  • Green: FwUtilsTests 33, FwCoreDlgControlsTests 17, FwCoreDlgsTests 19. Coverage:
    catalog/info 100%, reader 86%.
  • Manual acceptance passed (WS setup → Font tab → Charis → Font Features).

🤖 Generated with Claude Code


This change is Reviewable

- Read the font-supplied names and named options from GSUB featureParams
  and the name table so features are comprehensible and character
  variants are selectable, as with Graphite.
- Add OpenTypeFontFeatureInfoReader adapted from Paratext's
  OpenTypeFeatures.Ttf.
- Add OpenTypeFeatureCatalog registered-feature hidden and default-on
  classification plus English names.

Co-Authored-By: Claude Fable 5 <[email protected]>
@github-actions

Copy link
Copy Markdown

NUnit Tests

    1 files  ± 0      1 suites  ±0   10m 51s ⏱️ + 2m 38s
4 345 tests +32  4 272 ✅ +32  73 💤 ±0  0 ❌ ±0 
4 354 runs  +32  4 281 ✅ +32  73 💤 ±0  0 ❌ ±0 

Results for commit b8a2dd1. ± Comparison against base commit ac89f08.

This pull request removes 2 and adds 34 tests. Note that renamed tests count towards both.
SIL.FieldWorks.FwCoreDlgControlsTests.TestFontFeaturesButton ‑ OpenTypeFontFeatureReader_CachesFeatureTagsForSameFontKey
SIL.FieldWorks.FwCoreDlgControlsTests.TestFontFeaturesButton ‑ OpenTypeFontFeatureReader_FiltersRequiredShapingFeatures
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ AccessAllAlternates_IsHidden
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ CommonlyDefaultOnFeatures_AreDefaultOn("calt")
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ CommonlyDefaultOnFeatures_AreDefaultOn("clig")
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ CommonlyDefaultOnFeatures_AreDefaultOn("kern")
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ CommonlyDefaultOnFeatures_AreDefaultOn("liga")
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ DiscretionaryLigatures_AreUserVisible
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ LegacyBlocklistTags_AreAllHidden
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ UnknownTag_IsNeitherKnownNorHidden
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ VisibleFeatures_HaveEnglishNames
SIL.FieldWorks.Common.FwUtils.OpenTypeFontFeatureInfoReaderRobustnessTests ‑ FeatureParamsOffsetPastTableEnd_YieldsTagWithoutLabel
…

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.02148% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 33.09%. Comparing base (321ba87) to head (b8a2dd1).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...rc/Common/FwUtils/OpenTypeFontFeatureInfoReader.cs 81.46% 16 Missing and 17 partials ⚠️
...FwCoreDlgs/FwCoreDlgControls/FontFeaturesButton.cs 85.36% 3 Missing and 9 partials ⚠️
Src/Common/FwUtils/OpenTypeFeatureCatalog.cs 99.37% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1029      +/-   ##
==========================================
+ Coverage   33.02%   33.09%   +0.07%     
==========================================
  Files        1202     1204       +2     
  Lines      278232   278559     +327     
  Branches    37169    37226      +57     
==========================================
+ Hits        91880    92196     +316     
+ Misses     158502   158493       -9     
- Partials    27850    27870      +20     
Files with missing lines Coverage Δ
Src/Common/FwUtils/OpenTypeFeatureCatalog.cs 99.37% <99.37%> (ø)
...FwCoreDlgs/FwCoreDlgControls/FontFeaturesButton.cs 62.85% <85.36%> (+2.17%) ⬆️
...rc/Common/FwUtils/OpenTypeFontFeatureInfoReader.cs 81.46% <81.46%> (ø)

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@johnml1135 johnml1135 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Four merge issues; details are inline:

  1. Preserve cvNN option positions when labels cannot decode.
  2. Align default-on features with verified renderer behavior, spec, and docs; hide size.
  3. Move user-visible catalog names to .resx.
  4. Add default-on persistence tests: unset stays absent; unchecked writes liga=0.

Next: address or explicitly defer each inline finding.

{
var option = LookupName(names, firstParamNameId + i);
if (!string.IsNullOrEmpty(option))
resolved.Add(option);

@johnml1135 johnml1135 Jul 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preserve undecodable cvNN option slots. Dropping one renumbers later values, so selecting "option 2" can persist the wrong glyph. Use a numbered placeholder and add a missing-option-name test.

["psts"] = Hidden("Post Base Substitutions"),
["pwid"] = Visible("Proportional Width"),
["qwid"] = Visible("Quarter Widths", OpenTypeFeatureDefaultState.Off),
["rand"] = Visible("Randomize", OpenTypeFeatureDefaultState.On),

@johnml1135 johnml1135 Jul 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catalog's default-on set exceeds the four tags specified by the proposal, design, tasks, and shipped docs. Align it with verified Uniscribe defaults or update the spec/docs deliberately; confirm kern, then pin the exact set in a test.

["salt"] = Visible("Stylistic Alternatives", OpenTypeFeatureDefaultState.Off),
["sinf"] = Visible("Scientific Inferiors", OpenTypeFeatureDefaultState.Off),
["smcp"] = Visible("Lowercase to Small Capitals", OpenTypeFeatureDefaultState.Off),
["size"] = Visible("Optical size", OpenTypeFeatureDefaultState.On),

@johnml1135 johnml1135 Jul 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

size is optical-sizing metadata, not a meaningful size=1 toggle. Hide it under the spec's "otherwise not user-configurable" rule.

["abvf"] = Hidden("Above Base Forms"),
["abvm"] = Hidden("Above Base Mark"),
["abvs"] = Hidden("Above Base Substitutions"),
["afrc"] = Visible("Vertical Fractions", OpenTypeFeatureDefaultState.Off),

@johnml1135 johnml1135 Jul 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move these user-visible English names to FwUtilsStrings.resx; C# fallbacks bypass Crowdin and violate AGENTS.md. Preserve or deliberately replace the translated keys removed from FwCoreDlgControls.resx.

return candidateScore > existingScore;
}

private static byte[] SafeGet(Func<string, byte[]> tableSource, string tag)

@johnml1135 johnml1135 Jul 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep graceful degradation, but trace parse failures. Bare catches make malformed fonts indistinguishable from fonts without features and remove the prior diagnostic path; route failures through the existing trace switch or an equivalent FwUtils source.

return cachedTags.ToArray();
OpenTypeFontFeatureInfo[] cached;
if (s_featureCache.TryGetValue(cacheKey, out cached))
return cached;

@johnml1135 johnml1135 Jul 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return a copy or immutable collection. Returning the cached array exposes shared mutable state across callers, unlike the previous .ToArray() behavior.

@johnml1135

johnml1135 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Three follow-ups:

  1. Align visible defaults with verified Uniscribe behavior; confirm kern and document the policy.
  2. Move FieldWorks-authored names to .resx; track UI-language wiring separately. Not known: whether bundled fonts contain reusable translations.
  3. File the planned UI follow-up for a selection summary and grouped live preview. LibreOffice is a license-compatible reference, subject to normal review.

Next: decide which items belong in this PR versus Jira.

@johnml1135

johnml1135 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Consolidated into the follow-up summary.

1 similar comment
@johnml1135

johnml1135 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Consolidated into the follow-up summary.

@johnml1135 johnml1135 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three follow-up findings; details are inline:

  1. OpenType character variants after option 31 are unreachable.
  2. Same-tag records can expose one script's labels and values in another writing system.
  3. s_tableReader is read outside the lock guarding test-time replacement.

Next: fix or explicitly accept each limitation.

if (info != null && info.Options.Count > 0)
{
// value 0 = "None", value i = the i-th named character-variant option.
var optionCount = Math.Min(info.Options.Count, Math.Max(0, maxValues - 1));

@johnml1135 johnml1135 Jul 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenType character variants after option 31 are silently unreachable because this reuses Graphite's kMaxValPerFeat. Raise the OpenType limit or expose truncation explicitly; add a test for more than 31 named options.

foreach (var info in ReadFeatureList(table, names))
{
OpenTypeFontFeatureInfo existing;
if (!byTag.TryGetValue(info.Tag, out existing) || IsRicher(info, existing))

@johnml1135 johnml1135 Jul 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same-tag feature records are merged across scripts by "richest" metadata, so a Cyrillic writing system can receive Latin labels and values. Prefer the writing system's script, then DFLT; otherwise document this as an accepted limitation.

}
var discoveredTags = tags.ToArray();
var discovered = OpenTypeFontFeatureInfoReader
.Read(tag => s_tableReader(hdc, MakeTableTag(tag))).ToArray();

@johnml1135 johnml1135 Jul 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snapshot s_tableReader inside s_cacheLock before invoking it. Tests write this mutable static under the lock but production reads it outside, allowing parallel fixtures to observe the wrong reader.

@johnml1135

johnml1135 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Correction incorporated into the follow-up summary.

2 similar comments
@johnml1135

johnml1135 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Correction incorporated into the follow-up summary.

@johnml1135

johnml1135 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Correction incorporated into the follow-up summary.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants