Teknisk artikel

Rowspan-rutnät och upprepade tabellhuvuden i HotPDF

HotPDF renderar HTML-tabeller genom sin HTML5 paged-media-profil med ett verkligt upptagsrutnät för rowspan och colspan, uppmätta radhöjder i stället för teckenräkningsuppskattningar, och huvudrader upprepade på varje fortsättningssida. Två situationer får den att avstå från att upprepa ett huvud, och att känna till dem i förväg är billigare än att felsöka en duplicerad cell senare

Dokumentklassen som tvingar fram det här är den varje rapporteringsteam förr eller senare levererar: en faktura eller en efterlevnadsrapport där källan till sanningen är HTML, tabellen löper över fyra sidor, och huvudet måste vara läsbart på varenda en. Allt mindre än en verklig tabellayout ger de två felen läsare märker direkt, ett huvud som visas en gång på sida ett och rader vars höjder gissats från teckenantal

Varför flyttades tabellförmågan in i HTML-renderaren?

Därför att alternativet tappar rich text, och rich text är anledningen till att innehållet är HTML från början. Den uppenbara planen ser ut som återanvändning: HotPDF har redan ett layout-DOM-tabellobjekt med ett ordentligt rutnät, så broa HTML-parsaren in i det och få spanning gratis. Problemet är vad det tabellobjektet ritar med. Dess celler bär text och en stil, och dess ritningsväg avger vanlig textutmatning, så allt HTML:en faktiskt innehöll bortom ett teckensnitt och en färg, länkar, upphöjd text, inline-storleksändringar, färg per körning, är borta när det når sidan

Riktningen som överlever kontakten med riktiga dokument är den omvända. Flytta tabellmotorns förmågor, upptagsrutnätet, verklig mätning, huvudupprepning och kolumnvikter, in i HTML-renderaren, och låt rich-text-renderingen stanna där den redan fungerar. Det är en större ändring än bron, och det är ändringen som håller en hyperlänk inuti en tabellcell en hyperlänk

Rowspan utan union-find

Spannande celler skapar atomära radgrupper, men stängningen över de grupperna behöver ingen allmän disjoint-set-struktur, eftersom upptaget alltid är ett sammanhängande intervall. En cell med rowspan="3" som startar vid rad K upptar rad K till K+2 och inget annat, så gruppinformationen reduceras till en slutmarkör per rad

Algoritmen är två rader avsikt. När du placerar en cell som spänner från K till E, registrera GroupEnd[K] := Max(GroupEnd[K], E). Gå sedan igenom raderna en gång i bakvänd ordning och tillämpa G[R] := G[G[R]], vilket propagerar varje radslut bakåt genom överlappande spann och ger den transitiva stängningen i ett enda pass. Det du får är, för varje rad, den sista rad som måste stanna på samma sida som den, vilket är precis vad pagineringssteget behöver för att avgöra var en brytning får falla

Att fördela höjd är den andra halvan. När en spännande cell behöver mer vertikalt utrymme än de rader den täcker för närvarande ger, går överskottet till spännets sista rad, inte jämnt fördelat över dem. Behandla de spännande cellerna efter att vanliga radhöjder ställts, och fyll sedan på varje spans sista rad. Att sprida överskottet jämnt verkar rättvisare och ger synligt felaktig utmatning: rader som bara innehåller korta celler med en enda textrad blåses upp för att någon orelaterad cell tre rader upp råkade vara hög

Ett HotPDF HTML-tabellrutnät där en cell med rowspan 3 som startar vid rad 2 upptar rad 2 till 4 som en enda atomär rektangel, jämte per-rad-gruppslutvärdena G[R] producerade av en enda bakvänd genomgång som visar rad 2, 3 och 4 bundna till samma sida
Spännande upptag är alltid ett sammanhängande intervall, så slutmarkörer per rad och en enda bakvänd genomgång ersätter union-find och berättar för pagineringen exakt var en brytning får falla
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 tar en valfri författarstilmall som sitt andra argument, och det är där utskriftsreglerna hör hemma. Håll skärmstilmallen borta från den. Profilen är versionsmärkt, och HTML5ProfileMilestones rapporterar vilka förmågegrupper det aktuella bygget implementerar, himParserCascade, himPagedLayout, himTablesForms och himBoundedResources, så en applikation kan degradera medvetet i stället för att upptäcka ett tomrum i produktion

Mätningen måste överensstämma med ritningen, exakt

Radhöjd är bara korrekt när koden som mäter radbrutna rader bryter dem efter samma regel som koden som ritar dem. Det låter uppenbart och är den enskilt vanligaste källan till tabeller vars kanter inte stämmer med innehållet. HotPDF mäter med en girig radräknare, och den räknaren måste matcha radbrytningssmantiken hos rich-text-utdatavägen i tre specifika avseenden: den bryter endast vid mellanslag, den delar aldrig ett ord, och ett ord bredare än kolumnen får en egen rad

