Articol tehnic

Grile rowspan și anteturi de tabel repetate în HotPDF

HotPDF randează tabele HTML prin profilul său HTML5 pentru mediu paginat, folosind o grilă de ocupare reală pentru rowspan și colspan, înălțimi de rând măsurate, nu estimate după numărul de caractere, și rânduri de antet repetate pe fiecare pagină de continuare. Două situații îl fac să refuze repetarea unui antet, iar a le ști din vreme e mai ieftin decât să depanați ulterior o celulă dublată

Clasa de documente care impune asta e cea pe care orice echipă de raportare o livrează mai devreme sau mai târziu: o factură sau un raport de conformitate unde sursa adevărului e HTML, tabelul aleargă pe patru pagini, iar antetul trebuie să fie lizibil pe fiecare dintre ele. Orice mai puțin de un aranjament de tabel real produce cele două eșecuri pe care cititorii le observă imediat: un antet care apare o singură dată pe pagina unu și rânduri ale căror înălțimi au fost ghicite după numărul de caractere

De ce s-a mutat capacitatea de tabel în renderer-ul HTML?

Pentru că alternativa pierde rich text, iar rich text e chiar motivul pentru care conținutul e HTML de la bun început. Planul evident pare o reutilizare: HotPDF are deja un obiect de tabel în layout DOM, cu grilă adevărată, deci conectați parser-ul HTML la el și obțineți spanning-ul de gratis. Problema e cu ce desenează acel obiect de tabel. Celulele lui poartă text și un stil, iar calea sa de desenare emite text simplu, deci orice conținea efectiv HTML-ul dincolo de un font și o culoare — legături, exponenți, schimbări de mărime în interiorul rândului, culori per segment — dispare până ajunge pe pagină

Direcția care supraviețuiește contactului cu documente reale e cea inversă. Mutați capacitățile motorului de tabel — grila de ocupare, măsurarea reală, repetarea antetului și ponderea coloanelor — în renderer-ul HTML și lăsați randarea rich-text acolo unde funcționează deja. E o schimbare mai mare decât puntea și e schimbarea care păstrează o hiperlegătură din interiorul unei celule de tabel o hiperlegătură

Rowspan fără union-find

Celulele care se întind pe mai multe rânduri creează grupuri de rânduri atomice, dar închiderea peste acele grupuri nu are nevoie de o structură generală de mulțimi disjuncte, pentru că ocuparea e întotdeauna un interval contigu. O celulă cu rowspan="3" care începe la rândul K ocupă rândurile K până la K+2 și nimic altceva, deci informația de grup se reduce la un marker de sfârșit pe fiecare rând

Algoritmul e de două rânduri de intenție. Când plasați o celulă cu întindere care începe la K și se termină la E, notați GroupEnd[K] := Max(GroupEnd[K], E). Apoi parcurgeți rândurile o dată în sens invers și aplicați G[R] := G[G[R]], care propagă fiecare sfârșit de rând înapoi prin span-urile care se suprapun și dă închiderea tranzitivă într-o singură trecere. Ce obțineți e, pentru fiecare rând, ultimul rând care trebuie să rămână pe aceeași pagină cu el, exact ce are nevoie pasul de paginare ca să decidă unde poate cădea o întrerupere

Repartizarea înălțimii e cealaltă jumătate. Când o celulă cu întindere are nevoie de mai mult spațiu vertical decât oferă în prezent rândurile pe care le acoperă, surplusul merge la rândul ultim al span-ului, nu se împarte uniform peste ele. Procesați celulele cu întindere după ce înălțimile obișnuite ale rândurilor s-au așezat, apoi completați rândul final al fiecărui span. Împărțirea uniformă a surplusului pare mai dreaptă și produce un rezultat vizibil greșit: rânduri care conțin doar celule scurte, de o linie, se umflă pentru că vreo celulă fără legătură, cu trei rânduri mai sus, s-a întâmplat să fie înaltă

O grilă de tabel HTML HotPDF în care o celulă cu rowspan 3 care începe la rândul 2 ocupă rândurile 2 până la 4 ca un singur dreptunghi atomic, alături de valorile de sfârșit de grup pe rând G ale R produse de o singură parcurgere inversă, arătând rândurile 2, 3 și 4 legate la aceeași pagină
Ocuparea cu întindere e întotdeauna un interval contigu, deci markerele de sfârșit pe rând și o singură parcurgere inversă înlocuiesc union-find și îi spun paginării exact unde poate cădea o întrerupere
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 primește o foaie de stil de autor opțională ca al doilea argument, iar acolo își au locul regulile de tipărire. Țineți foaia de stil pentru ecran departe de ea. Profilul e versionat, iar HTML5ProfileMilestones raportează ce grupuri de capacități implementează build-ul curent, himParserCascade, himPagedLayout, himTablesForms și himBoundedResources, astfel încât o aplicație poate degrada deliberat, în loc să descopere o lacună în producție

Măsurarea trebuie să fie de acord cu desenarea, exact

Înălțimea unui rând e corectă doar când codul care măsoară liniile împachetate le împachetează după aceeași regulă ca codul care le desenează. Sună evident și e cea mai frecventă sursă unică de tabele ale căror borduri nu se aliniază cu conținutul lor. HotPDF măsoară cu un contor de linie lacom, iar acel contor trebuie să se potrivească cu semantica de împachetare a căii de ieșire rich-text în trei privințe precise: rupe doar la spații, nu despică niciodată un cuvânt, iar un cuvânt mai lat decât coloana primește o linie întreagă pentru el

