Articolo tecnico

Da HTML a PDF in Delphi: il percorso paged-media HTML5

HotPDF converte HTML in pagine PDF tramite THPDFHTMLImporter.RenderHTML5, un profilo paged-media versionato con un DOM reale, una cascata CSS reale, tabelle, interruzioni di pagina e generazione opzionale di AcroForm a partire dai controlli dei form HTML. Viene eseguito interamente in-process: nessun motore browser, nessun Chromium, nessun convertitore esterno e, per impostazione predefinita, nessun accesso a file o rete

Il motivo per cui esiste un punto di ingresso separato invece di un RenderHTML migliorato è la compatibilità. Il metodo precedente renderizza fatture e lettere in applicazioni di produzione da anni, e un DOM permissivo con una cascata completa cambierebbe l'output di documenti calibrati sul vecchio comportamento. Quindi il vecchio metodo mantiene la sua semantica esatta e tutto ciò che è nuovo vive dietro RenderHTML5, la cui versione del profilo è riportata da HTML5ProfileVersion e i cui traguardi supportati sono elencati da HTML5ProfileMilestones

Cosa aggiunge realmente il profilo HTML5?

Quattro capacità contano nella pratica. Un DOM strutturale in cui html e body sono contenitori anziché nodi di testo. Una cascata CSS con la precedenza corretta. Il layout delle tabelle. E il controllo esplicito delle interruzioni di pagina, che è ciò che significa "paged media": l'HTML è stato progettato per un viewport scorrevole, e trasformarlo in pagine richiede decidere dove il contenuto può interrompersi

Il punto sui contenitori sembra un dettaglio implementativo ed è in realtà la differenza tra funzionare e non funzionare. Se gli elementi radice vengono trattati come contenitori di testo, l'intero DOM collassa in un unico blocco di testo in fase di parsing, e ogni regola del foglio di stile, interruzione di pagina, tabella e sottoalbero di form applicata in seguito non ha più nulla su cui agire. Qualsiasi renderer HTML che produce un unico muro di testo indifferenziato da un documento strutturato ha commesso esattamente questo errore

L'ordine della cascata, spiegato nel dettaglio

La precedenza CSS non è un unico ordinamento. HotPDF applica le dichiarazioni normali dalle regole corrispondenti in ordine di specificità e poi di ordine di origine, applica sopra le dichiarazioni inline normali, poi le dichiarazioni !important dalle regole corrispondenti, e infine le !important inline. Quattro passaggi, in quest'ordine

HotPDF risolve ogni proprietà CSS in Delphi attraverso quattro passaggi di cascata ordinati, dalla corrispondenza delle regole normali fino alle dichiarazioni inline !important
I passaggi successivi vincono i pareggi deliberatamente: le dichiarazioni inline prevalgono sui fogli di stile, e !important prevale su tutto ciò che è dichiarato normalmente

Le implementazioni che riducono tutto questo a "vince l'ultimo" ottengono risultati corretti nei casi comuni e sbagliati in quelli interessanti, il che è peggio che sbagliarli tutti uniformemente, perché il fallimento appare solo nei template che usano !important per sovrascrivere un foglio di stile di base — proprio i template scritti da chi si aspetta che la cascata funzioni:

uses
  HPDFDoc, HPDFHTMLImport;

var
  Pdf: THotPDF;
  Importer: THPDFHTMLImporter;
  Html, Css: WideString;
begin
  Pdf := THotPDF.Create(nil);
  Importer := THPDFHTMLImporter.Create(Pdf);
  try
    Pdf.FileName := 'statement.pdf';
    Pdf.BeginDoc;

    Importer.Margin := 56;              // punti
    Importer.BaseFontName := 'Arial';
    Importer.BaseFontSize := 10;
    Importer.CreateAcroForms := False;  // renderizza i controlli, non crea i campi

    Css := '@page { margin: 56pt } ' +
           'table { border-collapse: collapse } ' +
           'tr.total { page-break-inside: avoid; font-weight: bold }';
    Html := LoadTemplate('statement.html');

    if not Importer.RenderHTML5(Html, Css) then
      raise Exception.Create('nothing was emitted - check the markup');

    Pdf.EndDoc;

    Writeln(Format('profile=%d nodes=%d rules=%d layout ops=%d',
      [Importer.Statistics.ProfileVersion,
       Importer.Statistics.ParsedNodeCount,
       Importer.Statistics.CSSRuleCount,
       Importer.Statistics.LayoutOperationCount]));
  finally
    Importer.Free;
    Pdf.Free;
  end;
end;

Il secondo argomento è un foglio di stile dell'autore applicato insieme a quello che il documento già porta con sé, che è il modo pulito per imporre regole interne come i margini di pagina e il controllo delle righe orfane senza modificare ogni template

