Tehnički članak

Rowspan mreže i ponavljani zaglavlji tabela u HotPDF-u

HotPDF renderuje HTML tabele kroz svoj HTML5 paged-media profil, koristeći pravu mrežu zauzeća za rowspan i colspan, izmerene visine redova umesto procena po broju znakova, i redove zaglavlja ponovljene na svakoj stranici nastavka. Dve situacije navode ga da odbije ponavljanje zaglavlja, i njihovo poznavanje unapred je jeftinije od debagovanja udvostrućene ćelije kasnije

Klasa dokumenta koji to nameće je ona koju svaki tim za izveštavanje eventualno isporuči: faktura ili izveštaj o usaglašenosti gde je izvor istine HTML, tabela se proteže preko četiri strane, i zaglavlje mora biti čitljivo na svakoj od njih. Išta manje od pravog rasporeda tabele proizvodi dva otkaza koje čitaoci odmah primeće: zaglavlje koje se pojavi jednom na prvoj strani i redovi čije su visine pogođene iz broja znakova

Zašto je mogućnost tabele prešla u HTML renderer?

Zato što alternativa gubi rich text, a rich text je razlog zašto je sadržaj uopšte HTML. Očigledan plan deluje kao ponovna upotreba: HotPDF već ima layout DOM objekat tabele sa pravom mrežom, pa mostom spojite HTML parser u njega i dobijete spaning džabe. Problem je u time čime taj objekat tabele crta. Njegove ćelije nose tekst i stil, a njegova putanja crtanja emituje čist tekstualni izlaz, pa sve što je HTML stvarno sadržao van fonta i boje — linkovi, superskripte, inline promene veličine, boja po segmentu — nestaje do trenutka kad stigne do strane

Smer koji preživi kontakt sa stvarnim dokumentima je obrnuti. Premestite mogućnosti table engine-a — mrežu zauzeća, pravo merenje, ponavljanje zaglavlja i teženje kolona — u HTML renderer, i ostavite rich-text renderovanje tamo gde već radi. To je veća izmena od mosta, i to je izmena koja hyperlink unutar ćelije tabele ostavlja hyperlinkom

Rowspan bez union-find strukture

Spaning ćelije prave atomske grupe redova, ali zatvaranje nad tim grupama ne treba opšta disjoint-set struktura, jer je zauzeće uvek kontinuiran interval. Ćelija sa rowspan="3" koja počinje u redu K zauzima redove K do K+2 i ništa drugo, pa se informacija o grupi svodi na oznaku kraja po redu

Algoritam su dve linije namere. Kad postavite spaning ćeliju koja počinje u K a završava u E, zapišite GroupEnd[K] := Max(GroupEnd[K], E). Zatim prođite redove jednom unazad i primenite G[R] := G[G[R]], što svaki kraj reda propagira unazad kroz preklapajuće spanove i daje tranzitivno zatvaranje u jednom prolazu. Ono što dobijete je, za svaki red, poslednji red koji mora ostati na istoj strani s njim, a to je upravo ono što korak paginacije treba da odluči gde sme da padne prelom

Raspodela visine je druga polovina. Kad spaning ćeliji treba više vertikalnog prostora nego što joj redovi koje pokriva trenutno daju, višak ide u poslednji red spana, ne raspoređuje se ravnomerno po njima. Obradite spaning ćelije posle nego što se obične visine redova izmire, pa dopunite završni red svakog spana. Ravnomerna raspodela viška deluje pravednije i proizvodi vidljivo pogrešan izlaz: redovi koji sadrže samo kratke jednoredne ćelije se naduju jer se neka nepovezana ćelija tri reda više slučajno pokazala visokom

HotPDF HTML tabelarna mreža gde jedna ćelija sa rowspan 3 koja počinje u redu 2 zauzima redove 2 do 4 kao jedan atomičan pravougaonik, pored vrednosti kraja grupe po redu G od R proizvedenih jednim obrnutim prolazom koja pokazuje redove 2, 3 i 4 vezane za istu stranu
Spaning zauzeće je uvek kontinuiran interval, pa oznake kraja po redu i jedan obrnuti prolaz zamenjuju union-find i paginaciji govore tačno gde sme da padne prelom
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 uzima opcion author style sheet kao drugi argument, i tamo pripadaju print pravila. Držite screen style sheet van toga. Profil je verzioniran, i HTML5ProfileMilestones prijavljuje koje grupe mogućnosti trenutni build implementira: himParserCascade, himPagedLayout, himTablesForms i himBoundedResources, pa aplikacija može da propada namerno umesto da otkrije rupu u produkciji

Merenje mora da se slaže sa crtanjem, tačno

Visina reda je ispravna samo kad kod koji meri prelomljene linije prelama istim pravilom kao kod koji ih crta. Zvuči očigledno i jeste najčešći pojedinačni uzrok tabela čije se ivice ne poklapaju sa sadržajem. HotPDF meri gramzivim brojačem linija, i taj brojač mora da se poklopi sa semantikom preloma rich-text izlazne putanje u tri konkretna pogleda: prelomi samo na razmacima, nikada ne seče reč, i reč šira od kolone dobija liniju za sebe

