Skip to content

fix(pdf): emit ToUnicode surrogate pairs for non-BMP glyphs (#77) - #78

Merged
sonatique merged 1 commit into
mainfrom
fix/issue-77-tounicode-surrogate
Jun 21, 2026
Merged

fix(pdf): emit ToUnicode surrogate pairs for non-BMP glyphs (#77)#78
sonatique merged 1 commit into
mainfrom
fix/issue-77-tounicode-surrogate

Conversation

@sonatique

Copy link
Copy Markdown
Owner

Fixes #77.

Problem

After #75 added the Symbola fallback, supplementary-plane (non-BMP) symbols render correctly, but their ToUnicode mapping is wrong: the bfchar destination is written as a raw >U+FFFF value (e.g. <1F6C7>), which isn't valid UTF-16BE. Extractors read only the first 16-bit unit, so copy/paste, search (Ctrl+F), and text extraction return a truncated, wrong character (U+1F6C7 → U+1F6C, a Greek letter). BMP symbols extract correctly.

Root cause

BuildToUnicodeCMap formatted the destination with {cp:X4} — four hex digits for a BMP codepoint, but five for a supplementary one. A ToUnicode bfchar destination must be a UTF-16BE string (16-bit units).

Fix

The destination is now emitted via a new Utf16BeHex helper (char.ConvertFromUtf32 → per-unit X4):

  • BMP → one 16-bit unit (e.g. <26D4>) — unchanged.
  • Supplementary → high+low surrogate pair (e.g. U+1F6C7 → <D83DDEC7>, U+1F6AB → <D83DDEAB>).

So non-BMP glyphs are now selectable / searchable / extractable as the correct character, restoring (for the SMP case) the goal of #52.

Tests

Adds PdfToUnicodeTests: Utf16BeHex unit cases (BMP + supplementary), and an end-to-end check that the rendered PDF maps U+1F6C7/U+1F6AB to their surrogate pairs and never the raw 5-hex form, while U+26D4 (BMP) stays a single 16-bit unit. (The ToUnicode stream is uncompressed, so the bfchar destinations are asserted directly in the PDF bytes.)

Full Release suite green (0 failures; +5 tests). Related: #75 (Symbola fallback chain), #72 (non-BMP handling), #52 (embedded fallback + ToUnicode).

After #75 added Symbola, supplementary-plane symbols rendered correctly but their
ToUnicode bfchar destination was formatted with {cp:X4} — five hex digits for a
>U+FFFF codepoint (e.g. <1F6C7>), which is not valid UTF-16BE. Extractors read
only the first 16-bit unit, so copy/paste, search, and text extraction returned a
truncated, wrong character (U+1F6C7 -> U+1F6C, a Greek letter).

BuildToUnicodeCMap now emits the destination via Utf16BeHex, which encodes a BMP
codepoint as four hex digits and a supplementary one as its high+low surrogate
pair (e.g. U+1F6C7 -> <D83DDEC7>). BMP glyphs are unchanged.

Adds PdfToUnicodeTests: Utf16BeHex unit cases (BMP + supplementary) and an
end-to-end check that the rendered PDF maps U+1F6C7/U+1F6AB to their surrogate
pairs and never the raw 5-hex form, while U+26D4 (BMP) stays a single unit.
@sonatique
sonatique merged commit 1186ac5 into main Jun 21, 2026
4 checks passed
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.

PDF: ToUnicode for non-BMP glyphs (Symbola fallback) is wrong — supplementary-plane chars extract as a truncated codepoint

1 participant