[Win32] Let GDI lay out text again unless GDI+ layout is required - #3579
Draft
HeikoKlare wants to merge 1 commit into
Draft
[Win32] Let GDI lay out text again unless GDI+ layout is required#3579HeikoKlare wants to merge 1 commit into
HeikoKlare wants to merge 1 commit into
Conversation
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>
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
TextLayoutand 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
DrawStringandMeasureStringfunctions 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: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:
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
DrawDriverStringexists for, and it is the same conclusion Microsoft drew for their own stack: Windows Forms moved off GDI+ text in .NET 2.0, whereApplication.SetCompatibleTextRenderingDefaultdefaults to the GDI basedTextRendererbecause "GDI calculates character spacing and word wrapping differently from GDI+". KB 307208 does suggest an alternative remedy, combining the typographicStringFormatthat SWT already passes withTextRenderingHintAntiAlias. 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::DrawDriverStringdoes not support font decoration and renders blank space instead of the glyphs, which is a known GDI+ limitation whose recommended remedy is to useDrawStringfor 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.useGDITextRenderingForDecoratedFontsaccordingly. 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_TABin 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 aNullPointerException. The defect was latent as long as GDI+ performed the layout for every string.New tests in
GCWin32Testspin 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:

After:

Rendering
"Hello World 12345"through an advanced GC, as in the screenshots of #3577, gives the following widths in the three relevant configurations:master, the reported bugmasterplus the workaround flag from the issue, the "old behavior" screenshotThis 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_GDIPlusTextRenderinggains 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