DEV Community

EngelbertPierce7942
EngelbertPierce7942

Posted on

PDF Generation vs HTML Rendering: Print Layout Lessons from Logistics Invoices (and Why)

Short answer: HTML is a useful input format, but dependable invoice PDFs need a print-layout pipeline with explicit page rules, deterministic fonts, and visual checks. For a one-person logistics SaaS, I render only the documents that need archival fidelity; I keep ordinary previews in the browser.

The choice matrix for an invoice workflow

Need Browser HTML print Dedicated PDF layout engine Practical choice
Fast customer preview Strong Usually unnecessary HTML
Stable page breaks Variable across engines Stronger control PDF layout
Pixel-level branding Sensitive to fonts and margins Explicit coordinates and styles PDF layout
Low render cost at high volume Usually lower Can be higher Measure both

The recommendation is conditional: use HTML for the interactive preview, then use a controlled print path for the final invoice when a carrier, auditor, or customer depends on its pagination. This keeps the revenue-per-hour math sane. I can ship a weekly feature without making every page a graphics project.

The catch is that a second renderer creates another thing to test. If the invoice has no legal or operational need for fixed pagination, a browser print stylesheet is often the better engineering decision.

Why is PDF generation harder than HTML rendering for print layout?

HTML describes a flow of boxes that can reflow when a font, viewport, or margin changes. PDF describes a finished page: text and drawing commands are placed into a coordinate system, and the page boundary is part of the artifact. A browser can decide that a table row moves to the next page. A PDF pipeline must decide what to do with the row, its header, its footer, and any continuation marker.

That difference shows up in logistics data. A single order may contain 48 line items, a long consignee address, a hazardous-material note, and a tax summary. The address wraps differently when a fallback font is loaded. The note pushes the totals block below a page boundary. The output still looks acceptable in a casual preview, yet a warehouse printer clips the last two rows.

I once treated a 12-page invoice as “just HTML with a download button.” The first production sample had a repeated table header, but the footer overlapped it on page 7. The only useful signal was a rendered image diff, not a successful HTTP response. Small failures matter.

PDF also has details that HTML leaves to the user agent: embedded font subsets, color profiles, metadata, links, and accessibility structure. ISO 32000-2 defines the PDF format and its page model; an implementation that only checks that a file opens is not checking conformance or readability.

A small pipeline that keeps fidelity measurable

I split generation into four stages: normalize order data, produce a print-specific document model, render, then inspect. The model contains fixed fields such as invoice number, shipper, consignee, currency, line items, and totals. It does not contain arbitrary HTML from a customer. That boundary makes escaping and pagination predictable.

type LineItem = {
  sku: string;
  description: string;
  quantity: number;
  unitPrice: number;
};

type InvoiceModel = {
  invoiceNumber: string;
  shipper: string;
  consignee: string;
  items: LineItem[];
  currency: string;
};

export function buildInvoiceModel(order: {
  id: string;
  origin: string;
  destination: string;
  items: LineItem[];
}): InvoiceModel {
  return {
    invoiceNumber: `INV-${order.id}`,
    shipper: order.origin.trim(),
    consignee: order.destination.trim(),
    items: order.items.map((item) => ({
      ...item,
      description: item.description.trim(),
    })),
    currency: "USD",
  };
}
Enter fullscreen mode Exit fullscreen mode

The renderer receives this model and a versioned template. I pin the font files in the build artifact, set the page size and margins explicitly, and keep totals in a block that can move as a unit. A test fixture should include an empty item list, a long address, a description that wraps to three lines, and enough rows to force a page break.

The check is visual and structural. Extract text to verify invoice number and totals. Count pages. Render each page to an image and compare against a reviewed baseline with a small tolerance for anti-aliasing. Record renderer version, template version, and font hash beside the PDF. When a diff appears, that metadata tells me which input changed.

Where the browser path wins

Browser printing is the sensible runner-up when the document is a preview, when users can choose “print current view,” or when volume makes a heavyweight render queue dominate the budget. CSS paged-media rules such as break-inside and @page help, but support varies by engine, so test the exact browser used in production.

A dedicated layout engine is a poor fit for a live, highly interactive screen. It adds queueing, font packaging, and operational monitoring. Stick with HTML when a one-page packing slip can tolerate a page break moving by a line. Switch paths when a contract, customs form, or monthly statement must reproduce the same page boundaries years later.

I'm not sure one universal fidelity threshold exists; your mileage may vary with paper sizes, scripts, and printer drivers. Define the threshold with the people who consume the invoice, then measure render time and failure rate against that threshold.

Start with HTML preview and a narrow print stylesheet. Add a controlled PDF renderer for the smallest document set that has a real pagination requirement. Keep the input model stable, make fonts and templates versioned, and treat visual regression fixtures as part of deployment. Outsource the undifferentiated rendering work only after the acceptance tests describe what “correct” means. That approach spends complexity where it protects revenue: fewer support tickets from unreadable invoices, without paying a render cost for every screen in the product. It also leaves an escape hatch. If the PDF path becomes the bottleneck, the model and tests can move to another implementation without rewriting order logic. In a one-person operation, this matters because a renderer upgrade can consume an entire week that would otherwise go to billing or carrier integrations; I keep a small corpus of real-shaped fixtures and require the replacement to pass those files before changing the default.

Keep the rule boring.

References

Top comments (0)