Technisch artikel

Rowspangrids en herhaalde tabelkoppen in HotPDF

HotPDF rendert HTML-tabellen via zijn HTML5 paged-media-profiel met een echt bezettingsgrid voor rowspan en colspan, gemeten rijhoogten in plaats van schattingen op tekenaantal, en koprijen die op elke vervolgpagina worden herhaald. Twee situaties maken dat hij weigert een kop te herhalen, en die van tevoren weten is goedkoper dan later een gedupliceerde cel debuggen

De documentcategorie die dit afdwingt, is degene die elk rapportageteam uiteindelijk uitlevert: een factuur of compliance-rapport waarin de source of truth HTML is, de tabel over vier pagina's loopt, en de kop op elke pagina leesbaar moet zijn. Alles wat minder is dan een echte table layout levert de twee fouten op die lezers meteen opmerken: een kop die één keer op pagina één verschijnt en rijen waarvan de hoogten uit tekenaantallen zijn gegokt

Waarom verhuisde de tabelcapaciteit naar de HTML-renderer?

Omdat het alternatief rich text verliest, en rich text is nu eenmaal de reden dat de inhoud HTML is. Het voor de hand liggende plan lijkt op hergebruik: HotPDF heeft al een layout DOM table-object met een fatsoenlijk grid, dus brug de HTML-parser daarheen en krijg spanning gratis. Het probleem is waarmee dat table-object tekent. Zijn cellen dragen tekst en een stijl, en zijn tekenpad produceert platte tekstoutput, dus alles wat de HTML werkelijk bevatte buiten een font en een kleur, links, superscripts, inline grootteveranderingen, per-run kleur, is weg tegen de tijd dat het de pagina bereikt

De richting die contact met echte documenten overleeft, is de omgekeerde. Verhuis de capaciteiten van de tabelengine, het bezettingsgrid, echte meting, kopherhaling en kolomweging, naar de HTML-renderer, en laat rich-text rendering waar die al werkt. Dat is een grotere wijziging dan de brug, en het is de wijziging die een hyperlink in een tabelcel een hyperlink laat blijven

Rowspan zonder union-find

Spans vormen atomaire rijgroepen, maar de afsluiting over die groepen heeft geen algemene disjoint-set-structuur nodig, want de bezetting is altijd een aaneengesloten interval. Een cel met rowspan="3" die bij rij K begint, bezet rij K tot en met K+2 en niets anders, dus de groepsinformatie reduceert tot een eindmarkering per rij

Het algoritme is twee regels intentie. Plaatst u een cel die begint bij K en eindigt bij E, noteer dan GroupEnd[K] := Max(GroupEnd[K], E). Loop daarna de rijen één keer achterstevoren en pas G[R] := G[G[R]] toe, wat elk rij-einde achterwaarts door overlappende spans propageert en de transitieve afsluiting in één pas oplevert. Wat u krijgt is, voor elke rij, de laatste rij die op dezelfde pagina moet blijven, en dat is precies wat de pagineringsstap nodig heeft om te besluiten waar een break mag vallen

Hoogte verdelen is de andere helft. Als een cel meer verticale ruimte nodig heeft dan de rijen die hij beslaat nu bieden, gaat het overschot naar de laatste rij van de span, niet gelijkmatig verdeeld. Verwerk de spanning-cells nadat de gewone rijhoogten vastliggen en vul daarna de slotrij van elke span aan. Het overschot gelijkmatig verdelen lijkt eerlijker en levert zichtbaar foutieve output op: rijen die alleen korte cellen van één regel bevatten, worden opgeblazen omdat een ongerelateerde cel drie rijen hoger toevallig lang was

Een HotPDF HTML-tabelgrid waarin één cel met rowspan 3 die bij rij 2 begint rij 2 tot en met 4 als één atomaire rechthoek bezet, naast de groepseindwaarden G van R per rij die één achterwaartse walk oplevert en rij 2, 3 en 4 aan dezelfde pagina bindt
Spanningsbezetting is altijd een aaneengesloten interval, dus eindmarkeringen per rij en één achterwaartse walk vervangen union-find en vertellen de paginering precies waar een break mag vallen
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 neemt een optionele author style sheet als tweede argument, en daar horen printregels thuis. Houd de screen style sheet erbuiten. Het profiel is geversioneerd, en HTML5ProfileMilestones rapporteert welke capability-groepen de huidige build implementeert, himParserCascade, himPagedLayout, himTablesForms en himBoundedResources, zodat een applicatie bewust kan degraderen in plaats van in productie een gat te ontdekken

Meting moet exact overeenkomen met het tekenen

Rijhoogte is alleen correct als de code die omgebroken regels meet ze ombreekt volgens dezelfde regel als de code die ze tekent. Dit klinkt vanzelfsprekend en is toch de allergewoonlijkste bron van tabellen waarvan de randen niet aansluiten op de inhoud. HotPDF meet met een greedy-regelteller, en die teller moet op drie specifieke punten matchen met de wrapping-semantiek van het rich-text-outputpad: hij breekt alleen op spaties, hij splijt nooit een woord, en een woord breder dan de kolom krijgt een eigen regel

De tweede eis is het font. De meting moet draaien met het font van de cel zelf, gezet via SetFont met de werkelijke naam, stijlset en grootte vóór het aanroepen van de breedtefunctie, niet met wat er toevallig actief was. Vette tekst is doorgaans ruim tien procent breder dan regular op dezelfde grootte, en dat is genoeg om een cel van drie regels er een van vier te maken. Een tabel waarin kopcellen vet zijn en bodycellen niet, gemeten met één font, zit fout in precies de rijen die lezers het eerst bekijken

Als dit goed zit, verandert dat wat u in een test kunt asserten. Het waarneembare effect van accurate meting is regelspatiëring, niet glyph-aantallen: een rij van één regel is ongeveer 20 punten hoog, terwijl een schatting op tekenaantal voor dezelfde inhoud twee regels en zo'n 35 voorspelt. Assert op de verticale afstand tussen rijen. En onthoud dat Y in PDF user space omhoog toeneemt, dus een kop die boven een bodyrij zit betekent dat de kop-Y-waarde de grotere is, het omgekeerde van wat schermcoördinaat-instinct schrijft

Wanneer weigert HotPDF een kop te herhalen?

In twee gevallen, die beide zichtbaar foutieve output zouden opleveren als hij doorging. Het eerste is een kopblok met een cel die voorbij de kop doorloopt in de bodyrijen. De kop herhalen zou die celinhoud een tweede keer tekenen op een plek waar hij niet meer thuishoort, dus de kop wordt één keer getekend en de tabel gaat zonder hem verder. Het tweede is een kop die hoger is dan 90 procent van de bruikbare paginahoogte; herhaling zou bijna geen ruimte voor data overlaten en de tabel zou geen vooruitgang boeken

De beslissingsstroom van HotPDF voor het herhalen van HTML-tabelkoppen over paginabreaks: een kop waarvan de rowspan in bodyrijen doorloopt wordt één keer getekend, een kop hoger dan 90 procent van de bruikbare paginahoogte wordt één keer getekend, en elke andere kop wordt op elke vervolgpagina herhaald
De twee weigeringen zijn bewust: een kop herhalen die een spanning bodycel bezit of het grootste deel van de pagina vult, zou inhoud tekenen waar die niet meer thuishoort of geen ruimte voor data overlaten

Beide weigeringen zijn bewust en vanzelfsprekend stil, want het alternatief is erger. Als uw kop niet herhaalt terwijl u dat verwachtte, zoek dan in de markup naar een rowspan die de thead-grens oversteekt voordat u de engine verdenkt. Dat ene markup-patroon verklaart het grootste deel van de verrassing

// Kolomgewichten komen uit de markup, dus de print style sheet is de plek om ze
// te sturen. Breedtes worden als gewichten behandeld, niet als pixels
const
  PrintStyleSheet =
    'table { width: 100%; }' +
    'thead th { font-weight: bold; background: #eee; }' +
    'td.amount { text-align: right; }';

// Een koprij met een rowspan die de body inloopt, onderdrukt kopherhaling.
// Houd spans binnen één sectie:
//   <thead><tr><th rowspan="2">Item</th>...</tr></thead>  ok
//   <tr><th rowspan="3">Item</th>...  loopt in tbody, geen herhaling

Kolombreedtes gedragen zich als gewichten in plaats van absolute maten, en dat is het gedrag dat een tabel bruikbaar houdt als de inhoud niet aan de schatting van de auteur voldoet. Een kolom die op 30 procent is gedeclareerd krijgt ruwweg 30 procent van de beschikbare breedte, maar de verdeling respecteert de minimale breedte die elke kolom werkelijk nodig heeft, dus een smalle kolom met een lang onbreekbaar token overloopt niet stilletjes de tabelbox

Waar dit past in een documentpipeline

Het tabelwerk zit binnen het bredere paged-media-profiel, en de pagineringsregels, resourcebudgetten en CSS-behandeling uit het HTML5 paged-media-importpad gelden ongewijzigd voor documenten met tabellen. Begint uw data niet als HTML, dan omzeilt de route van directe constructie in tabellen rechtstreeks in een PDF bouwen de parsellaag volledig en geeft dezelfde gridgedrag via een API. En omdat rijhoogte uiteindelijk afhangt van waar regels breken, is de meetdiscussie in text justification en regelafbreking het kompaanartikel voor wie dichte tabeloutput afstelt

De herbruikbare les hier gaat helemaal niet over tabellen. Als een nieuw subsysteem een capaciteit nodig heeft die een oud subsysteem al heeft, vraag dan welke van de twee de eigenaar is van het deel dat het lastigst opnieuw te implementeren is. Grid-rekenkunde is een paar tientallen regels en verhuist makkelijk. Rich-text rendering met inline links, superscripts en per-run styling niet, dus het grid verhuisde en de tekst bleef. HotPDF levert beide paden als onderdeel van de HotPDF Delphi PDF component, zodat de keuze tussen HTML-invoer en directe constructie een projectbeslissing is in plaats van een library-beslissing