Skip to content

text-freetype2: Read the matched font's face index from fontconfig - #1411

Merged
zackees merged 2 commits into
obs-rust:masterfrom
rdoupe:fix/ft2-face-index
Oct 18, 2026
Merged

zackees merged 2 commits into
obs-rust:masterfrom
rdoupe:fix/ft2-face-index

Conversation

@rdoupe

@rdoupe rdoupe commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #1409

What and why

The Text (FreeType 2) source on Linux now renders the font face that was picked when it is not the first face of its file. Upstream: obsproject/obs-studio#11819 (confirmed, open); fix by reyohoho in obsproject/obs-studio#13290 (open, unreviewed since April 2026).

  • get_font_path() (plugins/text-freetype2/find-font-unix.c.h) read the face index with FcPatternGetInteger(match, FC_INDEX, 1, &fc_index). The third argument is the position among the property's values, and a matched pattern has one index, at position 0. The call always failed, so fc_index stayed 0 and FT_New_Face() loaded face 0.
  • Any font that is not face 0 of its file was affected:
    • Variable fonts: the other named instances, such as Adwaita Sans Bold (index 458752 in AdwaitaSans-Regular.ttf), rendered as Regular.
    • Collections: the faces of a .ttc/.otc, such as Noto Sans CJK SC (face 2 of NotoSansCJK-Regular.ttc), rendered as face 0, which is JP there. A collection face with less glyph coverage drops letters, as in the #11819 report.
  • The fix reads position 0. It is a port of the first commit of #13290, with reyohoho as co-author. The other two commits there, a bigger glyph atlas and pre-cached Cyrillic, are not needed: text-freetype2: Evict old glyphs when the atlas is full #196 already redraws text when the atlas is full.

Headless (private Xvfb, private D-Bus, fresh HOME), driven over obs-websocket. Text (FreeType 2) at 96 px, source screenshots; fonts from the host's fontconfig:

Font v0.1.0-beta.20261014 AppImage This branch
Adwaita Sans Regular, "OBS Studio" 513x117, ink 0.185 513x117, ink 0.185
Adwaita Sans Bold, "OBS Studio" 513x117, ink 0.185 (same as Regular) 525x117, ink 0.273
Noto Sans CJK JP, 骨直黄 288x117, ink 0.2684 288x117, ink 0.2684
Noto Sans CJK SC, 骨直黄 288x117, ink 0.2684 (same as JP) 288x117, ink 0.2737

RED → GREEN

  • RED: 149a506 test/cmocka: Add failing test for a font that is not face 0 of its file. test_text_freetype2_face_index points fontconfig at a directory holding only test/cmocka/data/text-collection.ttc, a 2 KB two-face collection ("OBS Collection Fixture" Regular as face 0, Bold as face 1). It is generated by text-collection.py from synthetic outlines, like text-aa.ttf. The test asks get_font_path() for each style. On master it fails 1 of 2: bold_resolves_to_face_1 ("Bold resolved to face 0 of .../text-collection.ttc (expected 1)"). It needs no display or installed fonts.
  • GREEN: fdf012d text-freetype2: Read the matched font's face index from fontconfig. 2 of 2; the other text-freetype2 tests (bundled font, host font, platform) still pass. Full build (C libobs lane) and full ctest -j4 (private Xvfb, private D-Bus, fresh HOME, system libobs-opengl masked): 304/304; the only skipped case is open_crashes_without_obs_core, as on master. The plugin has no Rust port.

Checklist

  • Base is master
  • I claimed the issue, or no one else has an open PR for it
  • Characterization test (if the unit had none), then RED, then GREEN commits
  • (Optional) Local gate passed and the pushed head is stamped; otherwise PR CI runs the full suite
  • Warning or flag changes (-W..., ratchets): none
  • Intentional differences from C listed in the parity test header and rust/README.md (ports): not a port

AI note: Claude Code (Claude Opus 5.5) wrote the test, the font fixture, the commit messages and this text under the submitter's direction, and ran the tests above on the submitter's machines. The fix itself is reyohoho's.

@rdoupe
rdoupe requested a review from zackees as a code owner October 18, 2026 08:37
@zackees
zackees added this pull request to the merge queue Oct 18, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 18, 2026
@rdoupe
rdoupe force-pushed the fix/ft2-face-index branch from c659161 to b304cd7 Compare October 18, 2026 16:09
@zackees
zackees enabled auto-merge October 18, 2026 16:13

@zackees zackees left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Coordinator review: LGTM.

  • FcPatternGetInteger(match, FC_INDEX, 0, ...) is the right fix: the third argument is the value position, and FC_INDEX has one value, so position 1 always failed and left fc_index at 0.
  • Variable-font named instances (index in the high 16 bits, e.g. 458752) pass through FT_New_Face unchanged, so they work too.
  • RED then GREEN commits; the test is hermetic (its own fontconfig file and cache dir, only the synthetic fixture visible) and the fixture is reproducible from text-collection.py; features ledger line present.

The Tier 2 (tests ON) failure is test_group_hide_transition_audio, the known load flake in #838 and unrelated to this change. I've re-run it; auto-merge is on.

rdoupe and others added 2 commits October 18, 2026 12:57
The Text (FreeType 2) source on Linux loads the first face of a font file
whatever face fontconfig matched (obsproject/obs-studio#11819). A face of
a .ttc/.otc collection, such as Noto Sans CJK SC, TC or KR, or a named
instance of a variable font, such as Adwaita Sans Bold, renders as face
0.

test_text_freetype2_face_index points fontconfig at a directory that
holds only text-collection.ttc, a two-face collection generated by
test/cmocka/data/text-collection.py from synthetic outlines ("OBS
Collection Fixture" Regular as face 0, Bold as face 1), and asks
get_font_path() for each style.

Fails 1 of 2: bold_resolves_to_face_1 ("Bold resolved to face 0 of
.../text-collection.ttc (expected 1)"). Regular passes. Needs no display.

Refs obs-rust#1409.
get_font_path() read FC_INDEX with FcPatternGetInteger(match, FC_INDEX,
1, ...). The third argument is the position among the property's values,
and a matched pattern has one index, at position 0. The call always
failed, fc_index kept its default 0, and FT_New_Face() loaded the first
face of the file (obsproject/obs-studio#11819).

So any font that is not face 0 of its file rendered as face 0: Bold and
the other named instances of a variable font (Adwaita Sans Bold is index
458752 in AdwaitaSans-Regular.ttf), and the faces of a .ttc collection
(Noto Sans CJK SC is face 2 of NotoSansCJK-Regular.ttc; it rendered as
the JP face 0).

Port of the first commit of obsproject/obs-studio#13290 by reyohoho. The
other two commits there (a larger glyph atlas, pre-cached Cyrillic) are
covered here by obs-rust#196's atlas fix and are not part of this change.

test_text_freetype2_face_index: 2 of 2. In the app, Adwaita Sans Bold now
renders bold (513 -> 525 px wide, ink 0.185 -> 0.273 at 96 px), and Noto
Sans CJK SC renders the SC glyphs instead of JP's.

Fixes obs-rust#1409.

Co-Authored-By: reyohoho <reyohoho@proton.me>
@rdoupe
rdoupe force-pushed the fix/ft2-face-index branch from b304cd7 to fdf012d Compare October 18, 2026 16:57
@zackees
zackees added this pull request to the merge queue Oct 18, 2026
Merged via the queue into obs-rust:master with commit cda0f4d Oct 18, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

bug: text-freetype2: fonts that are not face 0 of their file render as face 0 (Bold of variable fonts, CJK collections; upstream #11819)

2 participants