Perché l'importatore non recupera mai nulla dalla rete?

Un convertitore da HTML a PDF che risolve riferimenti esterni è un motore di server-side request forgery travestito da strumento di generazione documenti. Puntatelo verso markup fornito dall'utente e recupererà volentieri http://169.254.169.254/ o un URL di file dal vostro server applicativo, incorporando il risultato in un PDF che qualcuno scaricherà

HotPDF quindi, per impostazione predefinita, non legge né file né rete. Le immagini e le altre risorse arrivano esattamente in due modi: come URI data: all'interno del markup, oppure tramite il callback OnResource, dove il vostro codice decide cosa significa un URI e fornisce i byte:

L'importatore HTML di HotPDF per Delphi risolve ogni URI di risorsa localmente, accettando URI data e callback OnResource mentre rifiuta le richieste a file e rete dietro limiti di conteggio e di byte
Solo i byte che arrivano localmente raggiungono la pagina, i riferimenti rifiutati incrementano il contatore, e nessuna richiesta esce mai dal processo
procedure TReportForm.SupplyResource(Sender: TObject;
  const URI: WideString; ResourceKind: THPDFHTMLResourceKind;
  var Data: TBytes; var ContentType: string; var Handled: boolean);
begin
  Handled := False;
  // Servire solo da una directory di asset locale nella allow-list. Non
  // passare mai l'URI a un client HTTP, né combinarlo con un percorso
  // senza prima averlo validato
  if StartsText('asset:', URI) then
  begin
    Data := LoadAssetBytes(Copy(URI, 7, MaxInt));
    ContentType := 'image/png';
    Handled := Length(Data) > 0;
  end;
end;

Ogni risorsa che arriva viene comunque conteggiata rispetto a tre limiti separati: quante risorse, quanti byte ciascuna e quanti byte in totale. Un callback che restituisce fedelmente qualunque cosa gli venga richiesta non può essere indotto a esaurire la memoria da un documento che referenzia la stessa immagine da 50 MB duecento volte

Quattro budget, un'unica modalità di fallimento

I nodi del parser, le regole CSS, le operazioni di layout e le risorse vengono misurati in modo indipendente, perché falliscono in modo indipendente. Un documento può essere piccolo e patologicamente costoso da impaginare; un altro può essere superficiale ma con un foglio di stile progettato per far esplodere il matching dei selettori. Esaurire uno qualsiasi di questi budget solleva EHPDFHTMLImportBudget, una classe di eccezione dedicata, in modo che l'esaurimento del budget possa essere intercettato separatamente da un input malformato

Le statistiche si azzerano all'inizio di ogni chiamata a RenderHTML5, quindi i numeri descrivono una singola importazione e non l'intero ciclo di vita dell'oggetto importatore. In un servizio che renderizza template non attendibili, registrate ParsedNodeCount, LayoutOperationCount e ResourceRejectCount per documento: un conteggio di rifiuti in crescita di solito significa che l'autore del template sta referenziando asset che il vostro callback non riconosce, e un conteggio di operazioni di layout vicino al limite significa che il prossimo documento leggermente più grande fallirà

Scegliere tra importazione HTML e API di layout

L'importazione HTML prevale quando il contenuto è scritto in HTML da persone che non sono sviluppatori Delphi, quando i template cambiano più spesso del codice, o quando lo stesso contenuto deve anche essere inviato come email. L'albero di layout dichiarativo descritto in layout PDF dichiarativo con output taggato prevale quando il documento viene generato a partire da dati dal vostro stesso codice, quando avete bisogno che la struttura PDF/UA venga emessa automaticamente, o quando il controllo preciso sull'impaginazione conta più della comodità di scrittura

Non sono soluzioni esclusive. Uno schema comune è l'importazione HTML per il corpo di un report, con intestazione, piè di pagina e blocchi firma disegnati direttamente, poiché si tratta di geometria fissa che nessun template dovrebbe possedere. Le tabelle sono l'unica area in cui vale la pena misurare entrambi gli approcci: le tabelle finanziarie complesse a volte si impaginano meglio tramite il percorso dedicato alle tabelle descritto in generare tabelle in PDF

Impostate CreateAcroForms in modo consapevole. Disattivato, i controlli dei form HTML vengono renderizzati come grafica statica, che è ciò che vuole un estratto conto stampato. Attivato, diventano veri campi AcroForm interattivi, che è ciò che vuole un modulo compilabile, e il comportamento dei campi che ne consegue è trattato in aggiungere campi form a un PDF caricato

L'importazione HTML, il layout dichiarativo e l'API di disegno diretto operano tutti sullo stesso oggetto documento in Delphi e C++Builder; l'elenco completo delle funzionalità è disponibile sulla pagina del componente PDF HotPDF Delphi