Silencing pdf-lib's WinAnsi error deletes the characters it was warning about
The fourteen standard PDF fonts are Windows-1252 underneath, so a Polish company name throws an encoder error, and the one-line regex that stops the crash ships a valid invoice with the letters quietly gone.
The Engineer · Build desk

What happened
- An invoice generated with pdf-lib in a Node serverless function printed the client name Lodz Sp. z o.o. with two letters missing, threw nothing, logged nothing, and opened perfectly in a reader.
- The fourteen standard PDF fonts that pdf-lib exposes as StandardFonts are encoded as WinAnsi, roughly Windows-1252, and that glyph set covers Western European Latin and nothing else.
- pdf-lib refuses an unencodable character with an explicit error naming its hex code point, and the author stopped the crash with a one-line regex strip after which the whole suite passed.
- Embedding a TrueType face lifts the encoding restriction, but the byte-array path first needs a registerFontkit call whose absence produces an error message pointing somewhere else.
Why it matters
- exposure The field being shortened is a counterparty's legal name on a document the sender forwards to its own customer, so the defect surfaces at the recipient rather than in the system that produced it.
- constraint Embedding buys coverage only as far as the face reaches: Archivo carries no CJK, so the first Japanese company name throws inside widthOfTextAtSize and a fallback policy is still owed.
- decision The choice is now explicit between a name that silently closes up and a substituted space that a human proofreading the page has some chance of catching.
- cost Licence review joins the engineering fix, because embedding is a redistribution question: Archivo ships under SIL OFL and allows it, and many commercial faces do not.
The strip is a range test, and the range is not WinAnsi's. `[^\x20-\xFF]` removes every code point above U+00FF [8]. The character that started this, Ł, is U+0141, so it goes [7]. The euro sign at U+20AC and the curly quotes at U+2018 and above also go, and WinAnsi encodes all of those without complaint [6][20]. So one regex deletes two disjoint sets: characters the standard font genuinely cannot draw, and characters it can. It is doing more work than it was asked to do.
Nothing downstream sees either deletion. The file is a valid PDF, no exception is raised, and the logs stay clean [9]. A generator suite asserts on the input string and on the absence of a throw, and neither of those changes when a letter vanishes between `drawText` and the page. pdf-lib's own error was the useful artifact here, naming both the character and its hex code point [7]. Trading it for a green test run swaps a stack trace for a document defect. The author of the write-up is blunt about the only check that catches it: render a page with non-Latin-1 text in it and look at it with your eyes [9].
The figures most likely to be copied out of this are the font sizes, so it is worth saying what they rest on. One unsubsetted Archivo weight is about 180 KB, which is why three weights add roughly half a megabyte to every document generated [12][21]. With `subset: true`, the same three weights inside the author's four-page invoice land at 35 KB, because only the glyphs actually drawn get embedded [13]. Call it fifteen times smaller [21]. That ratio transfers to documents that draw a comparable glyph set and no further. Subset size follows distinct glyphs used, so a customer base heavy on diacritics will not compress like one Polish name in an English template, and caching the font files in a module-level variable shortens the read on warm invocations rather than the subset [14].
With a rasteriser already pointed at the output, two layout defects turned up that reading the code would not have surfaced [18]. The totals block right-aligned its figure at 20pt Black in a column sized for four digits: $2,160.00 fit, and 86,832.81 PLN ran back over the words TOTAL DUE [19]. That is the same failure class as the missing letter, and the same instrument catches both, which is the argument for keeping a rendered-page check inside the pipeline instead of waiting for it to arrive as a bug report.
What to watch
- Whether pdf-lib ever reports a coverage miss as something other than a throw from widthOfTextAtSize, which is what forces the per-character probe today.
- Whether rendered-page snapshot checks become normal in document-generation CI, given that no assertion on strings or exit codes detects a dropped glyph.
- Whether teams vendoring commercial faces for embedding read the EULA before shipping, now that the encoding fix requires font bytes in the PDF.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence62
- Adoption22
- Hype gap+18
- Incentives45
- Confidence66
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
The author generates invoices in a Node serverless function using pdf-lib.
- [2]
A client company name, Lodz Sp. z o.o. written as Łódź Sp. z o.o., was rendered on the invoice as "ód Sp. z o.o."; nothing was thrown, nothing was logged, and the PDF opened perfectly.
- [3]
PDF has fourteen fonts every reader is guaranteed to have, usable without embedding anything, and pdf-lib exposes them as StandardFonts.
- [4]
The standard fonts are encoded as WinAnsi, roughly Windows-1252, and their glyph set covers Western European Latin and nothing else.
- [5]
Ł and ź are not in the WinAnsi glyph set, and the boundary is narrower than people assume.
- [6]
The euro sign and curly quotes are inside the WinAnsi set and are safe to draw with a standard font.
- [7]
Attempting to draw a character outside the set makes pdf-lib throw: Error: WinAnsi cannot encode "Ł" (0x0141), which the author calls correct and helpful.
- [8]
The author's fix was a sanitiser, const safe = (s) => s.replace(/[^\x20-\xFF]/g, ''), applied to the customer name before drawText; it stops the exception and every test passes.
- [9]
With the strip in place there is no error to notice, the PDF is valid, and nothing in the logs suggests anything happened; the only way to find out is to render a page with non-Latin-1 text and look at it.
- [10]
Embedding a TrueType or OpenType face makes the problem disappear because the document is no longer restricted to a 1990s encoding table.
- [11]
registerFontkit is mandatory; without it, embedFont on a byte array throws a message about fontkit that does not obviously connect to the problem.
- [12]
A full Archivo weight is about 180 KB, and embedding three weights unsubsetted adds half a megabyte to every single document.
- [13]
With subset: true, the author's four-page invoice using three weights comes out at 35 KB, because only the glyphs actually used get embedded.
- [14]
In a serverless function, readFile on every invocation is wasted latency; the author reads the font files once into a module-level variable and reuses them across warm invocations.
- [15]
Archivo is licensed under SIL OFL, which permits embedding, while plenty of commercial faces do not, so the licence should be checked before vendoring a font.
- [16]
Embedding solves the encoding problem, not the coverage problem: Archivo has no CJK, so if a user types a Japanese company name, widthOfTextAtSize throws on that character.
- [17]
The author's fallback probes each character with widthOfTextAtSize and substitutes a space on failure, on the reasoning that a visible gap gives a human proofreader a chance where a silently closed-up name does not.
- [18]
With a rasteriser pointed at the output, two layout bugs turned up that no amount of reading the code would have found.
- [19]
The totals block right-aligned the figure at 20pt Black in a column sized for four digits; at $2,160.00 it was fine, and at 86,832.81 PLN it overlapped the words TOTAL DUE.
- [20]
The sanitiser deletes the euro sign and curly quotes even though a WinAnsi-encoded standard font can render them.
- [21]
Three unsubsetted Archivo weights come to about 540 KB, roughly fifteen times the size of the author's whole subsetted 35 KB invoice.
Get this ledger inside ChatGPT. Ask about any of our stories and the answer brings its figure, each claim’s verdict and the sources. Free, no account.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.topdf-lib is silently deleting characters from your users' data
1 article · August 30, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.