Matterhorn 31-016 · PDF/UA-1 UA1:7.21.5-1

Glyph widths that disagree with the embedded font

BlockerCheckpoint 31: FontsWCAG 1.3.1 (A)

The /Widths array must agree with the embedded font program.

The condition, as the protocol states it. “For one or more glyphs, the glyph width information in the font dictionary and in the embedded font program differ by more than 1/1000 unit.”

Matterhorn Protocol 1.1, checkpoint 31, index 31-016, section UA1:7.21.5-1. Software can decide this one on its own.

What a screen reader actually does

Text positioning drifts, which corrupts geometry-based reading-order inference.

What the condition is

A glyph’s width is recorded twice in a PDF. The font program has its own advance width for every glyph — in a TrueType font, the hmtx table, in font units. The font dictionary repeats them in thousandths of an em: /Widths for a simple font, indexed from /FirstChar, or /W and /DW in the descendant of a CID font. PDF/UA-1 requires the two to agree, to within one unit.

The file Taggart’s test suite uses for this page has a TrueType font whose program advances every glyph 600 units. Its dictionary says otherwise for B:

<< /Type /Font /Subtype /TrueType
   /BaseFont /BatchJWidths
   /FirstChar 65 /LastChar 67
   /Widths [ 600 400 600 ]      % A, B, C — the program says 600, 600, 600
   /Encoding /WinAnsiEncoding
   /FontDescriptor … >>         % /FontFile2: hmtx B = 600

Why two numbers for one thing

Because they do different jobs. A viewer positions text with the dictionary: after drawing B it moves along by B’s width from /Widths, and starts C there. It draws with the program: the shape of B comes from the outline, whatever width the outline was designed for. When the numbers agree, nobody can tell which source was used. When they don’t, both are used, for different halves of the same operation.

In Poppler’s rendering of the test file, C starts a fifth of an em early and its outline runs into B’s. The page shows A and then one joined shape.

What a screen-reader user hears

In the test file, ABC: the characters come from the encoding, not from the widths, and nothing about the order changes.

Where it starts to matter is word boundaries. A screen reader gets text from an extraction layer that has to decide where words end, and it does that by measuring the gaps between glyphs — with the dictionary’s widths, since those are what position text. Some producers place each glyph or each word explicitly, at the advance their layout engine used, which is the program’s. If the dictionary then says a glyph is narrower, the extractor sees a gap after it that is not on the page.

The test suite checks this directly: the same font, with A, B and C each placed at the program’s 600-unit advance. With correct widths, pdftotext and Taggart’s own text reader both read ABC. With B recorded as 400, both read AB C. A sighted reader sees one word; a screen-reader user hears two. The opposite error — a dictionary width wider than the outline — merges what the eye sees as separate words.

Where it comes from

A font program embedded after the fact. The widths were written for the font the document was made with; a later tool embeds a different version of it — the copy installed on the machine doing the embedding — and keeps the original dictionary. This is the hidden cost of fixing an unembedded font (31-009) with whatever is to hand.

Widths written in the wrong units. TrueType fonts often have 2048 units per em, and /Widths is in thousandths. A generator that copies advances without scaling writes every width roughly twice as large as it should be.

An off-by-one /FirstChar. If the array starts one code too early or too late, every glyph gets its neighbour’s width. In a proportional font that is wrong almost everywhere and right by coincidence wherever two neighbours happen to match.

How to check it free

Look at the letter spacing. Letters touching or overlapping, or a regular gap inside words, in a font that is otherwise rendered cleanly, is the visible symptom. Then copy a line: a space inside a word, or two words run together, is the extraction symptom.

Acrobat Pro has a tool Adobe describes for this case. In Adobe’s guide to advanced preflight inspections, browsing a document’s fonts is offered as a way to find the source of preflight problems such as mismatches caused by inconsistent glyph widths. Select Use print production → Preflight from the All tools menu, then Options → Browse Internal Structure of All Document Fonts. For one font, expand Fonts under Results → Overview, select it, and choose Browse Internal Font Structure from the Options menu.

veraPDF compares the two widths glyph by glyph under clause 7.21.5, allowing a difference of one unit, and leaves out text in rendering mode 3.

How to fix it free

Re-export from the source. Positions and widths are then computed from the same font in the same pass, which is the only way to be sure the text lands where it did before.

Acrobat cannot edit the widths. Adobe’s page on the font browser says you can navigate fonts in it but not edit them, and Adobe documents no fixup that rewrites a width array.

