Skip to content

[Win32] Let GDI lay out text again unless GDI+ layout is required - #3579

Draft
HeikoKlare wants to merge 1 commit into
eclipse-platform:masterfrom
HeikoKlare:gdi-text-layout-for-undecorated-fonts
Draft

[Win32] Let GDI lay out text again unless GDI+ layout is required#3579
HeikoKlare wants to merge 1 commit into
eclipse-platform:masterfrom
HeikoKlare:gdi-text-layout-for-undecorated-fonts

Conversation

@HeikoKlare

@HeikoKlare HeikoKlare commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #3577

What is wrong

Since #3100 made GDI+ lay out text for advanced GCs, glyph advances come from the font's unhinted design metrics instead of the hinted, grid-fitted advances the platform uses everywhere else, that is in native controls, in TextLayout and in a non-advanced GC. The error is a fraction of a pixel per glyph and mostly averages out across proportional text, which is why it went unnoticed.

This is documented GDI+ behavior rather than a defect. Microsoft KB 307208, "Why text appears different when drawn with GDIPlus versus GDI", states that "text layout with GDIPlus is resolution-independent" and that "the DrawString and MeasureString functions in the GDIPlus application programming interface (API) lay out text independent of device resolution". A string therefore occupies the same fraction of an em at every resolution, at the price of the layout no longer matching the grid-fitted glyphs that are actually rasterized. The same article documents how GDI+ reconciles the two when the grid-fitted glyphs turn out narrower than designed, which is exactly the case here:

  1. The line is allowed to contract by up to an em without any change of glyph spacing.
  2. Remaining contraction is made up by increasing the width of any spaces between words, to a maximum of doubling.
  3. Remaining contraction is made up by introducing blank pixels between glyphs.

Rule 3 is what becomes visible. Once the slack of rule 1 is used up, the surplus is spent as blank pixels inserted between individual glyphs.

It does not average out for digits. A font's figures are typically tabular, so all ten share a single advance and therefore a single error that repeats with the same sign for every digit of a group. The excess accumulates, and because glyphs are still rasterized grid-fitted, it lands unevenly in the gaps between them, which reads as broken tracking. For Segoe UI at 9pt the digit advance is 1104/2048 em = 6.469px unhinted versus 6px hinted, so every digit is 7.8% too wide and consecutive gaps alternate between 6 and 7 pixels.

Measured as the width of a 20 digit string, an advanced GC deviates from a non-advanced one as follows:

Font before after
Segoe UI -7 to +10 px 0 to 1 px
Arial -6 to -2 px 0 px
Times New Roman -10 to 0 px 0 px
Courier New -7 to +5 px 0 to 1 px
Consolas -9 to -4 px 0 px

How it is fixed

GDI becomes the text layout engine again, and GDI+ lays out text itself only where the glyph run cannot be drawn at all. That is the case for text containing characters GDI has no glyph for, where GDI would draw missing-glyph boxes, and for fonts with an underline or strikeout style. Both cases still draw with GDI+, so this only selects which engine performs the layout, and the reason for using GDI+ in the first place, namely alpha blending, transforms and antialiasing over transparent backgrounds, is unaffected.

Letting GDI compute the advances and GDI+ draw the resulting glyph run is what DrawDriverString exists for, and it is the same conclusion Microsoft drew for their own stack: Windows Forms moved off GDI+ text in .NET 2.0, where Application.SetCompatibleTextRenderingDefault defaults to the GDI based TextRenderer because "GDI calculates character spacing and word wrapping differently from GDI+". KB 307208 does suggest an alternative remedy, combining the typographic StringFormat that SWT already passes with TextRenderingHintAntiAlias. That does even out the gaps, because unhinted glyphs are then drawn at fractional positions, but ten Segoe UI digits still measure 64.7px against GDI's 60, so it removes the artifact without restoring the platform's metrics, and it gives up ClearType sharpness.

The decorated-font case is what actually caused #3091 in the first place: Graphics::DrawDriverString does not support font decoration and renders blank space instead of the glyphs, which is a known GDI+ limitation whose recommended remedy is to use DrawString for such fonts. Routing only those fonts to GDI+'s own layout keeps that fix intact and needs no decoration drawing of SWT's own. Bold and italic are unaffected by the limitation and keep using the glyph run.

