Fonts are where invoice PDFs fail first
Put a customer name in Arabic, Hebrew or Japanese into most invoice tools and the PDF comes back with empty rectangles where the name should be. The cause is dull and specific. When fontconfig has no better match for a script, Chromium falls back to DejaVu Sans, and DejaVu Sans carries no Arabic glyphs at all. Nothing errors, nothing warns. The render succeeds and the document is wrong, which is the worst of the two possible outcomes.
The renderer behind this page installs the Noto core families alongside Noto CJK and Noto Sans Mono, and a script we run against the built image asserts that six scripts each resolve to their own family rather than to the fallback. The six are Latin, Cyrillic, Chinese, Japanese, Arabic and Hebrew.
Your browser draws the preview with the same faces. They are vendored into this site as woff2 subsets carrying a unicode-range, so a Latin invoice never downloads the Cyrillic file, and a Cyrillic one fetches that subset the first time a character in its range is actually used. The outlines on screen and the outlines on paper come from the same files.
Why the lines can add up and the total still look wrong
Money arithmetic on an invoice is not addition. It is a chain of roundings, and where you round changes the answer.
Take four lines sharing one tax rate. Round the tax on each line and add the four results, and you can land a cent or two away from that rate applied to the total. The rate summary at the bottom then disagrees with the sum of the lines, and the invoice needs a note explaining its own arithmetic. This generator groups the lines by rate, sums the net amounts in each group, and rounds once. Each line then gets its share of that single figure, allocated so the shares add back to it exactly.
A discount across the whole invoice has the same shape of problem. If it sits outside the lines and the invoice carries two different rates, the discount belongs to neither of them, and the tax depends on the order somebody chose to apply things in. So the discount is spread over the lines first, by largest remainder, and tax is worked out on what is left.
None of it runs in floating point. Every intermediate is a whole number of minor units, basis points or thousandths, held as a bigint, because a line subtotal is unit price times quantity and that leaves the safe integer range behind well before either number looks unreasonable. Doubles would round quietly, and they would round differently in your browser than on our server. That is the one outcome this has to avoid.
What happens when the table runs past the bottom of the page
A twelve-line invoice fits on one page. A sixty-line one does not, and that is where layouts come apart.
Three rules in the stylesheet do the work. The table head is a table-header-group, so it repeats at the top of every page instead of appearing once and leaving page two onwards as unlabelled columns of numbers. Every row is break-inside: avoid, so a row carrying a three-line description does not get sliced through the middle. The totals block carries the same rule, because an invoice that splits between its last line item and the amount due sends the reader hunting for the half they cannot see.
Page numbers come from Chromium's own footer band rather than from the document, and they have to. Chromium does not support CSS page counters, so a document cannot count its own pages. The footer carries the invoice number on the left and the page position on the right, drawn by the print pipeline once pagination has settled.
The preview is the document, not a picture of it
Most invoice tools draw the preview with one renderer and export with another: HTML on screen, a PDF library on the server. The two agree until a long address wraps differently, and then the file you send is not the one you approved.
Here both come from the same package. Your browser imports it to draw what you see, and the Cloudflare Worker that handles the download imports the same code to build the request that goes to the renderer. The page geometry lives in one file and is used in both places: A4 at 210 millimetres, the same top and bottom margins, the same horizontal inset. The left and right print margins are zero on purpose, so a template can run a colour band the full width of the paper and the document insets its own content instead.
Your logo is read in the browser and never uploaded on its own. It travels inside the invoice as a data URL when you press download, capped at 700,000 characters, which is a generous logo and nowhere near enough to slow a render down.
The finished PDF is kept for an hour and then deleted, rather than for the thirty days the account behind this tool would otherwise allow. The documents are yours and the purpose ends when your browser has the file, so there is nothing worth keeping after that. The privacy page spells out the rest.
When a form stops being enough
This page is one invoice at a time, typed by hand. For a few a month that is the right shape and an API would be overhead.
If your invoices come out of a database, a billing run or a subscription cycle, the same renderer takes a stored template and a payload of data. Store the layout once, post the numbers for each invoice, get back a PDF and a page count. The quickstart takes about five minutes, and the free tier covers a hundred pages a month.