HotPDF Component impagina testo CSS multi-colonna quando THPDFHTMLImporter.RenderHTML5 converte HTML in PDF in Delphi. Il motore HTML impaginato risolve column-count e column-width contro il content box, bilancia l'ultimo frammento di ogni contenitore multicol con una ricerca binaria, riempie le pagine precedenti colonna per colonna fino al fondo pagina, e supporta column-fill, column-span: all e column-rule
Il motivo usuale per volerlo è noioso e reale: un bollettino di prodotto a due colonne, un allegato di termini e condizioni composto in tre colonne strette, o un PDF di release notes che deve sembrare la newsletter stampata che il team marketing ha già disegnato in HTML. Senza supporto alle colonne l'importatore impila ogni paragrafo in una misura unica larga, la lunghezza di riga raddoppia, e il documento si legge come un file di log. Con il supporto, lo stesso stylesheet che guida l'anteprima nel browser guida il PDF, e la parte difficile diventa la paginazione, di cui i browser quasi mai devono occuparsi sullo schermo
Come decide HotPDF quante colonne usare?
HotPDF risolve il numero di colonne con le stesse regole dello pseudo-algoritmo in CSS Multi-column Layout Module Level 1 §3.4, applicato alla larghezza W del content box dopo aver tolto padding e bordi. Un column-count dichiarato si prende così com'è. Quando è dato solo column-width, il motore calcola N = Trunc((W + gap) / (column-width + gap)) con un minimo di una colonna. Quando sono presenti entrambi, il risultato è il minore dei due, quindi il conteggio dichiarato può porre un tetto al conteggio derivato ma mai gonfiarlo
Un caso concreto: un content box da 483pt (A4 con margini laterali di 56pt, arrotondati) e column-width: 140pt; column-gap: 12pt dà Trunc(495 / 152) = 3 colonne, ognuna larga (483 - 2 * 12) / 3 = 153pt. Aggiungere column-count: 2 alla stessa regola la fa scendere a due colonne, perché vince il minimo. La parola chiave normal per il gap si risolve a 1em della dimensione del font del contenitore, e quella risoluzione avviene al momento del layout e non del parse, quindi un font-size dichiarato dopo column-gap nella stessa regola produce comunque il gap giusto
Una guardia non è nella specifica, e c'è di proposito. Dopo che la larghezza di colonna è calcolata, qualunque valore sotto 12pt collassa il contenitore di nuovo a una singola colonna. Uno stylesheet che dice column-count: 9999, per sbaglio o perché venuto da un template non fidato, produrrebbe altrimenti migliaia di colonne con larghezze negative o microscopiche e un line breaker che prova a far entrare un glifo per riga. Ripiegare su una colonna tiene l'output leggibile e il costo di layout limitato. Conteggi zero o negativi non arrivano mai fin lì: il validatore accetta solo auto o un intero positivo per column-count, e column-width e column-gap prendono auto o normal rispettivamente, o una lunghezza in punti o em
Renderizzare una sezione HTML multi-colonna in PDF in Delphi
L'output multi-colonna non richiede nessun interruttore sull'importatore. THPDFHTMLImporter viene creato con il documento THotPDF in cui disegna, BeginDoc deve essere già partito, e le proprietà di colonna arrivano attraverso il normale cascade, nel markup o nello stylesheet dell'autore passato come secondo argomento di RenderHTML5
uses
SysUtils, HPDFDoc, HPDFHTMLImport;
procedure BuildBulletin(const FileName: string);
var
Pdf: THotPDF;
Importer: THPDFHTMLImporter;
Html, Css: WideString;
begin
Pdf := THotPDF.Create(nil);
Importer := THPDFHTMLImporter.Create(Pdf);
try
Pdf.FileName := FileName;
Pdf.BeginDoc;
Importer.Margin := 56;
Importer.BaseFontName := 'Arial';
Importer.BaseFontSize := 10;
Css := 'section.news { column-width: 140pt; column-gap: 12pt; ' +
' column-rule: 0.5pt solid #999999 } ' +
'section.news h2 { column-span: all }';
Html := '<section class="news">' +
'<p>Release 4.2 ships the new export pipeline...</p>' +
'<p>Licensing changes take effect on 1 March...</p>' +
'<h2>Known issues</h2>' +
'<p>Import of legacy archives & templates...</p>' +
'</section>';
if not Importer.RenderHTML5(Html, Css) then
raise Exception.Create('Nothing was emitted');
Pdf.EndDoc;
finally
Importer.Free;
Pdf.Free;
end;
end;
Notate l'escaping raddoppiato nell'ultimo paragrafo: & dentro la stringa Pascal è l'entità HTML per una e commerciale, che l'importatore riporta a un singolo carattere. Il resto del cascade, i budget e le callback delle risorse si comportano esattamente come descritto nella panoramica dell'import paged-media HTML5 di HotPDF; le colonne sono solo una modalità di layout in più dentro lo stesso profilo
Bilanciare le colonne con una ricerca binaria
HotPDF bilancia le colonne cercando la più piccola altezza di colonna alla quale un riempimento greedy colloca comunque ogni riga rimanente. Il passaggio greedy è semplice: si cammina nelle righe della colonna uno finché la riga successiva supererebbe l'altezza candidata, poi si continua nella colonna due, e così via. La ricerca parte con la riga singola più alta come limite inferiore e la somma di tutte le altezze di riga come limite superiore, dimezza l'intervallo a ogni round, e si ferma quando i limiti sono entro 0,05pt
Una ricerca binaria funziona solo se il predicato è monotono, e qui lo è: una colonna più alta non può mai contenere meno righe di una più bassa, quindi "ogni riga entra" si commuta da falso a vero esattamente una volta mentre l'altezza cresce. Quella proprietà garantisce la convergenza e garantisce che il riempimento greedy finale all'altezza trovata consumi ogni riga, invece di lasciare una riga orfana che innescherebbe una pagina extra spuria. Spiega anche un dettaglio del risultato: la banda bilanciata è corta quanto lo permettono i line box, non necessariamente a colonne uguali, perché le righe sono indivisibili e l'ultima colonna può chiudere una riga o due più in alto delle altre
Cosa accade quando le colonne attraversano un'interruzione di pagina?
Le colonne che non entrano nella pagina corrente vengono riempite in sequenza fino al fondo pagina, e solo l'ultimo frammento di pagina viene bilanciato. Questo è il comportamento che CSS Multi-column Layout Level 1 §7.1 prescrive per column-fill: balance nei contesti frammentati, ed è l'aspetto di una newsletter stampata: colonne piene su ogni pagina tranne l'ultima, dove il testo rimanente è diviso equamente invece di lasciare una colonna alta accanto a una vuota
Ogni frammento di pagina passa per lo stesso fragmenter in modo indipendente. Se le righe rimanenti entrano in N colonne dell'altezza disponibile, il frammento viene bilanciato; altrimenti ogni colonna si riempie fino al fondo pagina e il ciclo apre una nuova pagina. L'altezza naturale dell'intero contenitore, che il motore usa per le decisioni di break-inside: avoid e per la misura dell'estensione, è l'altezza della banda bilanciata con capacità illimitata, così un contenitore che starebbe in una pagina fresca come blocco bilanciato viene spostato lì intatto invece che affettato. Quando una singola riga è più alta di un'intera pagina, il motore rilassa il vincolo, colloca comunque la riga in cima a una pagina, e riporta un diagnostico hdkPaginationConstraint con soggetto multi-column
column-fill: auto e box ad altezza fissa
Con column-fill: auto e un'height definita, HotPDF riempie le colonne in ordine, e ogni altezza di colonna è il minore tra lo spazio di pagina rimanente e l'altezza del contenitore meno il suo padding e i suoi bordi. Il minimo conta più di quanto sembri. Usare solo lo spazio di pagina rimanente lascerebbe alla prima colonna ingoiare ogni riga ogni volta che la pagina ha più spazio rimasto del box. Senza altezza definita, column-fill: auto ripiega sul bilanciamento, come fanno i browser, poiché un contenitore multicol senza vincoli non ha un'altezza di colonna da raggiungere; la prima implementazione perse quel fallback, e uno dei 27 controlli di regressione lo beccò
column-span: all e paragrafi annidati
HotPDF supporta column-span: all sui figli diretti di un contenitore multi-colonna, seguendo CSS Multi-column Layout Level 1 §6.1. Un figlio span di tutto divide il contenitore in segmenti: i figli prima di lui formano una banda di colonne, l'elemento span si renderizza su tutta la larghezza, e i figli dopo di lui aprono una nuova banda. Ogni banda si bilancia, riempie, righella e impagina da sé, così un'intestazione in mezzo a una sezione a tre colonne produce due bande bilanciate con un'intestazione a tutta larghezza in mezzo, che è il layout che la gente disegna davvero nei mockup
Arrivarci ha richiesto prima di correggere un problema meno visibile. Un contenitore come <div style="column-count:2"> con parecchi figli <p> renderizzava un tempo ogni paragrafo in una singola colonna impilata, perché ogni paragrafo veniva impaginato come proprio blocco di testo e la dichiarazione di colonna sul padre non si applicava mai. HotPDF ora raccoglie il testo dei figli blocco annidati in un solo flusso per il contenitore. Il raccoglitore deve inserire un'interruzione dura a ogni confine di blocco mentre lo fa; saltarla concatena l'ultima parola di un paragrafo con la prima parola del successivo sulla stessa riga, un bug facile da perdere quando il testo di test capita di finire con la punteggiatura
Quel flusso unito ha un perimetro deliberato. Si usa solo quando l'intero sottoalbero è flusso di testo semplice. Un contenitore che contiene controlli di form, immagini, tabelle, liste o <hr> in qualunque punto al suo interno mantiene il percorso blocco ordinario e si renderizza in una singola colonna, perché il raccoglitore di testo unito non può esprimere la loro geometria. Le tabelle in particolare hanno regole di paginazione proprie, trattate nell'articolo sulla gestione del rowspan e delle intestazioni di tabella ripetute in HotPDF, e mescolarle in una banda di colonne romperebbe quelle garanzie
Dove va la column rule, e come testarla
HotPDF disegna column-rule sulla mezzeria di ogni gap, al bordo destro di una colonna più metà del gap, dalla cima al fondo della banda su ogni frammento di pagina, come descrive CSS Multi-column Layout Level 1 §4. La larghezza predefinita è 1pt. Il colore predefinito è il colore del testo dell'elemento quando non è dichiarato o è currentColor. La forma breve column-rule senza parola chiave di stile viene trattata come solid, come comunemente si legge la forma breve border, mentre none e hidden sopprimono la riga
La geometria delle colonne è osservabile senza fare il parsing del PDF. L'evento OnLineLayout su THPDFHTMLImporter scatta una volta per riga disegnata con un record THPDFHTMLLineLayout, e poiché il motore disegna ogni colonna separatamente, il campo X di ogni riga è il bordo sinistro della sua colonna. Colonne adiacenti differiscono esattamente della larghezza di colonna più il gap, il che rende facile distinguere un gap di 12pt da uno di 20pt in un test di regressione
type
TColumnProbe = class
public
Lines: TStringList;
constructor Create;
destructor Destroy; override;
procedure LineDrawn(Sender: TObject; const Line: THPDFHTMLLineLayout);
end;
constructor TColumnProbe.Create;
begin
inherited Create;
Lines := TStringList.Create;
end;
destructor TColumnProbe.Destroy;
begin
Lines.Free;
inherited;
end;
procedure TColumnProbe.LineDrawn(Sender: TObject;
const Line: THPDFHTMLLineLayout);
begin
// PageIndex è in base zero; X è il bordo sinistro della colonna della riga
Lines.Add(Format('page=%d x=%.1f y=%.1f %s',
[Line.PageIndex, Line.X, Line.Y, string(Line.Text)]));
end;
// Cablaggio, prima di RenderHTML5:
// Probe := TColumnProbe.Create;
// Importer.OnLineLayout := Probe.LineDrawn;
Due note pratiche dallo scrivere test contro questo evento. PageIndex è in base zero, che è la prima cosa che un'asserzione scritta a mano tende a sbagliare. E X include gli scostamenti di allineamento, quindi per testo centrato o allineato a destra confrontate le posizioni di colonna su fixture allineati a sinistra. La riga stessa non compare nell'evento; per asserirne la posizione, scannerizzate il content stream non compresso in cerca degli operatori verticali m e l
Limiti e trappole da conoscere
Qualche comportamento è un'approssimazione, e meglio conoscerlo prima di una revisione del layout che dopo. La spaziatura dei paragrafi dentro un flusso di colonna unito è approssimata da una riga dell'altezza di riga del contenitore a ogni confine di blocco, quindi i margini per paragrafo dentro una banda di colonne non si riproducono esattamente. Quando column-span: all divide un contenitore, ogni segmento ripete il padding del contenitore e il box di sfondo, il che tiene i segmenti visivamente continui ma non è un box unico nel senso CSS stretto. Ogni valore di column-rule-style diverso da none e hidden passa la validazione, ma la riga viene tracciata come linea solida
I valori non supportati non falliscono in silenzio. Impostare StrictCSS a True trasforma ogni diagnostico in un'eccezione EHPDFHTMLUnsupportedCSS, mentre OnDiagnostic lascia a un job batch scriverli nel log e continuare. Notate che la modalità strict si applica anche al diagnostico di paginazione, quindi un documento con una riga più alta di una pagina solleverà sotto StrictCSS invece di rilassare silenziosamente il vincolo
procedure TReportJob.HtmlDiagnostic(Sender: TObject;
const Diagnostic: THPDFHTMLDiagnostic);
begin
case Diagnostic.Kind of
hdkUnsupportedProperty, hdkUnsupportedValue:
FLog.Add('CSS ' + Diagnostic.Subject + ': ' + Diagnostic.MessageText);
hdkPaginationConstraint:
FLog.Add('Layout relaxed (' + Diagnostic.Subject + '): ' +
Diagnostic.MessageText);
else
FLog.Add(Diagnostic.MessageText);
end;
end;
// Scrittura template: fallite subito su tutto ciò che il motore ignorerebbe
Importer.StrictCSS := FValidatingTemplate;
Importer.OnDiagnostic := HtmlDiagnostic;
La modalità strict è l'impostazione giusta mentre un template è in scrittura, dato che un refuso come column-span: both viene rifiutato all'istante invece di lasciare in silenzio l'intestazione fluire dentro le colonne. In produzione, il log attraverso OnDiagnostic di solito è il compromesso migliore, perché una dichiarazione strana in HTML fornito dal cliente non dovrebbe costare l'intero documento. I diagnostici identici (stesso tipo, soggetto e valore) vengono riportati una volta finché stanno nell'elenco MaxDiagnostics, che vale 256 voci predefinite, così una dichiarazione che il cascade visita per centinaia di elementi non inonda il log
FAQ
HotPDF supporta la forma breve columns?
Sì. La forma breve columns accetta al massimo un conteggio intero positivo e una lunghezza di colonna in entrambi gli ordini, e auto azzera entrambe le longhand. I valori risolti seguono le stesse regole di conteggio e larghezza delle proprietà individuali
Un contenitore multi-colonna può contenere immagini o tabelle?
Può contenerle, ma HotPDF allora renderizza quel contenitore in una singola colonna. Il flusso a colonne si applica solo quando ogni discendente è flusso di testo semplice, perché immagini, tabelle, liste e controlli di form richiedono la propria geometria di box e regole di paginazione
Perché l'ultima pagina mostra colonne bilanciate mentre le precedenti sono piene?
È la regola CSS per column-fill: balance nei media impaginati: solo l'ultimo frammento viene bilanciato. Le pagine precedenti riempiono ogni colonna fino al fondo pagina così nessuno spazio si spreca a metà documento
Il layout multi-colonna, il profilo paged-media HTML5 e il resto della pipeline HTML-to-PDF fanno parte di HotPDF Component per Delphi e C++Builder, che copre anche disegno PDF diretto, form, cifratura ed estrazione di testo