The escape hatch introduced along with the GDI+ layout default is preserved but narrowed to its remaining purpose, and renamed to org.eclipse.swt.internal.win32.useGDITextRenderingForDecoratedFonts accordingly. It now only reverts the decorated-font case to the GDI glyph run, at the price of that text rendering blank again, and remains an internal safety net that may be removed at any point in time.

The change also includes SWT.DRAW_TAB in the condition deciding whether the glyph run path has to measure a segment. Placing the text after a tab consumes the bounds of the segment before it, so omitting the flag made tab-expanded text fail with a NullPointerException. The defect was latent as long as GDI+ performed the layout for every string.

New tests in GCWin32Tests pin the digit advances of an advanced GC to those of a non-advanced one across the existing set of proportional and monospace test fonts and several sizes, asserted within the same one pixel rounding tolerance the tab stop tests use rather than the loose tolerance the proportional kerning test needs, plus a regression test covering the tab drawing above. Users should see no change other than text laid out as it was before #3100, since text in decorated fonts keeps rendering exactly as it does today.

Verification against the reported symptom

The originally reported symptom, IDE tab headers containing numbers:

Before:
image

After:
image

Rendering "Hello World 12345" through an advanced GC, as in the screenshots of #3577, gives the following widths in the three relevant configurations:

Configuration Plain Bold Italic
current master, the reported bug 99 106 98
master plus the workaround flag from the issue, the "old behavior" screenshot 97 104 95
this branch, no flag 97 104 95

This branch is identical to the workaround configuration, not only in total width but pixel by pixel, in all three rows.

Related

The manual test SWTIssue3091_GDIPlusTextRendering gains digits in its sample strings so this class of regression is visible there, and its checkbox and documentation follow the narrowed system property.

🤖 Generated with Claude Code

Since GDI+ text layout became the default for advanced GCs, glyph
advances are derived from the font's unhinted design metrics instead of
the hinted, grid-fitted advances the platform uses everywhere else, i.e.
in native controls, in TextLayout and in a non-advanced GC. The
difference is a fraction of a pixel per glyph and mostly averages out
across proportional text, which is why it went unnoticed.

It does not average out for digits. A font's figures are typically
tabular, so all ten share a single advance and therefore a single error
that repeats with the same sign for every digit of a group. The excess
accumulates and, since glyphs are still rasterized grid-fitted, is
distributed unevenly across the gaps, which reads as broken tracking.
Measured against a non-advanced GC across the test fonts and sizes, a
20-digit string came out up to 8% too wide; it is now within the
rounding of the two extents.

This change therefore restores GDI as the text layout engine and limits
GDI+'s own layout to the cases where the glyph run cannot be drawn at
all: text containing characters GDI has no glyph for, and fonts with an
underline or strikeout style. The latter is the actual cause behind
eclipse-platform#3091 :
Graphics_DrawDriverString does not support font decoration and draws
blank space instead of the glyphs, which is a documented GDI+ limitation
whose recommended remedy is to use Graphics_DrawString for those fonts.
Both cases still draw with GDI+, so this only selects which engine
performs the layout, and decorated text keeps rendering exactly as it
does today.

Narrow the escape hatch introduced along with the GDI+ layout default
accordingly, and rename it to reflect its remaining scope: it now only
reverts the decorated-font case to the GDI glyph run, at the price of
that text rendering blank again. It remains an internal safety net that
may be removed at any point in time.

Include SWT.DRAW_TAB in the condition deciding whether the glyph run
path has to measure a segment. Placing the text after a tab consumes the
bounds of the segment before it, so omitting the flag made tab-expanded
text fail with a NullPointerException. The defect was latent while GDI+
performed the layout for every string.

Fixes eclipse-platform#3577

Assisted-by: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Test Results (win32)

   35 files  ±0     35 suites  ±0   5m 33s ⏱️ +25s
4 933 tests +6  4 855 ✅ +6  78 💤 ±0  0 ❌ ±0 
1 457 runs  +6  1 433 ✅ +6  24 💤 ±0  0 ❌ ±0 

Results for commit c78dab0. ± Comparison against base commit 9afdcd4.

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.

[Win32] Broken text kerning with GDI+

1 participant