Det andra kravet är typsnittet. Mätningen måste köras med cellens eget teckensnitt, satt via SetFont med det faktiska namnet, stilset och storlek innan breddfunktionen anropas, inte med vilket typsnitt som råkade vara aktivt. Fet text är rutinmässigt mer än tio procent bredare än vanlig vid samma storlek, vilket räcker för att förvandla en treradscell till en fyrradscell. En tabell där huvudceller är feta och brödtextceller inte är det, mätt med ett enda teckensnitt, blir fel i exakt de rader läsare tittar på först

Att få det här rätt ändrar vad du kan hävda i ett test. Den observerbara effekten av korrekt mätning är radavståndet, inte glyfantal: en rad med en enda textrad är omkring 20 punkter hög medan en teckenräkningsuppskattning av samma innehåll förutsäger två rader och ungefär 35. Hävda om det vertikala avståndet mellan rader. Och kom ihåg att PDF:s användarrymd har Y ökande uppåt, så ett huvud som sitter ovanför en brödrad betyder att huvudets Y-värde är det större, vilket är det motsatta av vad skärmkoordinatinstinkt skriver

När vägrar HotPDF upprepa ett huvud?

I två fall, vilka båda skulle ge synligt felaktig utmatning om den gick vidare. Det första är ett huvudblock som innehåller en spännande cell som sträcker sig förbi huvudet in i brödrader. Att upprepa huvudet skulle rita det cellinnehållet en andra gång på en plats där det inte längre hör hemma, så huvudet ritas en gång och tabellen fortsätter utan det. Det andra är ett huvud högre än 90 procent av den användbara sidhöjden, där upprepning skulle lämna nästan inget utrymme för data och tabellen inte skulle göra något framåtskridande

HotPDF:s beslutsflöde för att upprepa HTML-tabellhuvuden över sidbrytningar: ett huvud vars rowspan korsar in i brödrader ritas en gång, ett huvud högre än 90 procent av den användbara sidhöjden ritas en gång, och varenda annat huvud upprepas på varje fortsättningssida
Två avslag är medvetna: att upprepa ett huvud som äger en spännande brödtextcell eller fyller större delen av sidan skulle rita innehåll där det inte längre hör hemma eller lämna inget utrymme för data

Båda avslagen är medvetna och tysta av design, eftersom alternativet är värre. Om ditt huvud inte upprepas och du förväntade dig det, kontrollera uppmärkningen efter en rowspan som korsar thead-gränsen innan du misstänker motorn. Det enskilda uppmärkningsmönstret står för det mesta av överraskningen

// Kolumnvikter kommer från uppmärkningen, så utskriftsstilmallen är
// platsen att styra dem. Bredder behandlas som vikter, inte som pixlar
const
  PrintStyleSheet =
    'table { width: 100%; }' +
    'thead th { font-weight: bold; background: #eee; }' +
    'td.amount { text-align: right; }';

// En huvudrad som bär en rowspan som korsar in i brödtexten undertrycker
// huvudupprepning. Håll spann inom en sektion:
//   <thead><tr><th rowspan="2">Item</th>...</tr></thead>  ok
//   <tr><th rowspan="3">Item</th>...  spann in i tbody, ingen upprepning

Kolumnbredder beter sig som vikter snarare än absoluta mått, vilket är beteendet som håller en tabell användbar när innehållet inte matchar författarens uppskattning. En kolumn deklarerad till 30 procent får ungefär 30 procent av den tillgängliga bredden, men fördelningen respekterar den minimibredd varje kolumn faktiskt behöver, så en smal kolumn med en lång odelbar token svämmar inte tyst över tabellrutan

Var detta passar in i en dokumentpipeline

Tabellarbetet sitter inuti den bredare paged-media-profilen, och pagineringsreglerna, resursbudgeterna och CSS-hanteringen som beskrivs i HTML5-importvägen för paged media gäller oförändrade för dokument som innehåller tabeller. Om din data inte börjar som HTML undviker den direkta konstruktionsvägen i att bygga tabeller rakt in i en PDF parsningslagret helt och ger dig samma rutnätsbeteende via ett API. Och eftersom radhöjd i slutändan beror på var rader bryts är mätningsdiskussionen i textjustering och radbrytning följeslagaren för alla som trimmar tät tabellutmatning

Den återanvändbara lärdomen här handlar inte om tabeller alls. När ett nytt delsystem behöver en förmåga ett gammalt delsystem redan har, fråga vilket av de två som äger det som är svårast att återimplementera. Rutnätsaritmetik är några dussin rader och flyttas lätt. Rich-text-rendering med inline-länkar, upphöjd text och stil per körning är det inte, så rutnätet flyttade och texten stannade. HotPDF skickar båda vägarna som del av HotPDF Delphi PDF component, så valet mellan HTML-indata och direkt konstruktion är ett projektbeslut snarare än ett biblioteksbeslut