deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

pdf-lib's standard fonts can't encode non-WinAnsi names, and the common strip-fix silently drops them

A dev.to post details how pdf-lib's built-in fonts fail on non-WinAnsi characters such as Ł and ź, and how the common fix of stripping them before drawing silently corrupts user data in generated PDFs.

pdf-lib's standard fonts can't encode non-WinAnsi names, and the common strip-fix silently drops them

How a client's name lost two letters

A developer writing on dev.to has documented a data-corruption trap in pdf-lib, a widely used Node.js library for generating PDFs. While producing invoices in a serverless function, the author discovered that a client's company name, Łódź Sp. z o.o., appeared on the finished invoice as ód Sp. z o.o. The Ł and the ź were gone. The PDF was structurally valid, opened without complaint in any reader, and produced no log output — the only way to catch it was to look at the rendered page.

The standard fonts stop at Western European Latin

Every conforming PDF reader is guaranteed to support fourteen standard fonts, which is why pdf-lib tutorials almost always begin with something like embedFont(StandardFonts.Helvetica): no font files to ship, no licensing to consider. According to the post, those fonts are encoded in WinAnsi — approximately Windows-1252 — and their glyph set covers Western European Latin and nothing beyond it.

The author probed the boundary rather than assuming where it sat. The euro sign, curly quotes and dashes all draw fine, which is the range developers tend to worry about. Czech ř, the Turkish dotless ı, Cyrillic text and CJK characters all fail. In other words, the characters that appear in real names, addresses and company registrations are exactly the ones outside the set.

The workaround that corrupts data

Left alone, pdf-lib behaves well: attempting to draw Ł with a standard font raises an explicit error stating that WinAnsi cannot encode the character. The hazard is the obvious response to that crash. The author — like many developers before them — added a regex stripping every character outside the basic Latin-1 range before the text reaches drawText. The exception disappears, the test suite goes green, and the code proceeds to delete fragments of user data from a document destined for a third party.

At that point there is nothing to notice. The PDF is valid and the logs are quiet, and unit tests comparing strings cannot see a defect that exists only in the rendered output.

Embedding a real font

The proper fix, the post explains, is to embed a TrueType or OpenType font through @pdf-lib/fontkit, which removes the dependence on a 1990s encoding table. Several practical notes accompany it:

  • registerFontkit is mandatory; calling embedFont on raw font bytes without it throws an error whose connection to the underlying problem is not obvious.
  • Subsetting matters more than expected. A full weight of the Archivo typeface is roughly 180 KB, so embedding three weights unsubsetted would add about half a megabyte to every document. With subset: true, the author's four-page invoice carrying three weights came out at 35 KB, since only the glyphs actually used are embedded.
  • Font bytes should be read once and cached in a module-level variable, so warm serverless invocations do not pay the file-read cost on every call.
  • Licences must be checked before vendoring a typeface. Archivo is under the SIL Open Font License, which permits embedding, while many commercial fonts do not.

Coverage is still not guaranteed

Embedding solves the encoding problem, not coverage. Archivo contains no CJK glyphs, so a Japanese company name still throws at widthOfTextAtSize. The author's advice is to keep a fallback, but make it visible: replace an undrawable character with a space rather than deleting it, so a human proofreading the document has a chance of spotting the gap.

Bugs that only rendering reveals

With a rasteriser already pointed at the output, the author found two layout defects that no amount of code reading would surface. A six-figure total, right-aligned at 20pt in a column sized for four digits, ran backwards over its own "TOTAL DUE" label. Separately, a long company name continued straight past the right edge of the page. drawText positions text at absolute coordinates with no concept of a container, so text neither wraps nor warns — it simply draws.

The recommendation is to add a rasterisation step to the test loop — the post uses PyMuPDF, which needs no system dependencies — and then actually open the resulting image. The author calls it the only test that catches this class of bug.

Why it matters

pdf-lib is a default choice for generating invoices, receipts and tickets in Node, and those documents are routinely assembled from text users typed themselves. Any of that text can contain characters beyond WinAnsi, and the two failure modes are both bad: a hard exception in production, or — after the popular strip-the-characters fix — silent corruption of a customer's name in a document going out under your brand. The defences are cheap: embed a subsetted font via fontkit, substitute visible placeholders for undrawable characters, and render representative output to an image in CI. The author, who is building HourToBill — a product where the invoice is the product — suggests spending twenty minutes checking what your generator does with Łódź.

  • #pdf-lib
  • #node-js
  • #pdf
  • #unicode
  • #javascript

Related posts