Skip to content

Fonts + PDF: Mark-to-base positioning is computed but never reaches the PDF renderer #2503

Description

@swmal

Summary

MarkToBaseProvider resolves GPOS mark lookups and writes the result to
ShapedGlyph.XOffset/YOffset, but no renderer reads those fields. A grep across
Export.Pdf, PdfExport, DrawingRenderer and Graphics returns only OS/2 hits. Mark-to-base
is therefore dead code in the PDF path, and combining marks are drawn at the pen position with no
offset at all.

The visible symptom: a combining acute over a lowercase base looks correct, while the same accent
over a capital sits too far right and too low and collides with the apex.

Reproduction

KerningReproTests.Avatar_Calibri48_KerningReproSheet produces WP1KerningRepro.pdf. Cells A7
and A8 hold A + U+0301 and a + U+0301 in Calibri 48 pt. Cells B7 and B8 hold the precomposed
Á and á as a visual baseline; those resolve to a single glyph in cmap and never reach the mark
positioning code, so they render correctly and make the defect easy to see side by side.

The content stream for A7 is:

[<0002> <000d>] TJ

A followed by acutecomb, with no Td, no Tm and no adjustment. The mark is drawn at the pen
after the base's full advance.

Why lowercase looks right

acutecomb has LSB = −760 fu, so it draws far to the left of its own origin. Placed on the pen
after a lowercase letter it lands nearly correctly by coincidence. Measured from the anchors in
the embedded Calibri subset (mark lookup type 4, mark class 0, mark anchor (−501, 1091)):

Base Base anchor Pen-relative XOffset YOffset
A (593, 1382) −91 fu (−2.13 pt) +291 fu (+6.82 pt)
a (491, 1135) +11 fu (+0.26 pt) +44 fu (+1.03 pt)

For a the correct offset is essentially zero, which is why drawing on the pen is almost right.
For A it is 91 fu too far right and 291 fu too low.

Changes

# File Change
1 EPPlus.Fonts.OpenType/TextShaping/Positioning/MarkToBaseProvider.cs (line 114–115) Make the offset pen-relative, following the HarfBuzz convention: subtract baseGlyph.XAdvance, not BaseAdvance. Kerning has already been applied by the time mark positioning runs — ApplyPositioning orders SinglePos, then kerning, then mark.
2 Same file Use += instead of = so that XPlacement from SinglePos (TextShaper line 530–532) is not overwritten.
3 EPPlus.Export.Pdf/DocumentObjects/PdfContentStream.cs Read XOffset/YOffset. This requires the mark to break out of the TJ array and be positioned with its own Td/Tm, since TJ can only adjust horizontally.

Acceptance criteria

  • Pen-relative XOffset for A + U+0301 in Calibri is −91 fu and YOffset is +291 fu.
  • Pen-relative XOffset for a + U+0301 is +11 fu and YOffset is +44 fu.
  • The rendered YOffset difference between Á and á is 247 fu. It is 0 today — both accents
    occupy an identical row range in the rasterised output.
  • The rendered accent over Á does not collide with the apex.
  • á does not regress. Its correct offset is near zero, so a sign error would show up here as a
    small shift rather than an obvious break.

Notes

Depends on WP1, which is now merged. MarkToBaseProvider sets XAdvance = 0 but leaves
BaseAdvance untouched, which produces an unscaled TJ adjustment. That is only correct once the
units-per-em scaling from WP1 is in place. Harmless as long as marks have an hmtx advance of 0,
but not true in general.

Breaking change. XOffset/YOffset become pen-relative rather than relative to the base
glyph's origin. There are zero consumers today, so the change is free now and breaking after
release. Both fields are public in EPPlus.Interfaces.

Not in scope: mkmk (GPOS type 6) and MarkToLigature (type 5) are not loaded at all, so
stacked diacritics remain unsupported. Tracked separately under WP7.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpdfLabel used for pdf related issues

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions