4 min di lettura
PDF impaginati da HTML con Chromium: interruzioni di pagina, intestazioni e indice con i numeri
Generare report e dossier in PDF partendo da HTML e CSS, con Playwright e Chromium: capitoli su pagina nuova, tabelle che si spezzano bene, numeri di pagina, font incorporati e un indice con i numeri di pagina veri.
- Automazioni
- Documenti
Report mensili, preventivi, fogli presenze, dossier tecnici: prima o poi ogni gestionale deve produrre un PDF che sembri fatto a mano. Le librerie che disegnano il PDF riga per riga (pdfkit, ReportLab, jsPDF) danno controllo totale, ma ogni tabella diventa un esercizio di coordinate. Da tempo uso un'altra strada: scrivo il documento in HTML e CSS e lo stampo in PDF con Chromium, pilotato da Playwright. Ecco cosa ho imparato impaginando un dossier di 26 pagine con copertina, indice, tabelle e schermate.
Il motore: Playwright e page.pdf
import { chromium } from "playwright";
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto("file://" + percorsoHtml, { waitUntil: "networkidle" });
await page.evaluate(() => document.fonts.ready); // aspetta i font prima di stampare
await page.pdf({
path: "dossier.pdf",
format: "A4",
printBackground: true, // senza, sfondi e colori delle tabelle spariscono
displayHeaderFooter: true,
headerTemplate: "<div></div>",
footerTemplate:
'<div style="width:100%;font-size:8px;padding:0 18mm;display:flex;justify-content:space-between">' +
'<span>Dossier del software</span><span>Pagina <span class="pageNumber"></span> di <span class="totalPages"></span></span></div>',
margin: { top: "18mm", bottom: "20mm", left: "18mm", right: "18mm" },
});
await browser.close();
Le classi pageNumber e totalPages nel piè di pagina vengono riempite da Chromium. Il template è isolato dalla pagina: non vede il tuo CSS e i font vanno indicati dentro lo stile in linea.
Le regole CSS che fanno la differenza
@page { size: A4; margin: 18mm; }
h2 { break-before: page; } /* ogni capitolo in cima a una pagina nuova */
h2, h3 { break-after: avoid; } /* mai un titolo da solo in fondo alla pagina */
p { orphans: 3; widows: 3; } /* niente righe orfane */
table { break-inside: auto; } /* le tabelle lunghe possono spezzarsi... */
tr { break-inside: avoid; } /* ...ma non a metà riga */
thead { display: table-header-group; } /* intestazione ripetuta su ogni pagina */
figure { break-inside: avoid; }
figure img { max-height: 110mm; } /* nessuna immagine più alta di mezza pagina */
L'ultima regola è la più sottovalutata. Un'immagine che non sta nello spazio rimasto salta alla pagina dopo e lascia un buco bianco. Uno screenshot verticale del telefono a tutta larghezza occupa una pagina intera: meglio affiancare tre schermate in una tavola unica, larga e bassa, che si incastra nel testo.
Unità: millimetri, non viewport
Per la stampa usa mm e pt. Il vh in stampa non corrisponde alla pagina e 100vh per una copertina produce risultati diversi tra versioni del browser. Una copertina affidabile è un blocco alto quanto l'area utile (A4 è 297 mm, meno i margini) seguito da break-after: page.
Font: incorporali o non saranno quelli
Il PDF incorpora solo i font che Chromium ha davvero caricato. Se il font arriva da Google Fonts o da un file @font-face, la stampa deve partire dopo document.fonts.ready, altrimenti alcune pagine escono con il font di riserva. Controlla il risultato con pdffonts dossier.pdf: ogni font deve risultare incorporato (emb yes).
L'indice con i numeri di pagina veri
Qui Chromium ha un limite: non supporta target-counter(), la funzione CSS che nei motori di stampa professionali scrive «questo titolo è a pagina 14». La soluzione pratica è il doppio passaggio:
- Genero il PDF con l'indice senza numeri.
- Estraggo il testo pagina per pagina e cerco su quale pagina compare ogni titolo di capitolo.
- Rigenero il PDF con i numeri trovati.
import subprocess, re
def numeri_di_pagina(pdf, titoli):
info = subprocess.run(["pdfinfo", pdf], capture_output=True, text=True).stdout
totale = int(re.search(r"Pages:\s+(\d+)", info).group(1))
trovati = {}
for p in range(1, totale + 1):
testo = subprocess.run(["pdftotext", "-f", str(p), "-l", str(p), pdf, "-"],
capture_output=True, text=True).stdout.strip()
prima_riga = testo.split("\n")[0].strip() if testo else ""
if prima_riga in titoli and prima_riga not in trovati:
trovati[prima_riga] = p
return trovati
Funziona perché ogni capitolo inizia in cima a una pagina: il titolo è la prima riga. I numeri hanno tutti la stessa larghezza, quindi il secondo passaggio non sposta l'impaginazione. Per sicurezza confronto i numeri del secondo giro con quelli del primo.
Controllare l'impaginazione senza aprire il PDF
Quando i documenti sono tanti non li sfogli uno per uno. Due controlli automatici che uso:
- Pagine quasi vuote: con
pdftoppmrendo ogni pagina a bassa risoluzione e misuro dove finisce il contenuto. Una pagina riempita al 15% di solito è un capitolo che «deborda» di poche righe: riduco un'immagine o un margine. - Capitoli al posto giusto: con
pdftotextverifico che la prima riga delle pagine attese sia davvero il titolo del capitolo.
E se serve un Word?
Spesso il cliente vuole anche il .docx da modificare. Le interruzioni di pagina prima dei capitoli in Word non si fanno con il CSS: si aggiunge <w:pageBreakBefore/> allo stile del titolo dentro word/styles.xml. Così vale per tutti i capitoli, anche quelli aggiunti dopo.
In sintesi
- HTML e CSS per il contenuto, Chromium per la stampa: le tabelle e i testi lunghi si gestiscono come in una pagina web.
break-before,break-inside,orphansewidowsfanno l'80% dell'impaginazione.- Immagini con un'altezza massima, per evitare buchi bianchi.
- Font caricati prima della stampa e verificati con
pdffonts. - Indice con i numeri di pagina in due passaggi.
Se in azienda qualcuno passa ore a copiare dati in un modello Word per farne un PDF, quasi sempre si può automatizzare così. Scrivimi e vediamo il tuo caso.