Corrupted ToUnicode CMap (empty bfchar entries) for real-GSUB Arabic text; also emits an empty [] TJ
Environment
- WeasyPrint: 69.0
- Pango: 1.58.0
- HarfBuzz: 14.2.1
- Python: 3.13.7
- OS: macOS 26.5.2 (Build 25F84), Darwin 25.5.0, arm64
Summary
Rendering a single Arabic word with a real font that has a GSUB table (Noto Sans Arabic) produces two defects that don't appear when rendering the same font/CSS with unshaped (Latin) text:
- Two of the seven
ToUnicodebfcharentries for the shaped run map to an empty destination string instead of a Unicode code point. Any tool that extracts text from the PDF then falls back to the raw CID value at those positions, injecting literal control characters into the extracted text — even though the glyphs render correctly on the page. This affects everyone who copies/extracts text from such a PDF, independent of any particular viewer. - Separately, an empty
[] TJoperator is emitted right after the run'sTm/Tf. Per the PDF spec this is a harmless no-op (shows nothing, advances by zero) — not a spec violation — so this isn't a "must fix", just worth knowing: it seems to serve no purpose and could simply not be emitted. One consumer we know of, Adobe Illustrator, does crash on documents containing it (see "Notes" below), which is the only reason it caught our attention.
Steps to reproduce
python3 -m venv .venv && .venv/bin/pip install weasyprint==69.0Get the font: https://github.com/notofonts/arabic/releases/tag/NotoSansArabic-v2.012 (static instance,
NotoSansArabic-Regular.ttf, version 2.012 — has a real GSUB table).Save as
كتابarabic.html, with the font file in the same directory:Render:
.venv/bin/python -m weasyprint arabic.html arabic.pdf(macOS + Homebrew: prefix withDYLD_LIBRARY_PATH=/opt/homebrew/lib, per WeasyPrint's own install docs — unrelated to this bug.)Inspect the font's
/ToUnicodeCMap (e.g. viapikepdf): two of sevenbfcharentries have an empty destination:<000e> <> <0012> <>
Inspect the page content stream: a
[] TJappears right after theTm/Tffor the text object.
Control (proves this is shaping-specific, not general)
Identical HTML/CSS, only the body text changed to the Latin transliteration "ktab" (same font-face, same font file):
- All five
bfcharentries in ToUnicode resolve to a real code point, none empty. - No
[] TJanywhere in the content stream. - Extracted text (PyMuPDF
page.get_text()) is clean:'ktab \n'.
Whereas the Arabic version extracts as:
'ك\x12تا\x0eب\n'— literal \x12/\x0e control bytes injected exactly where the empty bfchar entries are.
Observation
Both glyphs with an empty bfchar destination are the same base glyph in different forms:
<000e> is glyph uni066E, and <0012> is glyph uni066E.medi.wide — a contextual
(medial) form of that same glyph.
Expected
ToUnicode bfchar entries for shaped Arabic glyphs resolve to real Unicode code points, so
extracted text matches what's visually rendered.
Notes on the [] TJ
Isolated via single-variable testing in an internal investigation: two otherwise-identical
documents (same corrupted-CMap Arabic run in both), one with the empty [] TJ present and
one with it stripped — Illustrator opens the file without it, and fails with "Out of memory"
on the one with it. That test only varied the [] TJ's presence; whether [] TJ alone, in a
document with an otherwise-correct ToUnicode CMap, also triggers this has not been tested.
Stripping empty [] TJ operators from the content stream is a pure no-op removal (0.000%
pixel diff before/after in that investigation).
Related
- The ToUnicode corruption looks like the same class of bug covered by the existing
regression test
test_to_unicode_rtl(tests/test_api.py; originally namedtest_cmap_rtlwhen added by #2404, which fixed #1686), but that test usesfont-family: weasyprint— the built-in test font — which doesn't have a real GSUB table, so it doesn't catch this case even though it uses the same "اب" input. - Confirmed independent of variable- vs static-font-instance — this repro already uses the static instance.
- Not affected by the
full_fonts/hintingoptions (checked in the original investigation, not re-verified in this minimal repro).
Source: Kozea/WeasyPrint