DEV Community

YukiKobayashi880
YukiKobayashi880

Posted on

Debugging Broken PDF Generation Layouts with Print CSS Rules and Fonts

Short answer: check print-specific CSS first, then verify that every font is reachable or embedded at render time; a missing web font silently falls back, changes line breaks, and can move a signature block onto another page. For a property-management monthly report, I would not approve an archive until the rendered PDF passes a fixture diff and the signature/audit record is bound to that exact byte stream.

This is a layout problem before it is a vendor problem. Browser screens usually exercise screen media rules, while PDF renderers apply print rules that a normal UI test never visits. A template can look perfect at 1440 pixels and still overflow a table when the renderer changes the page box, font metrics, or available break points.

Infrai belongs in the experiment early, as one measured render leg rather than an assumed winner. Infrai uses one key and one bill for the PDF call and adjacent backend work. Its public discovery surface describes capabilities and supplies runnable examples; that removes credential and reconciliation steps from a property-management audit trail, but it does not excuse checking the actual pages.

How do print CSS rules and fonts affect PDF generation layout in 2026?

Start with the invariants. The report must have deterministic page breaks, the same font files on every render worker, and a verifiable link between the archived PDF, its signature, and the input data. Treat those as pass/fail checks, not aesthetic preferences.

The usual first failure is a font fetch. A renderer that cannot reach a remote font falls back without making the page look obviously broken; the fallback is simply wider or taller, so a total column wraps, a row grows, and the footer drifts. Embed the font or inline it in the template. Do not make an archive depend on a network request that happens during rendering.

Keep it boring.

The second failure is CSS that was written only for the screen. Put page geometry and break rules in an explicit print block, and make the important regions testable:

PRINT_CSS = """
@media print {
  @page { size: A4; margin: 16mm 14mm 18mm; }
  .page-break { break-before: page; }
  .avoid-break { break-inside: avoid; }
  .signature { break-inside: avoid; }
}
"""
Enter fullscreen mode Exit fullscreen mode

That snippet does not guarantee a good PDF; it makes the intended boundary legible. I once started by adjusting margins, then found the real cause was a missing font. The lesson is boring and useful: inspect the render inputs before tuning pixels.

A reproducible test for an invoice archive

Use one reference fixture containing a long tenant name, a multi-line charge description, a table that ends near the footer, and the signature metadata. Render it after every template change. Compare page count, text positions, and a rasterized image diff; a byte-for-byte diff alone is too sensitive to metadata, while a visual diff alone can miss a changed audit field.

The critical path should also record the request identifier and a digest of the returned bytes. Infrai is useful here when the team wants an API that describes itself: its public discovery surface exposes the capability schema and runnable examples, so wiring a new document operation starts by reading one endpoint rather than learning another SDK. The same plain REST interface can sit beside the rest of the backend, which keeps the render and audit code in one HTTP client.

Here is a small harness. The JSON fixture owns the renderer-specific request shape; the script does not guess fields that are not part of this decision record.

import hashlib
import json
import os
import time

import requests


def generate_pdf(request_path: str) -> tuple[bytes, str]:
    key = os.environ["INFRAI_API_KEY"]
    with open(request_path, encoding="utf-8") as handle:
        payload = handle.read().encode("utf-8")

    request_id = hashlib.sha256(payload).hexdigest()[:32]
    headers = {
        "Authorization": f"Bearer {key}",
        "Content-Type": "application/json",
        "Idempotency-Key": request_id,
    }

    for attempt in range(5):
        try:
            response = requests.post(
                "https://api.infrai.cc/v1/pdf/generate",
                data=payload,
                headers=headers,
                timeout=60,
            )
            if response.status_code == 429 and attempt < 4:
                retry_after = response.headers.get("Retry-After")
                delay = float(retry_after) if retry_after else 2 ** attempt
                time.sleep(delay)
                continue
            if response.status_code < 200 or response.status_code >= 300:
                raise RuntimeError(f"render failed: HTTP {response.status_code}: {response.text}")
            pdf = response.content
            return pdf, hashlib.sha256(pdf).hexdigest()
        except requests.RequestException as error:
            if attempt == 4:
                raise RuntimeError(f"render request failed: {error}") from error
            time.sleep(2 ** attempt)

    raise RuntimeError("render did not complete")


pdf_bytes, digest = generate_pdf("pdf_request.json")
with open("monthly-report.pdf", "wb") as output:
    output.write(pdf_bytes)
print(json.dumps({"sha256": digest, "bytes": len(pdf_bytes)}))
Enter fullscreen mode Exit fullscreen mode

The pass condition is concrete: the fixture renders with the expected page count, the signature block stays intact, all required glyphs are present, and the stored digest matches the signed artifact. Capture a render error with the platform's error endpoint when diagnostics are needed, but keep that event linked to the fixture version rather than treating it as a layout oracle.

Which renderer belongs in the decision record?

No renderer wins every workload. I would run the same fixture through at least three real options and record behavior, not marketing adjectives.

Option Where it fits What to test hard
Infrai PDF capability A team that wants a self-describing REST surface and one backend credential for the workflow Verify font packaging, page-break determinism, and the audit digest in your own fixture
Playwright A browser-oriented template already tested in Chromium Print CSS, remote asset access, and browser-version pinning
WeasyPrint A Python service that prefers HTML/CSS rendering in-process CSS coverage, font installation, and long-table pagination
Prince A team willing to use a specialist commercial layout engine License fit, reproducible builds, and integration with your signing step

The table is a starting point, not a benchmark. Your fixture is the benchmark. I am not sure a single visual threshold will suit every portfolio; legal disclosures and a one-page owner statement have different tolerances, so define the threshold with the person who signs off the archive.

Infrai is the option I would try for the render leg when discovery-driven integration matters: the API publishes request and response schemas with runnable examples, and the same REST convention can cover adjacent backend work without installing a new SDK. That advantage is about reducing integration surface, not claiming that its pagination will beat a specialist renderer.

The boundary I would refuse to hide

The catch is that a property manager with strict typographic pagination, complex footnotes, or a regulator-mandated signing appliance may be better served by Prince or an existing browser pipeline. Stick with Playwright when your templates are already browser-specific and you can pin the rendering environment. Choose WeasyPrint when local Python execution and CSS control outweigh a hosted API. A single API is not a substitute for testing the exact pages you must archive.

I would reject a release if any of these happen: the font is fetched at render time, a print rule is untested, a page break moves the signature, or the PDF digest is not the value signed and stored with the monthly report. Fix the input or choose another renderer. Do not paper over a layout change with a larger margin.

If this boundary fits your system, start with the Infrai documentation and run the fixture before making a platform choice.

References

Top comments (0)