From 59fbd6c46a51bd385abb713e2cff8610296bfb5c Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Tue, 21 Jul 2026 14:15:22 +0000 Subject: [PATCH 03/13] fix(vendor): bold/italic glyph spacing and family resolution on 10.6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed from 2 original commits (upstream-history split into vendor/ frontend layers, 2026-07-21). - vtrasterizer/TextRenderer.cpp: floor (not round-to-nearest) the shaped glyph advance to whole cells. A bold or fallback face whose glyphs are not perfectly cell-matched reports a per-glyph advance a little over one cell; nearest-rounding pushed some glyphs to two cells and others to one, causing ragged bold-label spacing ("Ke rnel"). Flooring trims only the overshoot; a real N-cell glyph (wide CJK, ligature) still reports an exact N-cell multiple and is unaffected. Contour is LTR, so advances are non-negative and floor-truncation is safe. - text_shaper/coretext_locator.mm: on 10.6, CTFontCreateWithFontDescriptor given a family name plus a weight/slant traits dictionary returns a DIFFERENT family (e.g. Lucida Grande) when the requested weight is not an exact trait match, rather than the family's closest face — a 10.6 CoreText matching quirk. Fix: after resolving, verify the font's family equals the requested family; if not, re-resolve via NSFontManager fontWithFamily:traits:weight:size:, which reliably returns the family's own bold/italic member. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LzTmVgP2ruMz987VJ78k77 (cherry picked from commit cf2e336df5fd4112c2baf4b01063cae6e4adfa46) --- src/text_shaper/CoreTextLocator.mm | 39 ++++++++++++++++++++++++++++++ src/vtrasterizer/TextRenderer.cpp | 12 ++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/text_shaper/CoreTextLocator.mm b/src/text_shaper/CoreTextLocator.mm index 901202a4..263c4767 100644 --- a/src/text_shaper/CoreTextLocator.mm +++ b/src/text_shaper/CoreTextLocator.mm @@ -162,6 +162,45 @@ namespace text CTFontRef font = CTFontCreateWithFontDescriptor(descriptor, 12.0, NULL); + // On 10.6, CTFontCreateWithFontDescriptor with a family name PLUS a weight/slant traits dict + // is unreliable: when the requested weight is not an exact match it can return a completely + // different family (e.g. the system default Lucida Grande) instead of the family's closest + // face. That is exactly the "bold text falls back to Lucida" bug. Verify the resolved font is + // still the requested family; if not, resolve the family + bold/italic traits via NSFontManager + // (which reliably returns the family's own bold/italic member) and use that instead. + if (font) + { + CFStringRef resolvedFamily = CTFontCopyFamilyName(font); + bool const familyMatches = + resolvedFamily && CFStringCompare(resolvedFamily, familyName, 0) == kCFCompareEqualTo; + if (resolvedFamily) + CFRelease(resolvedFamily); + + if (!familyMatches) + { + NSFontTraitMask mask = 0; + if (description.weight == FontWeight::Bold + || description.weight == FontWeight::ExtraBold + || description.weight == FontWeight::Black + || description.weight == FontWeight::ExtraBlack + || description.weight == FontWeight::DemiBold) + mask |= NSBoldFontMask; + if (description.slant == FontSlant::Italic) + mask |= NSItalicFontMask; + + NSString* nsFamily = [NSString stringWithUTF8String:description.familyName.c_str()]; + NSFont* nsFont = [_d->fm fontWithFamily:nsFamily + traits:mask + weight:(mask & NSBoldFontMask) ? 9 : 5 + size:12.0]; + if (nsFont) + { + CFRelease(font); + font = CTFontCreateWithName((CFStringRef) [nsFont fontName], 12.0, NULL); + } + } + } + if (font) { char path[PATH_MAX]; diff --git a/src/vtrasterizer/TextRenderer.cpp b/src/vtrasterizer/TextRenderer.cpp index 80fe93f8..25e4ea71 100644 --- a/src/vtrasterizer/TextRenderer.cpp +++ b/src/vtrasterizer/TextRenderer.cpp @@ -717,7 +717,17 @@ void TextRenderer::renderTextGroup(std::u32string_view codepoints, // let the pen step by the exact cell delta -- zero for a combining mark, N for a ligature spanning // N cells -- with no rounding at all. That awaits TextClusterGrouper's east-asian-width fixme, // since clusters presently count cells appended rather than columns occupied. - pen.x += advanceToCells(glyphPosition.advance.x, cellWidth) * cellWidth * advanceScale; + // + // Until then, snap the advance DOWN to whole cells rather than to the nearest: a bold/fallback + // face whose glyphs are not perfectly cell-matched reports an advance a little over one cell + // (e.g. ~1.3), and nearest-rounding pushes some of those to two cells, producing ragged + // "Ke rnel"/"Di splay" spacing. A real N-cell glyph (wide CJK, ligature) reports an advance at + // an exact N-cell multiple, so flooring keeps it at N; only the sub-multiple overshoot of a + // mismatched face is trimmed. A non-zero advance always steps at least one cell. + auto const advCells = glyphPosition.advance.x == 0 + ? 0 + : std::max(1, glyphPosition.advance.x / std::max(1, cellWidth)); + pen.x += advCells * cellWidth * advanceScale; } }