A doua cerință e fontul. Măsurarea trebuie să ruleze cu fontul propriu al celulei, stabilit prin SetFont cu numele real, setul de stiluri și mărimea înainte de a apela funcția de lățime, nu cu fontul care se întâmpla să fie activ. Textul bold e de obicei cu peste zece la sută mai lat decât cel regulat la aceeași mărime, destul cât să transforme o celulă de trei linii într-una de patru. Un tabel în care celulele de antet sunt bold, iar cele de corp nu, măsurat cu un singur font, va greși exact în rândurile pe care cititorii le privesc primul

A dreptate aici schimbă ce puteți afirma într-un test. Efectul observabil al măsurării precise e spațierea liniilor, nu numărul de glife: un rând de o linie are cam 20 de puncte, în timp ce o estimare după numărul de caractere a aceluiași conținut prezice două linii și circa 35. Afirmați asupra distanței verticale dintre rânduri. Și nu uitați că în spațiul utilizator al PDF-ului axa Y crește în sus, deci un antet așezat deasupra unui rând de corp înseamnă că valoarea Y a antetului e cea mai mare, opusul instinctului de coordonate de ecran

Când refuză HotPDF să repete un antet?

În două cazuri, ambele cu rezultat vizibil greșit dacă ar merge înainte. Primul e un bloc de antet care conține o celulă cu întindere ce depășește antetul în rândurile de corp. Repetarea antetului ar desena conținutul acelei celule a doua oară, într-o poziție în care nu îi mai aparține, deci antetul se desenează o dată, iar tabelul continuă fără el. Al doilea e un antet mai înalt de 90 la sută din înălțimea utilă a paginii, unde repetarea ar lăsa aproape deloc loc pentru date, iar tabelul nu ar mai face niciun progres

Fluxul de decizie HotPDF pentru repetarea anteturilor de tabel HTML peste întreruperile de pagină: un antet al cărui rowspan traversează în rândurile de corp se desenează o dată, un antet mai înalt de 90 la sută din înălțimea utilă a paginii se desenează o dată, iar orice alt antet se repetă pe fiecare pagină de continuare
Cele două refuzuri sunt deliberate: repetarea unui antet care deține o celulă de corp cu întindere sau umple cea mai mare parte a paginii ar desena conținut unde nu îi mai aparține locul sau nu ar lăsa loc de date

Ambele refuzuri sunt deliberate și tăcute din design, pentru că alternativa e mai rea. Dacă antetul dumneavoastră nu se repetă, deși vă așteptați să o facă, verificați markup-ul după un rowspan care traversează granița thead înainte să bănuiți motorul. Același tipar unic de markup explică cea mai mare parte a surprizei

// Greutățile coloanelor vin din markup, deci foaia de stil de tipărire
// e locul unde le controlați. Lățimile sunt tratate ca greutăți, nu ca pixeli
const
  PrintStyleSheet =
    'table { width: 100%; }' +
    'thead th { font-weight: bold; background: #eee; }' +
    'td.amount { text-align: right; }';

// Un rând de antet care poartă un rowspan ce trece în corp suprimă
// repetarea antetului. Păstrați span-urile în interiorul unei secțiuni:
//   <thead><tr><th rowspan="2">Item</th>...</tr></thead>  ok
//   <tr><th rowspan="3">Item</th>...  span în tbody, fără repetare

Lățimile coloanelor se comportă ca greutăți, nu ca măsurători absolute, ceea ce e comportamentul care ține un tabel utilizabil când conținutul nu se potrivește cu estimarea autorului. O coloană declarată la 30 la sută primește cam 30 la sută din lățimea disponibilă, dar distribuția respectă lățimea minimă de care fiecare coloană are efectiv nevoie, astfel că o coloană îngustă care ține un token lung, nesparabil, nu depășește tăcut cutia tabelului

Unde se încadrează asta într-un pipeline de documente

Munca pe tabele stă în interiorul profilului mai larg de mediu paginat, iar regulile de paginare, bugetele de resurse și tratarea CSS descrise în calea de import HTML5 paged-media se aplică neschimbate documentelor care conțin tabele. Dacă datele dumneavoastră nu pornesc ca HTML, calea de construcție directă din construirea tabelelor direct într-un PDF ocolește cu totul stratul de parsare și vă dă același comportament de grilă printr-un API. Iar pentru că înălțimea rândului depinde, în ultimă instanță, de unde se rup liniile, discuția despre măsurare din justificarea textului și ruperea liniilor e piesa soră pentru oricine reglează ieșiri tabulare dense

Lecția reutilizabilă de aici nu ține deloc de tabele. Când un subsistem nou are nevoie de o capacitate pe care un subsistem vechi o are deja, întrebați-vă care dintre cele două deține lucrul cel mai greu de reimplementat. Aritmetica grilei sunt câteva zeci de linii și se mută ușor. Randarea rich-text cu legături în interiorul rândului, exponenți și stilizare per segment nu e, deci s-a mutat grila, iar textul a rămas. HotPDF livrează ambele căi ca parte din HotPDF Delphi PDF component, astfel încât alegerea între intrare HTML și construcție directă e o decizie de proiect, nu de bibliotecă