Articolo tecnico

Griglie rowspan e intestazioni di tabella ripetute in HotPDF

HotPDF renderizza le tabelle HTML attraverso il suo profilo HTML5 paged-media usando una vera griglia di occupazione per rowspan e colspan, altezze di riga misurate invece che stime a colpi di caratteri, e righe di intestazione ripetute su ogni pagina di continuazione. Due situazioni lo fanno desistere dal ripetere un'intestazione, e conoscerle in anticipo costa meno che debuggare una cella duplicata dopo

La classe di documento che lo impone è quella che prima o poi spedisce ogni team di reporting: una fattura o un report di conformità dove la fonte di verità è HTML, la tabella corre su quattro pagine, e l'intestazione dev'essere leggibile su ognuna. Qualcosa di meno di un vero layout di tabella produce i due difetti che i lettori notano subito, un'intestazione che compare una volta sola a pagina uno e righe le cui altezze sono state indovinate contando i caratteri

Perché la capacità tabella è finita nel renderer HTML?

Perché l'alternativa perde il rich text, e il rich text è il motivo per cui il contenuto è HTML in primo luogo. Il piano ovvio sembra riutilizzo: HotPDF ha già un oggetto tabella del layout DOM con una griglia per bene, quindi ponticella il parser HTML verso di esso e lo spanning arriva gratis. Il problema è con cosa disegna quell'oggetto tabella. Le sue celle portano testo e uno stile, e il suo percorso di disegno emette output di testo semplice, così tutto ciò che l'HTML conteneva davvero oltre a un font e un colore, link, apici, cambi di dimensione inline, colore per run, è perduto quando arriva alla pagina

La direzione che sopravvive al contatto con i documenti veri è quella inversa. Sposta le capacità del motore tabella, la griglia di occupazione, la misura reale, la ripetizione delle intestazioni e la ponderazione delle colonne, dentro il renderer HTML, e lascia il rendering del rich text dove già funziona. È una modifica più grande del ponte, ed è la modifica che mantiene un hyperlink dentro una cella di tabella un hyperlink

Rowspan senza union-find

Le celle in spanning creano gruppi di righe atomici, ma la chiusura su quei gruppi non richiede una struttura disjoint-set generale, perché l'occupazione è sempre un intervallo contiguo. Una cella con rowspan="3" che parte dalla riga K occupa le righe da K a K+2 e nient'altro, quindi l'informazione di gruppo si riduce a un marcatore di fine per riga

L'algoritmo sono due righe di intento. Quando piazzi una cella in spanning che parte a K e finisce a E, registri GroupEnd[K] := Max(GroupEnd[K], E). Poi percorri le righe una volta al contrario applicando G[R] := G[G[R]], che propaga ogni fine riga all'indietro attraverso gli span sovrapposti e produce la chiusura transitiva in un solo passaggio. Quello che ottieni è, per ogni riga, l'ultima riga che deve restare sulla stessa pagina, che è esattamente ciò che serve al passo di paginazione per decidere dove un salto può cadere

Distribuire l'altezza è l'altra metà. Quando una cella in spanning ha bisogno di più spazio verticale di quanto le righe che copre offrano attualmente, il surplus va all'ultima riga dello span, non distribuito in parti uguali. Elabora le celle in spanning dopo che le altezze ordinarie delle righe sono sistemate, poi rabbocca l'ultima riga di ogni span. Spalmare il surplus in egual misura sembra più equo e produce un output visibilmente sbagliato: righe che contengono solo celle corte a riga singola si gonfiano perché qualche cella senza relazione tre righe sopra capitava di essere alta

Una griglia di tabella HTML di HotPDF dove una cella con rowspan 3 che parte dalla riga 2 occupa le righe da 2 a 4 come un unico rettangolo atomico, accanto ai valori di fine gruppo per riga G di R prodotti da una sola passata al contrario che mostra le righe 2, 3 e 4 vincolate alla stessa pagina
L'occupazione in spanning è sempre un intervallo contiguo, quindi marcatori di fine per riga e una passata al contrario sostituiscono union-find e dicono alla paginazione esattamente dove un salto può cadere
var
  Pdf: THotPDF;
  Importer: THPDFHTMLImporter;
  Stats: THPDFHTMLImportStatistics;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'audit-report.pdf';
    Pdf.BeginDoc;
    Importer := THPDFHTMLImporter.Create(Pdf);
    try
      Importer.Margin := 48;
      Importer.BaseFontName := 'Arial';
      Importer.BaseFontSize := 10;
      Importer.MaxDOMNodes := 200000;
      Importer.MaxLayoutOperations := 2000000;
      if Importer.RenderHTML5(SourceHtml, PrintStyleSheet) then
      begin
        Stats := Importer.Statistics;
        Writeln('tables ', Stats.TableCount,
                '  page breaks ', Stats.PageBreakCount);
      end;
    finally
      Importer.Free;
    end;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

RenderHTML5 accetta un foglio di stile autore opzionale come secondo argomento, che è dove stanno le regole di stampa. Tieni fuori il foglio di stile per schermo. Il profilo è versionato, e HTML5ProfileMilestones riporta quali gruppi di capacità la build corrente implementa, himParserCascade, himPagedLayout, himTablesForms e himBoundedResources, così un'applicazione può degradare deliberatamente invece di scoprire un buco in produzione

La misura deve concordare col disegno, esattamente

