PDFPipe

Fonts / What happens to the text afterwards

Why searching a PDF misses words containing fi and fl

A ligature is one glyph standing for two or more characters, and unless the font tells the reader which characters those were, the text comes back out wrong.

What is actually going on

When a font substitutes a single glyph for the letters f and i, the document now contains one glyph where the text had two characters. Extracting text from that document means mapping every glyph back to the characters it represents, and that mapping is a table inside the file. Where the table is present and correct, the ligature comes back out as f followed by i and searching for the word works. Where it is missing or wrong, the extracted text contains something else: a private-use codepoint, a single wrong character, or nothing. The symptom is specific and baffling: a document where searching for most words works and searching for words containing certain letter pairs silently fails, and the visible page looks perfect throughout.

How fonts reach this API

There is no font option on the render. A font arrives the way every other subresource arrives: your @font-face names a URL, the renderer fetches it while the document loads, and what was used is embedded in the output. So there is no font upload, no list of available fonts, no substitution control and no subsetting switch. It also means the two failures that actually stop a font are network-shaped rather than typographic: the URL has to be absolute and publicly reachable, and the fetch has to finish before the page is captured. Everything else on this page is decided in your stylesheet and in the font file itself.

The decisions

Reasons rather than a description of the code. Each of these has a default, and the default is chosen by the engine rather than by you.

  • Test extraction, not appearance. This failure is invisible on the page and only shows up in the extracted text.
  • Keep standard ligatures on for normal typesetting, since a font with a correct mapping handles them fine and turning them off makes the type worse for no gain.
  • Turn ligatures off in fields that must extract exactly: reference numbers, identifiers, account numbers, code. Those are the places where a wrong character costs something real.
  • Be more cautious with discretionary ligatures and stylistic alternates than with standard ones. They are less commonly mapped correctly because they are less commonly used.
  • Where a document has to satisfy a standard that requires every glyph to map to Unicode, this is the specific thing that check is checking, and it is checked by a validator rather than by eye.
  • If a font extracts badly and you cannot fix the font, changing the font is the fix. There is nothing to do about it in CSS beyond disabling the substitution.

What it looks like

A fragment, with the wrong version kept in a comment where the wrong version is the thing people actually write.

css
/* Normal prose: standard ligatures are correct typography, and a font
   with a correct mapping extracts them properly. */
.prose { font-variant-ligatures: common-ligatures; }

/* Anything that has to extract character for character: no substitutions.
   A reference number is read by a machine as often as by a person. */
.ref,
.account-number,
code, pre {
  font-variant-ligatures: none;
  font-family: "IBM Plex Mono", ui-monospace, monospace;
}

/* Discretionary ligatures are the least reliably mapped. Switch them on
   deliberately and only where you have checked extraction.
   .display { font-variant-ligatures: discretionary-ligatures; } */

What people do instead

Proofreading the rendered page and concluding the text is fine. The page is fine: the glyph is exactly the right shape. What is broken is a table nobody looks at, and the people who find it are the ones searching the archive two years later for a word that happens to contain fi.

How to find out rather than assume

Render the document, select all the text in a reader, paste it into a plain text editor and search it for a word containing fi, fl or ffi. If the word is not there, or has the wrong characters in it, the mapping is wrong. Do the same for every identifier field, where the cost of a wrong character is highest.

Frequently asked

Can I upload a font instead of serving one?

No. There is no font upload and no font list. The renderer fetches what your @font-face points at, so the file has to be somewhere it can reach over the network. In practice that means a public URL on your own asset host or a font service, and it means a font on a private path or behind an authenticated endpoint fails to load silently and the fallback is used instead.

How do I know which fonts ended up in the document?

Open the finished file in a reader and look at its document properties, which list the embedded fonts. That list is the truth: it says whether the face you meant was used, whether a fallback was substituted, and how many faces the document is carrying. It is a five second check and it catches most of the failures in this cluster.

Why does the font work in my browser and not in the render?

Almost always because the URL resolves for you and not for the renderer. A relative path, a local file, a development host, or an asset behind a login all work while you are looking at the page and none of them work when a machine somewhere else loads it. The fallback is then used and nothing reports an error, which is why the symptom is a document that looks subtly wrong rather than a render that failed.

Related font topics

The topics that cause each other, then the rest of the same group.

Fonts reach a render the way every other subresource does, over the network from the URL you named. Render the document and look at what actually got embedded.