Zum Inhalt springen

Anleitung

HTML in Python in PDF umwandeln

Ein vollständiges Python-Skript, das eine HTML-Datei an die API von Ironfang Render sendet und das zurückgegebene PDF speichert, mit der Fehlerbehandlung, die ein echter Job braucht.

Die Dateien herunterladen

  • html_to_pdf.py: das vollständige Skript, das unten gezeigt wird
  • requirements.txt: seine einzige Abhängigkeit, requests
  • invoice.html: die Beispielrechnung aus der Anleitung zu Rechnungs-PDFs, zum ersten Umwandeln

Installieren und den Schlüssel setzen

Das Skript braucht Python 3.9 oder neuer und das Paket requests. Erstellen Sie im Portal einen API-Schlüssel und legen Sie ihn in der Umgebungsvariablen IRONFANG_API_KEY ab statt in der Datei.

python -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
export IRONFANG_API_KEY="if_live_..."

Das Skript

"""Convert an HTML file to PDF with the Ironfang Render API.

Usage:
    export IRONFANG_API_KEY="if_live_..."
    python html_to_pdf.py invoice.html invoice.pdf

The HTML is sent to the hosted API and the PDF comes back in the response.
Needs Python 3.9 or newer and the requests package.
"""

import os
import sys
from pathlib import Path

import requests

API_URL = "https://api.ironfang.uk/render/v1/pdf"

FOOTER = (
    '<div style="font-size:9px;width:100%;text-align:center;'
    'color:#666;font-family:sans-serif">'
    'Page <span class="pageNumber"></span> of <span class="totalPages"></span>'
    "</div>"
)


def main() -> None:
    if len(sys.argv) != 3:
        sys.exit("Usage: python html_to_pdf.py INPUT.html OUTPUT.pdf")
    source, target = Path(sys.argv[1]), Path(sys.argv[2])

    key = os.environ.get("IRONFANG_API_KEY")
    if not key:
        sys.exit("Set IRONFANG_API_KEY to your Ironfang API key.")

    body = {
        "html": source.read_text(encoding="utf-8"),
        "paper_format": "a4",
        "print_background": True,
        # Inches, per side. The bottom margin leaves room for the footer.
        "margin": {"top": 0.6, "right": 0.5, "bottom": 0.8, "left": 0.5},
        # A footer on its own makes Chromium print its default date and
        # title header, so an empty header goes with it.
        "header_html": "<span></span>",
        "footer_html": FOOTER,
    }

    try:
        resp = requests.post(
            API_URL,
            headers={"Authorization": f"Bearer {key}"},
            json=body,
            timeout=60,
        )
    except requests.RequestException as exc:
        sys.exit(f"The request did not complete: {exc.__class__.__name__}")

    content_type = resp.headers.get("Content-Type", "")
    if resp.status_code != 200 or not content_type.startswith("application/pdf"):
        # Errors are JSON even though success is binary. Never save them
        # as the PDF.
        try:
            err = resp.json()["error"]
            detail = f"{err['code']}: {err['message']}"
        except (ValueError, KeyError, TypeError):
            detail = f"unexpected response ({content_type or 'no content type'})"
        retry = resp.headers.get("Retry-After")
        hint = f" Retry after {retry} seconds." if retry else ""
        sys.exit(f"HTTP {resp.status_code} {detail}.{hint}")

    target.write_bytes(resp.content)
    credits = resp.headers.get("X-Renderwolf-Credits", "?")
    print(f"Wrote {target}: {len(resp.content):,} bytes, {credits} credits used.")


if __name__ == "__main__":
    main()

Ausführen

python html_to_pdf.py invoice.html invoice.pdf

Bei Erfolg gibt es die Größe und die verbrauchten Credits aus, und invoice.pdf ist eine einseitige A4-Rechnung mit einer Seitenzahl in der Fußzeile:

Wrote invoice.pdf: 69,936 bytes, 2 credits used.
Das PDF, das das Skript aus invoice.html erzeugt: Rechnung INV-1042 von Northwind Studio Ltd, sechs Positionen und ein fälliger Gesamtbetrag von 13.410,00 Pfund, mit "Page 1 of 1" in der Fußzeile.
Die erste Seite des PDFs, aus invoice.html mit den Optionen im Skript gerendert.

Was die Einstellungen bewirken

  • paper_format: a3, a4, a5, letter, legal oder tabloid. Ohne Angabe ist die Seite Letter.
  • print_background: true behält Hintergrundfarben und -bilder, die beim Drucken standardmäßig wegfallen.
  • margin: in Zoll, je Seite. Die Fußzeile wird innerhalb des unteren Rands gezeichnet, deshalb ist der untere Rand größer.
  • header_html und footer_html: Vorlagen, die auf jede Seite gezeichnet werden. Chromium füllt die Klassen pageNumber und totalPages. Eine Fußzeile schaltet beide ein, deshalb gehört eine leere Kopfzeile dazu; sonst erscheinen oben ein Datum und der Seitentitel.

Wenn es fehlschlägt

Ein Erfolg ist ein 200 mit Content-Type application/pdf, und nur dann wird die Datei geschrieben. Alles andere ist JSON in der Form {"error": {"code", "message"}}; das Skript gibt es aus und beendet sich mit Status 1. Einige der Fehler, die Ihnen begegnen können:

HTTP 401 invalid_api_key: missing or unknown API key.
HTTP 400 bad_request: paper_format must be one of a3, a4, a5, legal, letter, tabloid, got "a6".
  • 429 rate_limited: zu viele Renderings pro Minute für das Konto. Warten Sie und senden Sie die Anfrage erneut; das Skript gibt Retry-After aus, wenn die Antwort diesen Header enthält.
  • 429 quota_exhausted: Die Credits des Monats sind aufgebraucht. Das Rendern pausiert, statt Mehrverbrauch abzurechnen.
  • 422 render_failed: Die Seite konnte nicht gedruckt werden, zum Beispiel weil sie nicht rechtzeitig geladen wurde. Die Credits dafür werden erstattet.

Das Skript wiederholt Anfragen nicht von selbst. Wenn Sie Wiederholungen einbauen, wiederholen Sie nur Antworten mit 429 und 5xx, warten Sie zwischen den Versuchen und lassen Sie 400 und 401 aus: Sie schlagen jedes Mal auf dieselbe Weise fehl. Fehlermeldungen enthalten nie den Schlüssel oder das Dokument.

HTML aus Ihrer Anwendung umwandeln

Das Skript liest eine Datei, aber body["html"] kann jeder String sein, den Ihr Code erzeugt, etwa ein gerendertes Jinja-Template. Maskieren Sie jeden Wert, den Sie in das HTML einsetzen, so wie es die Anleitung zu Rechnungs-PDFs mit html.escape zeigt.

Wann eine lokale Bibliothek besser passt

Python-Bibliotheken wie WeasyPrint rendern HTML auf Ihrem eigenen Rechner mit einer eigenen Layout-Engine zu PDF, und Playwright steuert ein lokales Chromium. Sie vermeiden einen Netzwerkaufruf und halten Dokumente in Ihrer Infrastruktur, um den Preis, dass Sie die Engine, ihre Schriften und Systemabhängigkeiten installieren und aktualisieren und ihr den Speicher und die Zeit geben müssen, die ein Browser braucht. Eine gehostete API passt zu einem Dienst, der selbst keinen Browser betreiben soll; eine lokale Engine passt zu Offline-Arbeit oder zu Dokumenten, die Ihr Netzwerk nie verlassen dürfen.

Weiterlesen