From 916af11bdb029c94f26317fdb6304b276d8c799d Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Tue, 21 Jul 2026 17:27:34 +0000 Subject: [PATCH 06/13] fix(vendor): implement by-content font fallback (CJK/emoji missing from primary font) coretext_locator::resolve(codepoints) -- whose entire purpose, per its name and call site (open_shaper.cpp's resolveByCoverage()), is "find a font that covers these specific codepoints" -- was a stub returning an empty list, unconditionally. Combined with locate() only ever returning the primary font (no fallback chain; see the earlier vendor fix restoring 10.6 family/traits-dict correctness), this port had NO working font-fallback mechanism at all: any codepoint missing from the primary font -- CJK, emoji, anything outside it, even mixed into otherwise- Latin text -- fell through to the shaper's replacement glyph (open_shaper.cpp's replaceMissingGlyphs()). Implements resolve() via CTFontCreateForString, CoreText's per-content font- substitution primitive: builds a CFString from the UTF-32 codepoints (via kCFStringEncodingUTF32, which handles the surrogate-pair/astral-plane conversion CoreFoundation's own way rather than hand-rolling it), asks CoreText what font covers them, and resolves that font's file path via the same copyFontFilePath() helper locate() already uses. Deliberately NOT implemented via upstream's approach (populating locate()'s static cascade list from CTFontCopyDefaultCascadeListForLanguages): that function is 10.8+ only and does not exist on the primary PPC 10.6 target at all. It would also be a strictly weaker fix even where it did compile -- open_shaper.cpp's own resolveByCoverage() docstring notes a family/traits- ordered cascade can bury the only CJK-capable face arbitrarily far down the chain (observed: 83rd of 201 on a real system), past the walk-limit (fontFallbackLimit, default 16) ever worth paying for, whereas asking about the content directly finds it in one query regardless of chain position. CTFontCreateForString has existed since CoreText's 10.5 debut, confirmed via Apple's current documentation (macOS 10.5+) before relying on it, after two API availability mistakes earlier in this same investigation: CTFontCopyDefaultCascadeListForLanguages (10.8+) and kCTFontUIFontSystem (10.8+, hence anchoring resolve()'s CTFontCreateForString call on a plain CTFontCreateWithName(Helvetica) instead of a system-UI-font accessor). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LzTmVgP2ruMz987VJ78k77 (cherry picked from commit 1aba69e6fbe3224df3c109fcc8ccc7f25be0f686) --- src/text_shaper/CoreTextLocator.mm | 62 +++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/src/text_shaper/CoreTextLocator.mm b/src/text_shaper/CoreTextLocator.mm index 263c4767..cbe4c822 100644 --- a/src/text_shaper/CoreTextLocator.mm +++ b/src/text_shaper/CoreTextLocator.mm @@ -237,8 +237,66 @@ namespace text return output; } - FontSourceList CoreTextLocator::resolve(gsl::span /*codepoints*/) + // Finds a font that covers @p codepoints, via CoreText's own by-content cascading + // (CTFontCreateForString), rather than our locate()'s by-family/traits cascade (see the note above + // the call site in OpenShaper.cpp: a CJK-capable face can sort arbitrarily far down a + // family/traits-ordered chain -- past any walk-limit worth paying for -- while asking about the + // content directly finds it in one query, independent of that ordering). + // + // 10.6-safe: CTFontCreateForString has existed since CoreText's introduction in 10.5. (The + // newer CTFontCopyDefaultCascadeListForLanguages, used by locate()'s cascade-list scan on + // upstream, is 10.8+ only and is NOT an option on the primary PPC target -- do not reach for it + // here.) See docs/coretext-106-lessons for the class of 10.6 CoreText gotchas this file already + // works around. + FontSourceList CoreTextLocator::resolve(gsl::span codepoints) { - return {}; + if (codepoints.empty()) + return {}; + + FontSourceList result; + + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + + CFStringRef text = CFStringCreateWithBytes(kCFAllocatorDefault, + reinterpret_cast(codepoints.data()), + static_cast(codepoints.size() * sizeof(char32_t)), + kCFStringEncodingUTF32, + false); + if (!text) + { + locatorLog()("coretext: could not build a CFString from {} codepoint(s) to resolve.", + codepoints.size()); + [pool drain]; + return {}; + } + + // CTFontCreateForString consults ITS argument's own cascade list, not a global one, so the + // anchor font matters. resolve() is not given the document's primary font/description (see + // FontLocator.hpp -- the interface is shared with every platform locator, not ours to widen for + // a 10.6-only need), so anchor on Helvetica: present on every Mac OS X release including + // 10.5/10.6, and CTFontCreateWithName is the same primitive locate()/ctFontPath() already use + // elsewhere in this file -- no new, unverified API surface. Only the SUBSTITUTE's face is what + // we actually want; getOrCreateKeyForFont() at the call site re-opens it at the caller's real + // size/weight regardless of what size is asked for here. + CTFontRef anchor = CTFontCreateWithName(CFSTR("Helvetica"), 12.0, NULL); + if (anchor) + { + CFRange const range = CFRangeMake(0, CFStringGetLength(text)); + CTFontRef substitute = CTFontCreateForString(anchor, text, range); + if (substitute) + { + char path[PATH_MAX]; + if (copyFontFilePath(substitute, path, sizeof(path))) + result.emplace_back(FontPath { path }); + else + locatorLog()("coretext: could not resolve a file path for the by-content substitute font."); + CFRelease(substitute); + } + CFRelease(anchor); + } + + CFRelease(text); + [pool drain]; + return result; } }