For a simple TrueType font with WinAnsi encoding, the widths can be recomputed from the program with the free Python libraries pikepdf and fontTools:

import io
import pikepdf
from fontTools.ttLib import TTFont

with pikepdf.open("in.pdf") as pdf:
    for font in pdf.objects:
        if not (isinstance(font, pikepdf.Dictionary) and font.get("/Subtype") == "/TrueType"
                and font.get("/Encoding") == "/WinAnsiEncoding"
                and "/FontFile2" in font.FontDescriptor):
            continue
        tt = TTFont(io.BytesIO(bytes(font.FontDescriptor.FontFile2.read_bytes())))
        cmap, scale = tt.getBestCmap(), 1000 / tt["head"].unitsPerEm
        widths = []
        for code in range(int(font.FirstChar), int(font.LastChar) + 1):
            glyph = cmap.get(ord(bytes([code]).decode("cp1252", "replace")), ".notdef")
            widths.append(round(tt["hmtx"][glyph][0] * scale))
        font.Widths = pikepdf.Array(widths)
    pdf.save("out.pdf")

On the test file it turns [600 400 600] into [600 600 600]. Correcting a width moves every glyph set after it in the same run, so compare the page before and after: if the producer laid text out with the wrong widths, the fixed file will look different, and re-exporting is the better repair.

What Taggart does

Taggart does not detect this condition. It reads the widths in font dictionaries but does not open font programs, so it has nothing to compare them with. On the test file it reports nothing about the font; 31-016 is on the list of conditions Taggart asks a person to confirm before it claims conformance.

It is also exposed to the condition. Taggart measures text the way a viewer positions it, from the dictionary — that is what places reading-order badges, compares structure order with page order, and finds the word breaks in its screen-reader preview. On the per-glyph version of the test file, its preview reads AB C, exactly as pdftotext does. A wrong width misleads Taggart’s geometry as much as anyone’s, and no finding says so. Use the Acrobat font browser or veraPDF for this condition before you confirm it.

How Taggart handles it

Software could decide this one, and Taggart does not check it yet. A clean report from Taggart says nothing about it either way. It is on the list of conditions Taggart asks you to confirm before it writes the PDF/UA claim into the file; check it by hand or with another validator, as described above.

Whatever Taggart changes, the page still looks identical. Fixes edit the PDF object model, not the content streams, and every remediation is re-rendered and compared against the original at structural similarity ≥ 0.999 before you get the file back. A tool that quietly reflows your document is worse than no tool.

Frequently asked questions

Which width does a viewer actually use?

The dictionary's, to decide where the next glyph starts; the program's outline is what gets drawn. That is why a mismatch shows as letters running into each other or standing apart: the position comes from one source and the shape from the other.

How large a difference counts?

More than one thousandth of an em, which is the unit /Widths is written in, so any difference of a whole unit or more after scaling. Rounding a TrueType font's 2048-unit advances to thousandths stays inside that; a wrong font or a wrong scale does not.

Can it change what a screen reader says?

It can. Text extractors, including the ones screen readers rely on, decide where words end from the gaps between glyphs, measured with the dictionary's widths. If a producer placed each glyph at the program's advance and the dictionary says a glyph is narrower, a gap appears that is not on the page, and a word is read as two.

Can I correct the widths after the fact?

Yes, but it moves text: every glyph set after a corrected one shifts by the difference. Compare the pages before and after, and prefer a re-export, which recomputes positions and widths together.

Where this sits in the standards

Standards this condition maps to
StandardReference
Matterhorn Protocol 1.1Checkpoint 31 (Fonts), index 31-016
PDF/UA-1 (ISO 14289-1)Clause 7.21.5-1
WCAG 2.11.3.1 Info and Relationships — Level A
EN 301 549 / Section 508 / ADA Title IIAll three point at WCAG 2.1 Level AA for non-web documents, so a Level A or AA criterion here is in scope for each of them.

Other conditions in checkpoint 31

Checkpoint 31 covers fonts. These are its other failure conditions; the ones without a link are in the rule set but do not have a written page yet.

Check your own file. Taggart does not check this condition yet, and lists it for you to confirm. Its validator is free and checks 75 of the 87 Matterhorn conditions software can decide — no watermark, no expiry, and no account needed up to 60 pages.

Check a PDF free

Taggart does not check this condition yet. A generated file that fails it is in the repository, with a test that fails the day Taggart starts catching it. Written on . The Acrobat steps follow Adobe’s documentation and were not walked through by hand. Last updated .