Series: Building PdfWord — a free, no-backend PDF tools site (Part 8)
My first invoice generator worked. It produced a correct PDF with correct totals. And it looked… fine. Like a spreadsheet that learned to wear a tie.
Then a user sent me a screenshot of an invoice they'd made for their actual shop — and the "TOTAL DUE" line had a purple divider striking straight through the text. A real business, a real invoice, and my layout bug sitting in the middle of it.
That screenshot redesigned the whole thing. Here's the before-and-after story, plus the pdf-lib techniques behind the premium version.
Try it: Invoice Generator
The bug that started it
The totals section drew a divider line before rendering the TOTAL DUE row:
tr('Discount', -t.disc); // divider drawn at yy + 8... pg.drawLine({ start: { x: tx0, y: yy + 8 }, end: { x: tx1, y: yy + 8 }, thickness: 1.5 }); // ...then text drawn AT yy — the line lands mid-glyph on 12pt text tr('TOTAL DUE', t.grand, true); Text drawn at baseline yy extends roughly 12pt upward. A line at yy + 8 lands in the middle of the glyphs. On screen it read as a strikethrough. The fix was obvious once seen — but I'd shipped it, QA'd it, and never noticed, because my test data never made me look at that row the way a shop owner does.
The redesign: five decisions
1. The total became a band, not a line. Instead of a divider above "TOTAL DUE," the total now sits on a solid indigo band with white text. No line left to misplace — and it anchors the whole page visually.
2. The table header got inverted. Light header with colored text became a solid indigo header with white text. One drawRectangle, then white drawText on top. It instantly reads as "designed" instead of "default."
3. Zebra striping. Every other row gets a whisper-light background. Three lines of code, enormous readability payoff:
it.forEach((r, i) => { if (i % 2 === 1) pg.drawRectangle({ x: M, y: yy - 6, width: TW, height: 22, color: LT }); // ... draw the row's text on top }); 4. The header became a real header. Invoice number, issue date, and due date each got small-caps labels (INVOICE NO., ISSUE DATE, DUE DATE) right-aligned in the band — the way real invoices look, instead of a number floating in the corner.
5. A footer with manners. "Thank you for your business!" centered above the generator credit. Invoices are correspondence; they should sign off properly.
The unglamorous hero: right-alignment math
The fiddliest part of invoice layout is right-aligned numbers. pdf-lib won't do it for you — you measure the text and subtract:
const RX = (s, sz, ft) => W - M - ft.widthOfTextAtSize(String(s), sz); pg.drawText(amount, { x: RX(amount, 12, boldFont), y, size: 12, font: boldFont }); Every monetary value on the page goes through this helper. Get one wrong and the column wobbles. I use it religiously now.
One encoding gotcha
The pretty minus sign − (U+2212) is not in WinAnsi encoding — pdf-lib's standard Helvetica fonts will choke on it. Use ASCII - in PDFs and save the nice typography for the HTML preview. (The em-dash —, U+2014, is in WinAnsi, so that one's fine.) I keep a mental allowlist now.
The result
Same inputs, same pdf-lib, same browser. The difference is about 80 lines of layout code — and actually looking at the output the way a customer would. The shop owner who sent the screenshot now uses it for real invoices, in PKR with 16% tax.
That's the whole story: correctness gets you shipped, but the last 10% of layout is what makes someone trust it with their business.
Try it: Invoice Generator — PKR, USD, EUR and more, tax + discount, live preview, no signup.
What's the most embarrassing visual bug you've ever shipped? I just showed you mine.
![Why Object.keys(user) Returns string[], Not (keyof User)[]: TypeScript Means It](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2q4w48b9bzs43tynyhsf.png)
