Articolo tecnico

Flexbox, CSS Grid e note a piè di pagina nel PDF da Delphi

PDF Library for Delphi renderizza HTML in una pagina PDF con vero layout bidimensionale: display: flex e display: grid vengono misurati e posizionati anziché degradati a blocchi impilati, e le note a piè di pagina vengono riservate in fondo al box che porta il proprio riferimento, con una numerazione che resta continua attraverso colonne e pagine. I punti di ingresso sono quelli familiari, DrawHTMLTextBox per un box singolo e DrawHTMLStory per il flusso multi-colonna

Questo conta perché l'HTML è come arriva oggi la maggior parte del contenuto dei report. I template vengono scritti da persone che scrivono CSS, le dashboard vengono progettate come card, e un renderer che collassa silenziosamente una riga flex in quattro blocchi impilati produce un documento che non assomiglia affatto al design. Finché questa capacità non è esistita, l'unico contenitore bidimensionale che il motore misurava era la tabella, così ogni layout a card doveva essere riscritto a mano come tabella

Cosa è cambiato nel modello di layout?

Il ciclo principale precedente manteneva un unico line box e avanzava lungo la pagina. Quel modello gestisce perfettamente il contenuto inline e i blocchi impilati e non può esprimere un contenitore i cui figli sono dimensionati l'uno in relazione all'altro. Le tabelle erano l'unica eccezione, con una propria misurazione a due passaggi

Flex e grid aggiungono ciascuno un passaggio di misurazione limitato sui figli di un contenitore, e la parola importante è limitato. Un contenitore flex misura fino a 256 figli diretti in un array fisso. Una grid usa una matrice di occupazione di al massimo 64 per 64 celle per un posizionamento automatico deterministico. Questi limiti massimi esistono perché un foglio di stile malevolo o generato non possa causare ricorsione illimitata o memoria di posizionamento quadratica, il che è una preoccupazione reale quando l'HTML proviene da un template che un cliente modifica

Come i flex item ottengono le proprie dimensioni

Nella direzione riga, il contenitore somma la basis di ogni elemento insieme ai propri pesi di crescita e restringimento, poi distribuisce lo spazio residuo, positivo o negativo, secondo quei pesi. Con flex-wrap, ogni riga viene risolta indipendentemente, così una riga che si spezza in due righe assegna lo spazio libero per riga anziché sull'intero contenitore. Nella direzione colonna la stessa distribuzione sull'asse principale viene eseguita contro un'altezza esplicita oppure l'altezza del contenuto

justify-content, align-items, gap e le direzioni invertite operano su geometria già misurata. Spostano i box; non innescano mai una nuova misurazione del contenuto degli elementi. Questa separazione è ciò che impedisce a una dashboard complessa di misurare i propri figli più volte

uses
  PDFlibrary;

var
  Lib: TPDFlib;
  Html, Remainder: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.NewDocument;
    Lib.SetPageSize('A4');
    Lib.NewPage;

    Html :=
      '<div style="display:flex; gap:12px;">' +
      '  <div style="flex:2 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Revenue</b><br/>EUR 4,182,300</div>' +
      '  <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Margin</b><br/>18.4%</div>' +
      '  <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Backlog</b><br/>92 days</div>' +
      '</div>';

    Remainder := Lib.DrawHTMLTextBox(40, 40, 515, 120, Html);
    if Remainder <> '' then
      Log('content did not fit - carry the remainder to the next box');

    Lib.SaveToFile('dashboard.pdf');
  finally
    Lib.Free;
  end;
end;

Il valore di ritorno è la stringa di continuazione, il modo in cui ogni punto di ingresso di disegno HTML riporta cosa non è entrato. Passala al box successivo o alla pagina successiva e il flusso riprende da dove si è fermato

Posizionamento della grid, e cosa può essere una track

Le track della grid accettano lunghezze fisse, percentuali, l'unità fr, semplici espressioni repeat() e minmax(). Il posizionamento automatico riempie la matrice di occupazione in modo deterministico, così lo stesso HTML produce sempre la stessa disposizione. Le coordinate esplicite possono sovrapporsi, il che è deliberato: un design che sovrappone un badge a una card sta esprimendo un'intenzione, non un errore. Quando solo un asse viene fornito esplicitamente, il posizionamento cerca solo sull'altro asse

