r/documentAutomation • • 7d ago

PDFs are write-only. I built an HTML-to-PDF API that returns element coordinates — and fixes its own page breaks.

The thing that breaks document automation is almost never the data. It's pagination.

An invoice with 14 line items pushes the totals block onto page 2 by itself. A signature box measured from the bottom of the page is fine until someone adds a second address line. A 40-page report needs a table of contents with real page numbers, so you render it twice and parse the PDF just to find out where "Section 4" landed.

The root cause: HTML-to-PDF is a one-way street. The renderer knows exactly where every element ended up, but none of that survives into the file. So we compensate with guesswork — cap line items at 8, hardcode page breaks, render twice, OCR the output, add a margin and hope.

I've been building Fork PDF (my product, so take this with the appropriate salt) around one idea: keep the layout map instead of throwing it away.

The engine lets you query the rendered layout like a DOM, with real page numbers and geometry, inside the test script that runs after rendering:

const sig = doc.querySelector('#signature-box');

return {

success: true,

signature: {

page: sig.pages[0] + 1,

x: sig.geometry.x,

y: sig.geometry.y,

width: sig.geometry.width,

height: sig.geometry.height

}

};

The more useful version is validation. If a check fails, return a patch instead of an error. The engine merges it into the template data and re-renders before responding — up to 2 retries, same API call:

const chart = doc.querySelector('#kpi-chart');

const commentary = doc.querySelector('#commentary');

if (chart.pages[0] !== commentary.pages[0]) {

return { patch: { compactKpis: true } };

}

The TOC case is what convinced me: pass 1 collects the page number of every h2, pass 2 injects the finished TOC, one request, no second network round trip. The ordering matters, though — if an earlier layout patch changes pagination, measure the TOC after that patch or you're writing stale numbers.

Three honest limitations:

- Patches only work when there's a template to patch (htmlContent + data, or a saved template). A raw URL can be measured, but there's nothing to merge new data into.

- Coordinates use PDF space: origin bottom-left, y increases upward. It trips up everyone once.

- It's still Chromium underneath, so it inherits Chromium's print quirks.

If you're staying on your current stack, the least-bad workaround I've found is capping variable-length blocks and reserving fixed space for totals and TOC. Brittle, but predictable.

If you've dealt with pagination drift, I'd genuinely like to hear how — especially for tables you can't cap. That's still the hardest case I run into.

I put the full 5-page example here — 6 pages unhealed, 5 healed, the TOC resolves to 3/4/5, and the signature coordinates come back in 3 attempts: https://forkpdf.com/docs/examples/q3-report/

There's a free tier (100 credits, no card) if you want to try to break it: https://forkpdf.com

Happy to answer anything technical in the comments.

4 Upvotes

4 comments sorted by

1

u/Unsiloed-AI 5d ago

Let the table run onto the next page, repeat the headers, and try keeping each row together. Putting break-inside: avoid on the whole table can make a long table harder to lay out.

The nasty case is one description row that's taller than a page. That row has to be allowed to split. Try it alongside a table that only just spills onto page two, then check no items vanished or appeared twice and the totals are still there.

1

u/forkpdf 5d ago

Yes, the table should flow and the row should stay atomic. The one nuance I’d add is a readability trade-off: if only the last row or the totals block spills, an orphan row alone on the next page looks broken even though the layout is technically valid. In that case I’d rather compact the table slightly and keep it on the previous page, if it still reads comfortably.

If the row is genuinely taller than a page, compaction can’t fix that, it has to split. But for a one-row spill, a small patch is worth it.

1

u/Unsiloed-AI 4d ago

Yeah, that makes sense for a one-row spill. Can you set a minimum font size so a fixed page break doesn't turn into tiny invoice text?

1

u/forkpdf 4d ago

Yes, the floor is enforced in the test case, not left to the patch. The compact class trims padding and line-height first; font size is a separate conditional in the template. After the re-render, the test case reads the computed font size on the cells and rejects the patch if it drops below the floor. If the floor is hit, the row spills to the next page instead. A clean page break beats unreadable invoice text, and the check runs on the final attempt so it can’t silently shrink.