FontDescriptor /Flags never sets the Serif bit, which misaligns text selection in pdf.js viewers
Summary
Cpdf.php never sets the Serif bit (bit 2, value 2) in the /FontDescriptor /Flags
entry. Every descriptor dompdf writes ends up as /Flags 32 (Nonsymbolic) plus the
optional Italic and FixedPitch bits, regardless of whether the font is a serif face.
This does not affect rendering, so it is easy to miss. It does affect consumers that
read /Flags to pick fallback font metrics, and the most visible symptom is misaligned
text selection highlighting in pdf.js based viewers (including Firefox's built in viewer).
Affected code
lib/Cpdf.php, currently lines 1320 to 1331, unchanged on master as of today:
// determine flags (more than a little flakey, hopefully will not matter much)
$flags = 0;
if ($font['ItalicAngle'] != 0) {
$flags += pow(2, 6);
}
if ($font['IsFixedPitch'] === 'true') {
$flags += 1;
}
$flags += pow(2, 5); // assume non-sybolicBit 2 (Serif) is never considered.
Observed output
A document generated with dompdf 3.1.5 and rendered with Tinos and DejaVuSerif, all serif faces, all embedded and subsetted:
$ pdffonts document.pdf
name type encoding emb sub uni
SUBAAB+Tinos-Regular CID TrueType Identity-H yes yes yes
SUBAAC+Tinos-Bold CID TrueType Identity-H yes yes yes
SUBAAD+DejaVuSerif CID TrueType Identity-H yes yes yesEvery /FontDescriptor in the file carries:
/Flags 32 (Nonsymbolic only, Serif bit not set)Verified on dompdf 3.1.6 as well, and the generating code is identical on master.
Why it matters downstream
pdf.js uses the descriptor flags to choose the generic fallback family for its text
layer, which is the invisible, selectable text positioned over the rendered canvas.
With the Serif bit absent it selects sans-serif for a serif document, then
compensates for the resulting width difference with a per span
transform: scaleX(k).
Measured on page 1 of the document above, at 100 percent zoom:
- 271 text spans, of which 155 carry a
scaleXcorrection scaleXminimum0.789, median0.933, maximum1.179- 92 of those 155 spans deviate from
1.0by more than 5 percent
The correction normalises each span's total width, but the per character error is not uniform, so glyph positions inside a span drift progressively. The selection highlight therefore separates from the glyphs painted on the canvas, slightly at the start of a run and noticeably by the end. It is most obvious on justified text, where runs are longest.
Setting the Serif bit lets the viewer fall back to a serif face with much closer
metrics, which brings scaleX near 1.0 and the highlight back onto the glyphs.
Reproduction
- Generate any PDF with dompdf using a serif font family.
- Inspect the font descriptors, for example
qpdf --qdf --object-streams=disable out.pdf - | grep -A12 /FontDescriptor. Every/Flagsis32. - Open the file in any pdf.js based viewer with the text layer enabled and select a paragraph. The highlight boxes sit to the right of the glyphs, increasingly so toward the end of each line.
Suggested fix
The information needed is already available. dompdf/php-font-lib parses the OS/2
table including sFamilyClass and panose
(src/FontLib/Table/Type/os2.php, lines 33 and 34). Either is a standard way to
classify a face:
sFamilyClasshigh byte in the range 1 to 7 indicates a serif familypanose[1](Serif Style) distinguishes serif from sans serif
So the Serif bit could be derived rather than omitted:
if ($isSerif) {
$flags += pow(2, 1); // Serif, bit 2 per PDF 32000-1 Table 123
}Falling back to the current behaviour when the OS/2 table is missing or ambiguous would keep this safe for fonts that cannot be classified.
Happy to open a PR if the approach looks reasonable.
Environment
- dompdf 3.1.6 (also confirmed against
master) - php-font-lib 1.0.2
- PHP 8.5
Source: dompdf/dompdf