#3746·dompdf

FontDescriptor /Flags never sets the Serif bit, which misaligns text selection in pdf.js viewers

Author: zendevioCreated Aug 21, 2026Updated Aug 21, 2026

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:

php
// 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-sybolic

Bit 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 yes

Every /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 scaleX correction
  • scaleX minimum 0.789, median 0.933, maximum 1.179
  • 92 of those 155 spans deviate from 1.0 by 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

  1. Generate any PDF with dompdf using a serif font family.
  2. Inspect the font descriptors, for example qpdf --qdf --object-streams=disable out.pdf - | grep -A12 /FontDescriptor. Every /Flags is 32.
  3. 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:

  • sFamilyClass high byte in the range 1 to 7 indicates a serif family
  • panose[1] (Serif Style) distinguishes serif from sans serif

So the Serif bit could be derived rather than omitted:

php
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