L'altezza di riga è corretta solo quando il codice che misura le righe andate a capo le va a capo con la stessa regola del codice che le disegna. Sembra ovvio ed è la singola fonte più comune di tabelle i cui bordi non combaciano con il contenuto. HotPDF misura con un contatore di righe greedy, e quel contatore deve coincidere con la semantica di wrapping del percorso di output rich-text in tre rispetti specifici: spezza solo sugli spazi, non scinde mai una parola, e una parola più larga della colonna ottiene una riga tutta per sé

Il secondo requisito è il font. La misura deve girare col font della cella stessa, impostato tramite SetFont col nome vero, il set di stile e la dimensione prima di chiamare la funzione di larghezza, non col font che per caso era attivo. Il testo in grassetto è di routine più del dieci percento più largo del regolare alla stessa dimensione, il che basta a trasformare una cella di tre righe in una di quattro. Una tabella dove le celle di intestazione sono in grassetto e quelle del corpo no, misurata con un solo font, sarà sbagliata esattamente nelle righe che i lettori guardano per primi

Azzeccare questo cambia ciò che puoi asserire in un test. L'effetto osservabile di una misura accurata è la distanza tra le righe, non il conteggio dei glifi: una riga a riga singola è alta circa 20 punti mentre una stima a conteggio di caratteri dello stesso contenuto predice due righe e circa 35. Aserra sulla distanza verticale tra le righe. E ricorda che nello user space del PDF la Y cresce verso l'alto, quindi un'intestazione seduta sopra una riga del corpo significa che il valore Y dell'intestazione è il più grande, il contrario di ciò che l'istinto delle coordinate da schermo scrive

Quando HotPDF rifiuta di ripetere un'intestazione?

In due casi, entrambi i quali produrrebbero un output visibilmente sbagliato se andasse avanti. Il primo è un blocco di intestazione contenente una cella in spanning che si estende oltre l'intestazione dentro le righe del corpo. Ripetere l'intestazione disegnerebbe quel contenuto di cella una seconda volta in una posizione dove non appartiene più, quindi l'intestazione viene disegnata una volta e la tabella continua senza. Il secondo è un'intestazione più alta del 90 percento dell'altezza utile della pagina, dove la ripetizione lascerebbe quasi nessuno spazio ai dati e la tabella non progredirebbe

Il flusso decisionale di HotPDF per ripetere le intestazioni delle tabelle HTML oltre i salti di pagina: un'intestazione il cui rowspan sconfina nelle righe del corpo viene disegnata una volta sola, un'intestazione più alta del 90 percento dell'altezza utile della pagina viene disegnata una volta sola, e ogni altra intestazione si ripete su ogni pagina di continuazione
Due rifiuti sono deliberati: ripetere un'intestazione che possiede una cella in spanning nel corpo o riempie gran parte della pagina disegnerebbe contenuto dove non appartiene più o non lascerebbe spazio ai dati

Entrambi i rifiuti sono deliberati e silenziosi per design, perché l'alternativa è peggiore. Se la tua intestazione non si ripete e te l'aspettavi, controlla nel markup un rowspan che attraversi il confine del thead prima di sospettare del motore. Quel singolo schema di markup spiega la maggior parte delle sorprese

// I pesi delle colonne vengono dal markup, quindi il print style sheet è
// il posto per controllarli. Le larghezze sono trattate come pesi, non come pixel
const
  PrintStyleSheet =
    'table { width: 100%; }' +
    'thead th { font-weight: bold; background: #eee; }' +
    'td.amount { text-align: right; }';

// Una riga di intestazione con un rowspan che sconfina nel corpo sopprime
// la ripetizione dell'intestazione. Tieni gli span dentro una sezione:
//   <thead><tr><th rowspan="2">Item</th>...</tr></thead>  ok
//   <tr><th rowspan="3">Item</th>...  sconfinamento in tbody, nessuna ripetizione

Le larghezze delle colonne si comportano come pesi anziché come misure assolute, che è il comportamento che mantiene una tabella utilizzabile quando il contenuto non corrisponde alla stima dell'autore. Una colonna dichiarata al 30 percento riceve all'incirca il 30 percento della larghezza disponibile, ma la distribuzione rispetta la larghezza minima di cui ogni colonna ha davvero bisogno, così una colonna stretta che contiene un token lungo e inseparabile non sconfina in silenzio fuori dal box della tabella

Dove questo si colloca in una pipeline documentale

Il lavoro sulle tabelle sta dentro il più ampio profilo paged-media, e le regole di paginazione, i budget di risorse e la gestione CSS descritti in il percorso di import HTML5 paged-media si applicano immutati ai documenti che contengono tabelle. Se i tuoi dati non partono come HTML, la via della costruzione diretta in costruire tabelle direttamente in un PDF evita del tutto il livello di parsing e ti dà lo stesso comportamento di griglia tramite un'API. E dato che l'altezza di riga dipende in ultima analisi da dove le righe vanno a capo, la discussione sulla misura in giustificazione del testo e spezzatura delle righe è il pezzo gemello per chi tarina output tabellare denso

La lezione riutilizzabile qui non riguarda affatto le tabelle. Quando un sottosistema nuovo ha bisogno di una capacità che un sottosistema vecchio ha già, chiediti quale dei due possiede la cosa più difficile da reimplementare. L'aritmetica della griglia è poche decine di righe e si sposta facilmente. Il rendering del rich text con link inline, apici e stili per run non lo è, quindi la griglia si è spostata e il testo è rimasto. HotPDF spedisce entrambe le vie come parte del HotPDF Delphi PDF component, così la scelta tra input HTML e costruzione diretta è una decisione di progetto, non di libreria