Drugi zahtev je font. Merenje mora da ide sa fontom same ćelije, postavljenim kroz SetFont sa stvarnim imenom, stilskim skupom i veličinom pre poziva funkcije širine, a ne sa kojim god fontom bio slučajno aktivan. Bold tekst je redovno više od deset procenata širi od običnog pri istoj veličini, a to je dovoljno da ćelija od tri linije postane ćelija od četiri. Tabela gde su ćelije zaglavlja bold, a ćelije tela nisu, merena jednim fontom, biće pogrešna upravo u redovima koje čitaoci prvo pogledaju

Ispravno urađeno, ovo menja šta možete da asertujete u testu. Vidljivi efekat tačnog merenja je prored, a ne broj glifova: jednoredni red je visok oko 20 tačaka dok procena po broju znakova za isti sadržaj predviđa dve linije i otprilike 35. Asertujte vertikalno rastojanje između redova. I setite se da u PDF user space-u Y raste naviše, pa zaglavlje iznad reda tela znači da je Y vrednost zaglavlja veća, što je suprotno od onoga što instinkt ekranskih koordinata napiše

Kada HotPDF odbija da ponovi zaglavlje?

U dva slučaja, od kojih bi oba proizvela vidljivo pogrešan izlaz da je krenula dalje. Prvi je blok zaglavlja koji sadrži spaning ćeliju koja se proteže preko zaglavlja u redove tela. Ponavljanje zaglavlja nacrtalo bi taj sadržaj ćelije drugi put na poziciji gde više ne pripada, pa se zaglavlje crta jednom i tabela nastavlja bez njega. Drugi je zaglavlje više od 90 procenata upotrebljive visine strane, gde bi ponavljanje ostavilo skoro nikakav prostor za podatke i tabela ne bi napredovala

HotPDF tok odlučivanja za ponavljanje HTML zaglavlja tabela preko preloma strana: zaglavlje čiji rowspan prelazi u redove tela crta se jednom, zaglavlje više od 90 procenata upotrebljive visine strane crta se jednom, i svako drugo zaglavlje se ponavlja na svakoj stranici nastavka
Dva odbijanja su namerna: ponavljanje zaglavlja koje poseduje spaning ćeliju tela ili ispunjava veći deo strane nacrtalo bi sadržaj tamo gde više ne pripada ili ne ostavilo prostora za podatke

Oba odbijanja su namerna i tiha po dizajnu, jer je alternativa gora. Ako se vaše zaglavlje ne ponavlja a očekivali ste da hoće, proverite markup na rowspan koji prelazi thead granicu pre nego što posumnjate na engine. Taj jedan markup obrazac objašnjava većinu iznenađenja

// Težine kolona dolaze iz markup-a, pa je print style sheet mesto
// gde ih kontrolišete. Širine se tretiraju kao težine, ne kao pikseli
const
  PrintStyleSheet =
    'table { width: 100%; }' +
    'thead th { font-weight: bold; background: #eee; }' +
    'td.amount { text-align: right; }';

// Red zaglavlja koji nosi rowspan koji prelazi u telo potiskuje
// ponavljanje zaglavlja. Držite spanove unutar jedne sekcije:
//   <thead><tr><th rowspan="2">Item</th>...</tr></thead>  u redu
//   <tr><th rowspan="3">Item</th>...  prelazi u tbody, bez ponavljanja

Širine kolona ponašaju se kao težine, a ne kao apsolutna merenja, i to je ponašanje koje tabelu drži upotrebljivom kad sadržaj ne odgovara proceni autora. Kolona deklarisana na 30 procenata dobija otprilike 30 procenata dostupne širine, ali raspodela poštuje minimalnu širinu koju svaka kolona stvarno treba, pa uska kolona sa dugim nesecivim tokenom tiho ne preplavljuje kutiju tabele

Gde se ovo uklopi u pipeline za dokumente

Rad na tabelama sedi unutar šireg paged-media profila, i pravila paginacije, budžeti resursa i CSS rukovanje opisani u članku o HTML5 paged-media import putanji primenjuju se nepromenjeno na dokumente koji sadrže tabele. Ako vaši podaci ne počinju kao HTML, ruta direktne izgradnje iz članka o gradnji tabela pravo u PDF zaobilazi sloj raščlanjivanja u potpunosti i daje vam isto ponašanje mreže kroz API. A pošto visina reda na kraju zavisi od toga gde se linije lome, razmatranje merenja u tekstualnom poravnanju i prelamanju linija je pratilac za svakoga ko šteluje gust tabelarni izlaz

Ponovo upotrebljiva lekcija ovde uopšte nije o tabelama. Kad novi podsistem treba mogućnost koju stari podsistem već ima, pitajte koji od ta dva poseduje ono što je najteže ponovo implementirati. Aritmetika mreže je nekoliko desetina linija i lako se premesti. Rich-text renderovanje sa inline linkovima, superskriptama i stilizovanjem po segmentu nije, pa se mreža preselila a tekst ostao. HotPDF isporučuje obe putanje kao deo HotPDF Delphi PDF komponente, pa je izbor između HTML ulaza i direktne izgradnje odluka projekta, a ne biblioteke