Technický článek

Mřížky rowspan a opakované hlavičky tabulek v HotPDF

HotPDF vykresluje HTML tabulky přes svůj profil HTML5 paged media se skutečnou mřížkou obsazení pro rowspan a colspan, s měřenými výškami řádků místo odhadů z počtu znaků a s hlavičkovými řádky opakovanými na každé pokračovací stránce. Dvě situace ho donouší hlavičku neopakovat a znát je předem je levnější než později debugovat duplikovanou buňku

Třída dokumentů, která tohle vyvolá, je ta, kterou dřív nebo později pošle každý reportovací tým: faktura nebo compliance report, kde je zdrojem pravdy HTML, tabulka běží přes čtyři strany a hlavička musí být čitelná na každé z nich. Cokoli méně než skutečný tabulkový layout vyprodukuje ty dva pády, které si čtenáři všimnou okamžitě: hlavičku objevující se jednou na straně jedna a řádky s výškami hádanými z počtu znaků

Proč se tabulková schopnost přestěhovala do HTML rendereru?

Protože alternativa přijde o rich text a rich text je ten důvod, proč je obsah vůbec HTML. Zjevný plán vypadá jako reuse: HotPDF už má layout DOM objekt tabulky s pořádnou mřížkou, takže do něj přemostit HTML parser a mít spanning zadarmo. Problém je tím, čím ten tabulkový objekt kreslí. Jeho buňky nesou text a styl a jeho kreslicí cesta vysílá plain textový výstup, takže cokoli, co HTML skutečně obsahovalo nad rámec fontu a barvy, odkazy, horní indexy, inline změny velikosti, barvu per run, je v okamžiku dopadu na stránku pryč

Směr, který přežije kontakt s reálnými dokumenty, je opačný. Přestěhovat schopnosti tabulkového enginu, mřížku obsazení, skutečné měření, opakování hlavičky a vážení sloupců, do HTML rendereru a nechat rich-text vykreslování tam, kde už funguje. To je větší změna než most a je to změna, díky níž zůstane hyperlink uvnitř tabulkové buňky hyperlinkem

Rowspan bez union-find

Rozpínající buňky tvoří atomické skupiny řádků, ale uzávěr nad těmi skupinami nepotřebuje obecnou strukturu disjoint-set, protože obsazení je vždy souvislý interval. Buňka s rowspan="3" začínající na řádku K obsadí řádky K až K+2 a nic víc, takže informace o skupině se redukuje na koncovou značku per řádek

Algoritmus jsou dvě řádky záměru. Když umístíte rozpínající buňku začínající na K a končící na E, zapište GroupEnd[K] := Max(GroupEnd[K], E). Potom projděte řádky jednou pozpátku a aplikujte G[R] := G[G[R]], což šíří každý konec řádku zpět přes překrývající se rozpětí a dá tranzitivní uzávěr jediným průchodem. Co dostanete, je pro každý řádek poslední řádek, který s ním musí zůstat na totéž stránce, což je přesně to, co krok stránkování potřebuje k rozhodnutí, kde smí zlom padnout

Rozdělení výšky je druhá polovina. Když rozpínající buňka potřebuje víc svislého prostoru, než jí aktuálně poskytují řádky, jež pokrývá, přebytek jde do posledního řádku rozpětí, ne rovnoměrně mezi ně. Zpracujte rozpínající buňky poté, co se usadí obyčejné výšky řádků, a pak dotlačte poslední řádek každého rozpětí. Rovnoměrné roztření přebytku vypadá férověji a produkuje viditelně špatný výstup: řádky obsahující jen krátké jednořádkové buňky nafouknou, protože nějaká nesouvisející buňka o tři řádky výš se zrovna stala vysokou

Mřížka HTML tabulky HotPDF, kde jedna buňka s rowspan 3 začínající na řádku 2 obsadí řádky 2 až 4 jako jediný atomický obdélník, vedle per-řádkových koncových hodnot skupiny G[R] vyrobených jedním zpětným průchodem ukazujícím řádky 2, 3 a 4 vázané na totéž stránku
Rozpínající obsazení je vždy souvislý interval, takže koncové značky per řádek a jeden zpětný průchod nahradí union-find a řeknou stránkování přesně, kde smí zlom padnout
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 bere jako druhý argument volitelný autorův style sheet a právě tam patří tisková pravidla. Screen style sheet nechte venku. Profil je verzovaný a HTML5ProfileMilestones hlásí, které skupiny schopností aktuální build implementuje, himParserCascade, himPagedLayout, himTablesForms a himBoundedResources, takže aplikace může degradovat záměrně místo objevování mezery až v produkci

Měření se musí s kreslením shodovat, přesně

Výška řádku je správná jen tehdy, když kód měřící zalamované řádky zalamuje stejným pravidlem jako kód, který je kreslí. Zní to zjevně a je to jediný nejčastější zdroj tabulek, jejichž okraje nelíhnou s obsahem. HotPDF měří chamtivým počítadlem řádků a to počítadlo se musí ve třech konkrétních ohledech shodovat se zalamovací sémantikou rich-text výstupní cesty: láme jen na mezerách, nikdy nerozpůlí slovo a slovo širší než sloupec dostane řádek vlastní

Druhým požadavkem je font. Měření musí běžet s fontem vlastním buňce, nastaveným přes SetFont se skutečným názvem, stylem a velikostí před voláním šířkové funkce, ne s tím, jaký font se zrovna náhodou aktivoval. Tučný text bývá při stejné velikosti o víc než deset procent širší než regulérní, což stačí ke změně třířádkové buňky na čtyřřádkovou. Tabulka, kde jsou hlavičkové buňky tučné a buňky těla ne, měřená jediným fontem, bude špatná přesně v řádcích, na které se čtenáři dívají první

Trefit tohle správně mění to, co můžete v testu asertovat. Pozorovatelným efektem přesného měření je rozestup řádků, ne počty glyfů: jednořádkový řádek je vysoký zhruba 20 bodů, zatímco odhad z počtu znaků u téhož obsahu předpoví dva řádky a zhruba 35. Asertujte svislou vzdálenost mezi řádky. A pamatujte, že PDF user space má Y rostoucí vzhůru, takže hlavička sedící nad řádkem těla znamená, že hodnota Y hlavičky je ta větší, což je opak toho, co napíše instinkt ze screen souřadnic

Kdy HotPDF odmítne hlavičku opakovat?

Ve dvou případech a oba by při pokračování vyprodukovaly viditelně špatný výstup. První je hlavičkový blok obsahující rozpínající buňku, která sahá přes hlavičku do řádků těla. Opakování hlavičky by nakreslilo obsah té buňky podruhé na pozici, do níž už nepatří, takže hlavička se nakreslí jednou a tabulka pokračuje bez ní. Druhý je hlavička vyšší než 90 procent použitelné výšky stránky, kde by opakování nezanechalo téměř žádný prostor pro data a tabulka by nedělala žádný pokrok vpřed

Rozhodovací tok HotPDF pro opakování HTML hlaviček tabulek přes zlomy stránek: hlavička, jejíž rowspan přechází do řádků těla, se nakreslí jednou, hlavička vyšší než 90 procent použitelné výšky stránky se nakreslí jednou a každá jiná hlavička se opakuje na každé pokračovací stránce
Obě odmítnutí jsou záměrná: opakování hlavičky, jež vlastní rozpínající buňku těla, nebo té, jež zaplní většinu stránky, by nakreslilo obsah tam, kam už nepatří, nebo nezanechalo žádný prostor pro data

Obě odmítnutí jsou záměrná a podle návrhu tichá, protože alternativa je horší. Neopakuje-li se vaše hlavička, ač jste to čekali, zkontrolujte v markupu rowspan překračující hranici thead, dřív než začnete podezírat engine. Ten jediný markup vzor vysvětluje většinu překvapení

// Váhy sloupců pocházejí z markupu, takže print style sheet je
// místo, kde se řídí. Šířky se berou jako váhy, ne jako pixely
const
  PrintStyleSheet =
    'table { width: 100%; }' +
    'thead th { font-weight: bold; background: #eee; }' +
    'td.amount { text-align: right; }';

// Hlavičkový řádek nesoucí rowspan přecházející do těla potlačí
// opakování hlavičky. Nechávejte rozpětí uvnitř jedné sekce:
//   <thead><tr><th rowspan="2">Item</th>...</tr></thead>  ok
//   <tr><th rowspan="3">Item</th>...  rozpětí do tbody, bez opakování

Šířky sloupců se chovají jako váhy, nikoli jako absolutní míry, což je chování udržující tabulku použitelnou, když obsah neodpovídá odhadu autora. Sloupec deklarovaný na 30 procent dostane zhruba 30 procent dostupné šířky, ale rozdělení respektuje minimální šířku, kterou každý sloupec skutečně potřebuje, takže úzký sloupec držící dlouhý nerozlomitelný token nepřeteče tiše krabici tabulky

Kam tohle v dokumentové pipeline patří

Tabulková práce sedí uvnitř širšího profilu paged media a stránkovací pravidla, rozpočty resources a handling CSS popsané v článku importní cesta HTML5 paged media platí pro dokumenty obsahující tabulky beze změny. Nezačínají-li vaše data jako HTML, cesta přímé konstrukce v článku stavění tabulek rovnou do PDF obejde parsing vrstvu úplně a dá vám totéž chování mřížky přes API. A protože výška řádku nakonec závisí na tom, kde se řádky lámou, rozbor měření v článku zarovnávání textu a lámání řádků je doprovodný kousek pro každého, kdo ladí hustý tabulkový výstup

Přenositelné poučení odsud se vůbec netýká tabulek. Když nový subsystém potřebuje schopnost, již starý subsystém už má, zeptejte se, který z obou vlastní věc nejtěžší k reimplementaci. Aritmetika mřížky je pár desítek řádků a stěhuje se snadno. Rich-text vykreslování s inline odkazy, horními indexy a stylingem per run není, takže mřížka se přestěhovala a text zůstal. HotPDF dodává obě cesty jako část HotPDF Delphi PDF komponenty, takže volba mezi HTML vstupem a přímou konstrukcí je projektové rozhodnutí, ne knihovní