Gli elementi che si estendono su più righe contribuiscono la propria altezza misurata alle righe che coprono, mediata tra esse, il che impedisce a un elemento alto e in span di schiacciare una singola riga lasciando corti i suoi vicini:

Html :=
  '<div style="display:grid; grid-template-columns:repeat(3, 1fr); ' +
  '            gap:10px;">' +
  '  <div style="grid-row:span 2; background:#eef;">Site plan</div>' +
  '  <div>Inspector</div>' +
  '  <div>Date</div>' +
  '  <div style="grid-column:2 / span 2;">Findings summary</div>' +
  '</div>';

Remainder := Lib.DrawHTMLTextBox(40, 180, 515, 260, Html);

I figli flex e grid vengono renderizzati tramite lo stesso renderer HTML di tutto il resto, il che è la proprietà che rende la funzionalità utilizzabile anziché un mondo separato. Font, cascata CSS, link, immagini, tabelle e ulteriori contenitori flex o grid annidati si comportano tutti dentro un elemento flex esattamente come al livello superiore, e il piano di layout esterno registra i comandi finali di testo e rettangolo così che il disegno ripetuto riutilizzi la cache di misurazione esistente

Perché le note a piè di pagina sono un problema di impaginazione?

Una nota a piè di pagina non è contenuto che scorre dopo il paragrafo che contiene il proprio riferimento; è contenuto che deve apparire in fondo allo stesso box del proprio riferimento. Questo inverte l'ordine di misurazione abituale, perché lo spazio disponibile per il testo del corpo ora dipende da contenuto che non è ancora stato impaginato

Il renderer quindi misura la nota quando incontra il riferimento, e sottrae l'area della nota dal budget di altezza del corpo del box limitato corrente. Se il riferimento, il testo del corpo finora e la nota non entrano tutti insieme, il marcatore della nota e tutto ciò che segue si spostano insieme nella stringa di continuazione. Questa regola è ciò che previene i due fallimenti classici: una nota che si sovrastampa al testo del corpo, e una nota bloccata su una pagina il cui riferimento è su quella precedente

In un box limitato l'area della nota è fissata in fondo con una linea separatrice sopra di essa. Nella misurazione illimitata, dove non c'è un'altezza di box a cui ancorarsi, l'area della nota segue immediatamente dopo il corpo. La numerazione viene trasportata in un campo di estensione sullo stack di continuazione, così DrawHTMLTextBox e DrawHTMLStory mantengono la sequenza in corso attraverso colonne e pagine, e una stringa di continuazione prodotta prima che quel campo esistesse riprende comunque correttamente

// Le note a piè di pagina dentro una story multi-colonna mantengono un'unica sequenza continua
Html := LoadTemplate('chapter.html');    // usa marcatori float:footnote
Remainder := Lib.DrawHTMLStory(40, 40, 515, 700,
  2,        // colonne
  16,       // canale in punti
  20,       // numero massimo di pagine per questa story
  Html);
if Remainder <> '' then
  Log('story exceeded its page budget');

Guida pratica per chi scrive template

Progetta rimanendo entro i limiti massimi documentati. Un contenitore flex con più di 256 figli diretti è quasi sempre una tabella di dati travestita da flex, e il percorso tabella la misura comunque meglio. Una grid più grande di 64 per 64 è un foglio di calcolo, e vale lo stesso consiglio. Per il testo del corpo multi-colonna, il comportamento di colonne e sillabazione descritto in sillabazione e colonne di testo bilanciate governa l'aspetto del flusso dentro ogni colonna

Misura prima di disegnare quando un layout deve entrare in uno spazio. GetHTMLTextHeight riporta l'altezza che una data larghezza richiederebbe, il modo economico per decidere tra un layout e un altro prima di impegnare inchiostro. E tratta una stringa di continuazione non vuota come normale anziché eccezionale: è il meccanismo con cui il contenuto lungo si impagina, non un segnale di errore

Quando l'HTML proviene da un motore di report anziché da template scritti a mano, il percorso guidato dai dataset in il motore di report basato su dataset si combina bene con questo, generando il markup che flex e grid poi dispongono. E quando lo stesso contenuto deve anche uscire di nuovo dal PDF, il percorso di esportazione semantica in esportazione da PDF a Markdown e DOCX chiude il giro completo

Il layout HTML, la generazione di report e l'esportazione semantica fanno parte di un'unica libreria per Delphi, C++Builder e Free Pascal; l'elenco completo delle funzionalità si trova nella pagina di PDF Library per Delphi