Skip to content

FOP-1896: A font's descender is not taken from a typo descender above the baseline, nor guessed as 0 - #119

Open
plutext wants to merge 1 commit into
apache:mainfrom
plutext:FOP-1896
Open

plutext wants to merge 1 commit into
apache:mainfrom
plutext:FOP-1896

Conversation

@plutext

@plutext plutext commented Oct 4, 2026

Copy link
Copy Markdown

Fixes FOP-1896.

OpenFont sets a TrueType or OpenType font's ascender and descender in two steps, and each has a defect.
They stack: fixing the first alone makes the font in the issue worse.

1. A typo descender above the baseline. determineAscDesc takes OS/2 sTypoAscender and
sTypoDescender, in its first branch and in its fallback, without checking the descender's sign. Several
shipping fonts have it wrong. Windows' Wingdings, Wingdings 2 and 3, Lucida Sans, Lucida Fax and Lucida Sans
Typewriter have sTypoDescender +420 of 2048 where their hhea descender is -432. Maiandra GD,
Haettenschweiler and Lucida Handwriting are similar. FOP takes the positive value, so the text area lies wholly
above the baseline (0.565 em for Wingdings), and the underline runs through the letters, as the issue reports.
The OS/2 values are now used only when sTypoDescender <= 0.

2. A guess from glyphs the font does not have. When the chosen ascender and descender together exceed the
em, guessVerticalMetricsFromGlyphBBox replaces them with the top of the d glyph and the bottom of the p
glyph, whether or not it found them. A font without them gets an ascender and a descender of 0;
TTFFileTestCase asserts exactly that for AndroidEmoji, beside "TODO: Nedd to be fixed?". Of 2,547 distinct
font files loaded through FontLoader (the font sets of several Linux desktop distributions, and a Windows and Office
set), 978 get 0 and 0. They include most Noto fonts for scripts other than Latin, the Droid script fonts, MT
Extra and Algerian. With a text area of height 0, the baseline sits in the middle of the line and the glyphs
rise into the line above. The guess now replaces the table values only when both were found, either side of
the baseline (localAscender > 0 && localDescender < 0).

Wingdings needs both: its hhea box (1841 + 432) exceeds its em and it has no d or p, so refusing its OS/2
values alone sends it to the guess, which gives 0 and 0.

These are the two conditions of the patch Eugene Markovskyi attached to FOP-1896 in 2011, arrived at
independently and then compared. That patch also required a negative hhea descender in the second branch,
which none of the 2,547 fonts needs: none has a positive one.

Measured at 11pt (area tree; text area height, and baseline from its top):

  Wingdings             6.215pt, 8.470pt   ->  12.188pt, 9.878pt
  Lucida Sans           6.215pt, 8.470pt   ->  10.582pt, 8.470pt
  Noto Sans Devanagari  0pt, 0pt           ->  14.344pt, 9.856pt

Line pitch does not change, since line-height still sets it; what moves is where the baseline sits in the
line. Every font the change moves had, before it, an ascender of 0 or a descender at or above the baseline;
after it, none of the 2,547 has either.

Tests. No new font: TTFFileTestCase patches sTypoDescender (OS/2 offset 70) in memory.

  • testPositiveTypoDescenderNotTakenWithoutGlyphsToGuessFrom: AndroidEmoji with +650 is Wingdings' case, and
    expects its hhea values, 2200 and -650.
  • testPositiveTypoDescenderNotTakenWithGlyphsToGuessFrom: DejaVuLGCSerif with +492 is the Lucida case, and
    expects its glyph bounds, 1556 and -426.
  • testGetLowerCaseAscent expects AndroidEmoji's 2200 where it expected 0, and its descender, -650.

All three fail without the change. The fop-core suite passes with it: 3663 tests, 0 failures (4 skipped), and
checkstyle reports 0 violations.

🤖 Generated with Claude Code

… the baseline, nor guessed as 0

OpenFont.determineAscDesc took OS/2 sTypoAscender/sTypoDescender, in its first branch and in its
fallback, without checking the descender's sign. Wingdings, Wingdings 2 and 3, Lucida Sans, Lucida Fax
and Lucida Sans Typewriter have sTypoDescender +420 where their hhea descender is -432, so the text area
lay wholly above the baseline and the underline ran through the letters. The OS/2 values are now used
only when sTypoDescender <= 0.

guessVerticalMetricsFromGlyphBBox replaced the ascender and descender with the bounds of the 'd' and 'p'
glyphs whenever the two exceeded the em, whether or not it had found those glyphs, so a font without
them got 0 and 0 (most Noto fonts for scripts other than Latin, symbol fonts; Wingdings once its OS/2
values are refused). It now replaces them only with values found either side of the baseline.

The two conditions of the patch attached to FOP-1896 in 2011. Tests patch sTypoDescender in memory:
AndroidEmoji with +650 (Wingdings' case) and DejaVuLGCSerif with +492 (the Lucida case);
testGetLowerCaseAscent now expects AndroidEmoji's 2200 where it expected 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant