HotPDF Component sætter CSS multi-column tekst op, når THPDFHTMLImporter.RenderHTML5 konverterer HTML til PDF i Delphi. Den pagede HTML-motor resolver column-count og column-width mod content box'en, balancerer den sidste fragment af hver multicol-container med en binærsøgning, fylder tidligere sider kolonne for kolonne til sidebunden og understøtter column-fill, column-span: all og column-rule
Den sædvanlige grund til at ønske dette er kedelig og reel: en tokolonnet produktmeddelelse, et vilkårsbilag sat i tre smalle kolonner, eller en release notes-PDF, der skal ligne det trykte nyhedsbrev, marketingteamet allerede har designet i HTML. Uden kolonneunderstøttelse stakker importeren hvert afsnit i ét bredt mål, linjelængden fordobles, og dokumentet læses som en logfil. Med den driver det samme stylesheet, der driver browser-preview'en, PDF'en, og den svære del bliver paginering, som browsere stort set aldrig behøver tænke på på skærmen
Hvordan beslutter HotPDF, hvor mange kolonner der skal bruges?
HotPDF resolver kolonneantallet med samme regler som pseudo-algoritmen i CSS Multi-column Layout Module Level 1 §3.4, anvendt på content box-bredden W, efter padding og kanter er fjernet. En erklæret column-count tages, som den er. Er kun column-width givet, beregner motoren N = Trunc((W + gap) / (column-width + gap)) med et gulv på én kolonne. Er begge til stede, er resultatet det mindste af de to, så det erklærede antal kan sætte loft over det afledte antal, men aldrig puste det op
Et konkret tilfælde: en content box på 483 pt (A4 med 56 pt sidemargener, afrundet) og column-width: 140pt; column-gap: 12pt giver Trunc(495 / 152) = 3 kolonner, hver (483 - 2 * 12) / 3 = 153pt bred. Tilføjes column-count: 2 til samme regel, falder det til to kolonner, fordi minimum vinder. Nøgleordet normal for gap resolver til 1em af containerens fontstørrelse, og den resolution sker ved layouttid frem for ved parsetid, så en font-size erklæret efter column-gap i samme regel stadig giver det rigtige gap
Én vagt er ikke i specifikationen, og den er der med vilje. Efter at kolonnebredden er beregnet, kollapser enhver værdi under 12 pt containeren tilbage til én enkelt kolonne. Et stylesheet, der siger column-count: 9999, enten ved et uheld eller fordi det kom fra en utroværdig skabelon, ville ellers producere tusindvis af kolonner med negative eller mikroskopiske bredder og en linjeombryder, der forsøger at få én glyph pr. linje. At falde tilbage til én kolonne holder outputtet læsbart og layoutomkostningen afgrænset. Nul eller negative antal når aldrig så langt: validatoren accepterer kun auto eller et positivt heltal for column-count, og column-width og column-gap tager henholdsvis auto eller normal, eller en længde i points eller em
Rendering af en multi-column HTML-sektion til PDF i Delphi
Multi-column output behøver ingen kontakt på importeren. THPDFHTMLImporter oprettes med det THotPDF-dokument, den tegner ind i, BeginDoc skal allerede have kørt, og kolonneegenskaberne ankommer gennem den normale kaskade, enten i markuppet eller i det forfatters stylesheet, der gives med som andet argument til RenderHTML5
uses
SysUtils, HPDFDoc, HPDFHTMLImport;
procedure BuildBulletin(const FileName: string);
var
Pdf: THotPDF;
Importer: THPDFHTMLImporter;
Html, Css: WideString;
begin
Pdf := THotPDF.Create(nil);
Importer := THPDFHTMLImporter.Create(Pdf);
try
Pdf.FileName := FileName;
Pdf.BeginDoc;
Importer.Margin := 56;
Importer.BaseFontName := 'Arial';
Importer.BaseFontSize := 10;
Css := 'section.news { column-width: 140pt; column-gap: 12pt; ' +
' column-rule: 0.5pt solid #999999 } ' +
'section.news h2 { column-span: all }';
Html := '<section class="news">' +
'<p>Release 4.2 ships the new export pipeline...</p>' +
'<p>Licensing changes take effect on 1 March...</p>' +
'<h2>Known issues</h2>' +
'<p>Import of legacy archives & templates...</p>' +
'</section>';
if not Importer.RenderHTML5(Html, Css) then
raise Exception.Create('Nothing was emitted');
Pdf.EndDoc;
finally
Importer.Free;
Pdf.Free;
end;
end;
Bemærk den dobbelte escaping i sidste afsnit: & inde i Pascal-strengen er HTML-entity'en for en ampersand, som importeren decoder tilbage til ét enkelt tegn. Resten af kaskaden, budgetterne og resource callbacks opfører sig præcis som beskrevet i oversigten over HotPDF HTML5 paged-media import; kolonner er bare endnu en layoutmode inde i samme profil
At balancere kolonner med en binærsøgning
HotPDF balancerer kolonner ved at søge efter den mindste kolonnehøjde, hvorved en grådig udfyldning stadig placerer hver resterende linje. Det grådige pass er enkelt: gå linjerne ind i kolonne ét, til næste linje ville overskride kandidathøjden, fortsæt så i kolonne to og så videre. Søgningen starter med den højeste enkeltlinje som nedre grænse og summen af alle linjehøjder som øvre grænse, halverer intervallet hver runde og stopper, når grænserne er inden for 0,05 pt
En binærsøgning virker kun, hvis prædikatet er monotont, og her er det: en højere kolonne kan aldrig rumme færre linjer end en lavere, så "alle linjer passer" flipper fra falsk til sand præcis én gang, efterhånden som højden vokser. Den egenskab garanterer konvergens og garanterer, at den afsluttende grådige udfyldning ved den fundne højde bruger hver linje i stedet for at efterlade en enke-linje, der ville udløse en falsk ekstra side. Det forklarer også en detalje i resultatet: det balancerede bånd er så kort, som linjeboksene tillader, ikke nødvendigvis lige kolonner, fordi linjer er udelelige, og den sidste kolonne kan ende en linje eller to højere end de andre
Hvad sker der, når kolonner krydser et sideskift?
Kolonner, der ikke passer på den aktuelle side, fyldes sekventielt til sidebunden, og kun den sidste sidefragment balanceres. Det er adfærden, CSS Multi-column Layout Level 1 §7.1 foreskriver for column-fill: balance i fragmenterede kontekster, og det er, hvordan et trykt nyhedsbrev ser ud: fulde kolonner på hver side undtagen den sidste, hvor den resterende tekst deles jævnt i stedet for at efterlade én høj kolonne ved siden af en tom
Hver sidefragment går gennem den samme fragmenter uafhængigt. Passer de resterende linjer ind i N kolonner af den tilgængelige højde, balanceres fragmentet; ellers fylder hver kolonne til sidebunden, og løkken starter en ny side. Containerens naturlige højde, som motoren bruger til break-inside: avoid-beslutninger og udstrækningsmåling, er den balancerede båndhøjde med ubegrænset kapacitet, så en container, der ville passe på en frisk side som en balanceret blok, flyttes dertil intakt frem for at blive skåret. Er en enkelt linje højere end en hel side, afslapper motoren begrænsningen, placerer linjen alligevel øverst på en side og rapporterer en hdkPaginationConstraint-diagnostik med subjektet multi-column
column-fill: auto og bokse med fast højde
Med column-fill: auto og en definit height fylder HotPDF kolonnerne i rækkefølge, og hver kolonnehøjde er det mindste af den resterende sideplads og containerhøjden minus dens padding og kanter. Minimum betyder mere, end det ser ud. Kun at bruge den resterende sideplads ville lade første kolonne sluge hver linje, når helst siden har mere plads tilbage end boksen. Uden en definit højde falder column-fill: auto tilbage til balancing, som browsere gør, da en ubegrænset multicol-container ikke har nogen kolonnehøjde at fylde til; den første implementering missede den fallback, og én af de 27 regressionstjek fangede den
column-span: all og indlejrede afsnit
HotPDF understøtter column-span: all på direkte børn af en multi-column-container, efter CSS Multi-column Layout Level 1 §6.1. Et span-barn splitter containeren i segmenter: børnene før det danner ét kolonnebånd, span-elementet renders hen over fuld bredde, og børnene efter det starter et nyt bånd. Hvert bånd balanceres, fyldes, forsynes med regler og pagineres på egen hånd, så en overskrift midt i en trekolonne-sektion producerer to balancerede bånd med en overskrift i fuld bredde imellem, hvilket er det layout, folk faktisk tegner i mockups
At komme dertil krævede først at fikse et mindre synligt problem. En container som <div style="column-count:2"> med flere <p>-børn rendered hvert afsnit i én enkelt stakket kolonne, fordi hvert afsnit blev laid ud som sin egen tekstblok, og kolonneerklæringen på forælderen aldrig blev anvendt. HotPDF indsamler nu teksten fra indlejrede blokbørn i ét flow for containeren. Indsamleren skal indsætte et hårdt skift ved hver blokgrænse undervejs; springer man det over, sammenkædes sidste ord af ét afsnit med første ord af det næste på samme linje, en fejl der er let at overse, når testteksten tilfældigvis ender i tegnsætning
Det sammenslåede flow har et bevidst omfang. Det bruges kun, når hele subtree er ren tekstflow. En container, der rummer form controls, billeder, tabeller, lister eller <hr> hvor som helst indeni, beholder den almindelige blokrute og renders i én enkelt kolonne, fordi den sammenslåede tekstindsamler ikke kan udtrykke deres geometri. Tabeller har især deres egne pagineringsregler, dækket i artiklen om rowspan-håndtering og gentagne tabelheadere i HotPDF, og at blande dem ind i et kolonnebånd ville knække de garantier
Hvor column reglen ligger, og hvordan man tester den
HotPDF tegner column-rule på midterlinjen af hvert gap, ved en kolonnes højre kant plus halvdelen af gap'et, fra toppen til bunden af båndet på hver sidefragment, som CSS Multi-column Layout Level 1 §4 beskriver. Bredden default'er til 1 pt. Farven default'er til elementets tekstfarve, når den ikke er erklæret eller er currentColor. column-rule-shorthanden uden style-nøgleord behandles som solid, matchende hvordan border-shorthanden typisk læses, mens none og hidden undertrykker reglen
Kolonnegeometrien kan observeres uden at parse PDF'en. OnLineLayout-eventet på THPDFHTMLImporter fyres én gang pr. tegnet linje med en THPDFHTMLLineLayout-post, og fordi motoren tegner hver kolonne separat, er X-feltet for hver linje dens kolonnes venstre kant. Nabokolonner adskiller sig med præcis kolonnebredden plus gap'et, hvilket gør det nemt at skelne et 12 pt-gap fra et 20 pt-gap i en regressionstest
type
TColumnProbe = class
public
Lines: TStringList;
constructor Create;
destructor Destroy; override;
procedure LineDrawn(Sender: TObject; const Line: THPDFHTMLLineLayout);
end;
constructor TColumnProbe.Create;
begin
inherited Create;
Lines := TStringList.Create;
end;
destructor TColumnProbe.Destroy;
begin
Lines.Free;
inherited;
end;
procedure TColumnProbe.LineDrawn(Sender: TObject;
const Line: THPDFHTMLLineLayout);
begin
// PageIndex er nul-baseret; X er den linjes kolonnes venstre kant
Lines.Add(Format('page=%d x=%.1f y=%.1f %s',
[Line.PageIndex, Line.X, Line.Y, string(Line.Text)]));
end;
// Kobling, før RenderHTML5:
// Probe := TColumnProbe.Create;
// Importer.OnLineLayout := Probe.LineDrawn;
To praktiske noter fra at skrive tests mod dette event. PageIndex er nul-baseret, hvilket er det første, en håndskrevet assertion plejer at få forkert. Og X inkluderer justeringsforskydninger, så sammenlign for centreret eller højrejusteret tekst kolonnepositioner på venstrejusterede fixtures. Reglen selv optræder ikke i eventet; for at asserte dens position, så scan den ukomprimerede content stream efter de vertikale m- og l-operatorer
Grænser og fælder værd at kende
Et par adfærdsformer er approksimationer, og de er bedre kendt før en layoutgennemgang end efter. Afsnitsafstand inde i et sammenslået kolonneflow approksimeres af én linje af containerens linjehøjde ved hver blokgrænse, så per-afsnit-margener inde i et kolonnebånd gengives ikke eksakt. Når column-span: all splitter en container, gentager hvert segment containerens padding og baggrundsboks, hvilket holder segmenterne visuelt kontinuerlige, men ikke er én enkelt boks i streng CSS-forstand. Enhver column-rule-style-værdi undtagen none og hidden består validering, men reglen streges som en solid linje
Ikke-understøttede værdier fejler ikke lydløst. At sætte StrictCSS til True forvandler hver diagnostik til en EHPDFHTMLUnsupportedCSS-exception, mens OnDiagnostic lader et batchjob logge dem og fortsætte. Bemærk, at strict mode også gælder pagineringsdiagnostikken, så et dokument med en linje højere end en side vil rejse sig under StrictCSS i stedet for stille at afslappe begrænsningen
procedure TReportJob.HtmlDiagnostic(Sender: TObject;
const Diagnostic: THPDFHTMLDiagnostic);
begin
case Diagnostic.Kind of
hdkUnsupportedProperty, hdkUnsupportedValue:
FLog.Add('CSS ' + Diagnostic.Subject + ': ' + Diagnostic.MessageText);
hdkPaginationConstraint:
FLog.Add('Layout relaxed (' + Diagnostic.Subject + '): ' +
Diagnostic.MessageText);
else
FLog.Add(Diagnostic.MessageText);
end;
end;
// Skabelonarbejde: fejlk hurtigt på alt, hvad motoren ville ignorere
Importer.StrictCSS := FValidatingTemplate;
Importer.OnDiagnostic := HtmlDiagnostic;
Strict mode er den rigtige indstilling, mens en skabelon skrives, da en tastefejl som column-span: both afvises med det samme i stedet for lydløst at lade overskriften flyde inde i kolonnerne. I produktion er logning gennem OnDiagnostic som regel den bedre handel, fordi én mærkelig erklæring i kundeleveret HTML ikke bør koste hele dokumentet. Identiske diagnostikker (samme kind, subjekt og værdi) rapporteres én gang, så længe der er plads i MaxDiagnostics-listen, der default'er til 256 indgange, så en erklæring, kaskaden besøger for hundredvis af elementer, ikke oversvømmer loggen
FAQ
Understøtter HotPDF columns-shorthanden?
Ja. columns-shorthanden accepterer højst ét positivt heltalsantal og én kolonnelængde i vilkårlig rækkefølge, og auto nulstiller begge longhands. De resolverede værdier følger samme antals- og bredderegler som de enkelte egenskaber
Kan en multi-column-container rumme billeder eller tabeller?
Det kan den rumme, men HotPDF renderer så den container i én enkelt kolonne. Kolonneflow anvendes kun, når hver efterkommer er ren tekstflow, fordi billeder, tabeller, lister og form controls har brug for deres egen boksegeometri og pagineringsregler
Hvorfor viser den sidste side balancerede kolonner, mens tidligere sider er fulde?
Det er CSS-reglen for column-fill: balance i paged media: kun den sidste fragment balanceres. Tidligere sider fylder hver kolonne til sidebunden, så ingen plads spildes midt i dokumentet
Multi-column layout, HTML5 paged-media-profilen og resten af HTML-til-PDF-pipelinen er en del af HotPDF Component for Delphi og C++Builder, som også dækker direkte PDF-tegning, formularer, kryptering og tekstudtrækning