Technisch artikel

CSS multi-column layout in HTML naar PDF met HotPDF Delphi

HotPDF Component zet CSS multi-column-tekst op wanneer THPDFHTMLImporter.RenderHTML5 in Delphi HTML naar PDF converteert. De gepagineerde HTML-engine resolvt column-count en column-width tegen de content box, balanceert het laatste fragment van elke multicol-container met een binary search, vult eerdere pagina's kolom voor kolom tot de paginabodem en ondersteunt column-fill, column-span: all en column-rule

De gebruikelijke reden om dit te willen is saai en echt: een productbulletin in twee kolommen, een algemene-voorwaardenbijlage gezet in drie smalle kolommen, of een release notes-PDF die eruit moet zien als de gedrukte nieuwsbrief die de marketingafdeling al in HTML heeft ontworpen. Zonder kolomsteun stapelt de importer elke alinea op één brede zetregel, verdubbelt de regellengte en leest het document als een logbestand. Met kolomsteun stuurt dezelfde stylesheet die de browservoorvertoning stuurt ook de PDF, en wordt het moeilijke deel paginering, waar browsers op het scherm meestal nooit over hoeven na te denken

Hoe bepaalt HotPDF hoeveel kolommen er worden gebruikt?

HotPDF resolvt het kolomaantal met dezelfde regels als het pseudo-algoritme in CSS Multi-column Layout Module Level 1 §3.4, toegepast op de content box-breedte W nadat padding en randen zijn verwijderd. Een gedeclareerde column-count wordt overgenomen zoals hij staat. Geeft u alleen column-width, dan berekent de engine N = Trunc((W + gap) / (column-width + gap)) met een vloer van één kolom. Zijn beide aanwezig, dan is het resultaat de kleinste van de twee, dus het gedeclareerde aantal kan het afgeleide aantal kappen maar nooit opblazen

Een concreet geval: een content box van 483pt (A4 met zijmarges van 56pt, afgerond) en column-width: 140pt; column-gap: 12pt geeft Trunc(495 / 152) = 3 kolommen, elk (483 - 2 * 12) / 3 = 153pt breed. column-count: 2 aan dezelfde regel toevoegen verlaagt het naar twee kolommen, want het minimum wint. Het gap-trefwoord normal resolvt naar 1em van de lettergrootte van de container, en die resolutie vindt plaats op lay-outtijd in plaats van op parsetijd, dus een font-size gedeclareerd ná column-gap in dezelfde regel levert toch de juiste gap

Resolutie van het HotPDF CSS multi-column-aantal voor HTML naar PDF in Delphi: column-count wordt zoals gedeclareerd gebruikt, column-width leidt N = Trunc((W + gap) / (column-width + gap)) af, de kleinste wint wanneer beide zijn gezet, en elke kolom onder 12pt klapt in tot één kolom
De engine past het CSS-pseudo-algoritme toe op de content box, kapt het afgeleide aantal met een gedeclareerde en voegt een vloerguard van 12pt toe zodat onbetrouwbare templates geen duizenden microscopische kolommen kunnen opleveren

Eén guard staat niet in de specificatie, en die staat er met opzet. Nadat de kolombreedte is berekend, klapt elke waarde onder 12pt de container terug naar één kolom. Een stylesheet die column-count: 9999 zegt, of nu per ongeluk of omdat hij uit een onbetrouwbaar template kwam, levert anders duizenden kolommen op met negatieve of microscopische breedtes en een regelbreekker die per regel één glyph probeert te passen. Terugvallen op één kolom houdt de output leesbaar en de lay-outkosten begrensd. Nul of negatieve aantallen komen nooit zo ver: de validator accepteert voor column-count alleen auto of een positief geheel getal, en column-width en column-gap nemen respectievelijk auto of normal, of een lengte in punten of em

Een multi-column HTML-sectie naar PDF renderen in Delphi

Multi-column-output heeft geen schakelaar op de importer nodig. THPDFHTMLImporter wordt aangemaakt met het THotPDF-document waarin hij tekent, BeginDoc moet al hebben gedraaid, en de kolomeigenschappen komen binnen via de gewone cascade, in de markup of in de author stylesheet die als tweede argument van RenderHTML5 wordt meegegeven

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 &amp; 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;

Let op de dubbele escaping in de laatste alinea: &amp; binnen de Pascal-string is de HTML-entiteit voor een ampersand, die de importer terugdecodeert naar één teken. De rest van de cascade, de budgets en de resource-callbacks gedragen zich exact zoals beschreven in het overzicht van HotPDF HTML5 paged-media import; kolommen zijn gewoon nog één lay-outmodus binnen hetzelfde profiel

Kolommen balanceren met een binary search

HotPDF balanceert kolommen door te zoeken naar de kleinste kolomhoogte waarbij een greedy vulling nog elke resterende regel plaatst. De greedy-pas is simpel: loop de regels in kolom één tot de volgende regel de kandidaathoogte zou overstijgen, ga dan verder in kolom twee, enzovoort. De zoektocht begint met de hoogste enkele regel als ondergrens en de som van alle regelhoogten als bovengrens, halveert het interval elke ronde en stopt zodra de grenzen binnen 0.05pt liggen

Multi-column-balancering van HotPDF in Delphi: een binary search tussen de hoogste enkele regel en de som van alle regelhoogten vindt de kleinste kolomhoogte waarvan de greedy vulling elke regel plaatst, met grenzen die binnen 0.05pt convergeren
Het elke-regel-past-predikaat is monotoon in kolomhoogte, dus het halveren van het interval convergeert naar de kortste gebalanceerde band en de laatste greedy vulling verbruikt elke regel zonder weesregels

Een binary search werkt alleen als het predikaat monotoon is, en dat is hier zo: een hogere kolom kan nooit minder regels bevatten dan een lagere, dus "elke regel past" kipt exact één keer van false naar true naarmate de hoogte groeit. Die eigenschap garandeert de convergentie en garandeert dat de laatste greedy vulling op de gevonden hoogte elke regel verbruikt in plaats van een weesregel achter te laten die een onechte extra pagina zou afdwingen. Het verklaart ook een detail van het resultaat: de gebalanceerde band is zo kort als de line boxes toelaten, niet noodzakelijk gelijke kolommen, want regels zijn ondeelbaar en de laatste kolom kan een regel of twee hoger eindigen dan de anderen

Wat gebeurt er wanneer kolommen een paginagrens oversteken?

Kolommen die niet op de huidige pagina passen worden achtereenvolgend tot de paginabodem gevuld, en alleen het laatste paginafragment wordt gebalanceerd. Dit is het gedrag dat CSS Multi-column Layout Level 1 §7.1 voorschrijft voor column-fill: balance in gefragmenteerde contexten, en het is hoe een gedrukte nieuwsbrief eruitziet: volle kolommen op elke pagina behalve de laatste, waar de resterende tekst gelijkmatig wordt gesplitst in plaats van één hoge kolom naast een lege te laten staan

Elk paginafragment gaat onafhankelijk door dezelfde fragmenter. Passen de resterende regels in N kolommen van de beschikbare hoogte, dan wordt het fragment gebalanceerd; zo niet, dan vult elke kolom tot de paginabodem en begint de lus een nieuwe pagina. De natuurlijke hoogte van de hele container, die de engine gebruikt voor break-inside: avoid-beslissingen en omvangmetingen, is de gebalanceerde bandhoogte met onbeperkte capaciteit, dus een container die op een verse pagina als gebalanceerd blok zou passen, wordt daar onaangetast naartoe verplaatst in plaats van gesneden. Is een enkele regel hoger dan een hele pagina, dan versoepelt de engine de beperking, plaatst de regel toch bovenaan een pagina en meldt een hdkPaginationConstraint-diagnostiek met het subject multi-column

Multi-column-paginering van HotPDF voor HTML naar PDF in Delphi: fragmenten vóór de laatste pagina vullen elke kolom tot de paginabodem, het laatste fragment balanceert over de kolommen, en column-span all splitst een container in apart gebalanceerde banden rond een element op volledige breedte
Alleen het laatste fragment van een multicol-container wordt gebalanceerd; eerdere pagina's vullen achtereenvolgend, en elk fragment herhaalt de balans, de regels en de paginering onafhankelijk

column-fill: auto en boxes met vaste hoogte

Met column-fill: auto en een definitieve height vult HotPDF de kolommen op volgorde, en elke kolomhoogte is de kleinste van de resterende paginaruimte en de containerhoogte minus padding en randen. Het minimum telt zwaarder dan het lijkt. Alleen de resterende paginaruimte gebruiken zou de eerste kolom elke regel laten inslikken zodra de pagina meer ruimte over heeft dan de box. Zonder definitieve hoogte valt column-fill: auto terug op balanceren, zoals browsers doen, want een onbeperkte multicol-container heeft geen kolomhoogte om naar te vullen; de eerste implementatie miste die fallback, en een van de 27 regressiechecks ving het

column-span: all en geneste alinea's

HotPDF ondersteunt column-span: all op directe kinderen van een multi-column-container, volgens CSS Multi-column Layout Level 1 §6.1. Een spanning kind splitst de container in segmenten: de kinderen ervoor vormen één kolomband, het spanning-element rendert over de volledige breedte, en de kinderen erna beginnen een nieuwe band. Elke band wordt op eigen kracht gebalanceerd, gevuld, van regels voorzien en gepagineerd, dus een kop middenin een sectie van drie kolommen levert twee gebalanceerde banden op met een kop op volledige breedte ertussen, en dat is de lay-out die mensen werkelijk in mockups tekenen

Daar komen vereiste eerst het oplossen van een minder zichtbaar probleem. Een container als <div style="column-count:2"> met verschillende <p>-kinderen rendert elke alinea vroeger in één gestapelde kolom, omdat elke alinea als eigen tekstblok werd opgezet en de kolomdeclaratie op de ouder nooit van toepassing was. HotPDF verzamelt de tekst van geneste blokkinderen nu in één flow voor de container. De collector moet daarbij op elke blokgrens een harde breek invoegen; hem overslaan plakt het laatste woord van de ene alinea aan het eerste woord van de volgende op dezelfde regel, een bug die makkelijk ontgaat wanneer de testtekst toevallig op interpunctie eindigt

Die gemergde flow heeft een bewuste reikwijdte. Hij wordt alleen gebruikt wanneer de hele subtree kale tekstflow is. Een container die ergens binnen zichzelf form controls, images, tables, lists of <hr> herbergt houdt de gewone blokroute aan en rendert in één kolom, want de gemergde tekstcollector kan hun geometrie niet uitdrukken. Tabellen hebben vooral hun eigen pagineringsregels, behandeld in het artikel over rowspan-afhandeling en herhaalde tabelkoppen in HotPDF, en ze in een kolomband mengen zou die garanties breken

Waar de column rule loopt, en hoe u hem test

HotPDF tekent column-rule op de middenlijn van elke gap, op de rechterrand van een kolom plus de halve gap, van boven tot onder de band op elk paginafragment, zoals CSS Multi-column Layout Level 1 §4 beschrijft. De breedte defaultt naar 1pt. De kleur defaultt naar de tekstkleur van het element wanneer die niet is gedeclareerd of currentColor is. De column-rule-shorthand zonder style-trefwoord wordt als solid behandeld, passend bij hoe de border-shorthand doorgaans wordt gelezen, terwijl none en hidden de regel onderdrukken

Kolomgeometrie is observeerbaar zonder de PDF te parsen. Het event OnLineLayout op THPDFHTMLImporter gaat één keer per getekende regel af met een THPDFHTMLLineLayout-record, en omdat de engine elke kolom apart tekent, is het veld X van elke regel de linkerrand van zijn kolom. Aangrenzende kolommen verschillen exact om de kolombreedte plus de gap, wat het makkelijk maakt om in een regressietest een gap van 12pt van een gap van 20pt te onderscheiden

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 is zero-based; X is de linkerrand van de kolom van de regel
  Lines.Add(Format('page=%d x=%.1f y=%.1f %s',
    [Line.PageIndex, Line.X, Line.Y, string(Line.Text)]));
end;

// Bedraden, vóór RenderHTML5:
//   Probe := TColumnProbe.Create;
//   Importer.OnLineLayout := Probe.LineDrawn;

Twee praktische notities uit het schrijven van tests tegen dit event. PageIndex is zero-based, en dat is het eerste wat een handgeschreven assertion doorgaans verkeerd doet. En X omvat uitlijningsverschuivingen, dus vergelijk voor gecentreerde of rechts uitgelijnde tekst kolomposities op links uitgelijnde fixtures. De regel zelf verschijnt niet in het event; om zijn positie te toetsen, scan de ongecomprimeerde content stream naar de verticale operators m en l

Grenzen en valkuilen die het kennen waard zijn

Een paar gedragingen zijn benaderingen, en die zijn beter bekend vóór een lay-outreview dan erna. Alinea-afstand binnen een gemergde kolomflow wordt benaderd met één regel van de regelhoogte van de container op elke blokgrens, dus per-alinea-marges binnen een kolomband worden niet exact gereproduceerd. Wanneer column-span: all een container splitst, herhaalt elk segment de containerpadding en de achtergrondbox, wat de segmenten visueel continu houdt maar geen enkele box is in de strikte CSS-zin. Elke column-rule-style-waarde anders dan none en hidden passeert de validatie, maar de regel wordt getekend als een solide lijn

Niet-ondersteunde waarden falen niet stil. StrictCSS op True zetten maakt van elke diagnostiek een EHPDFHTMLUnsupportedCSS-exception, terwijl OnDiagnostic een batchtaak ze laat loggen en doorgaan. Bedenk dat strikte modus ook voor de pagineringsdiagnostiek geldt, dus een document met een regel hoger dan een pagina gooit onder StrictCSS een exception in plaats van de beperking stilletjes te versoepelen

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;

// Template-authoring: vroeg falen op alles wat de engine zou negeren
Importer.StrictCSS := FValidatingTemplate;
Importer.OnDiagnostic := HtmlDiagnostic;

Strikte modus is de juiste instelling terwijl een template wordt geschreven, want een typefout als column-span: both wordt direct geweigerd in plaats van de kop stilletjes binnen de kolommen te laten vloeien. In productie is loggen via OnDiagnostic doorgaans de betere afweging, want één rare declaratie in door klanten aangeleverde HTML mag niet het hele document kosten. Identieke diagnostiek (zelfde kind, subject en waarde) wordt één keer gemeld zolang ze in de lijst MaxDiagnostics past, die default naar 256 entries gaat, dus een declaratie die de cascade voor honderden elementen bezoekt zet het log niet onder water

FAQ

Ondersteunt HotPDF de columns-shorthand?

Ja. De columns-shorthand accepteert ten hoogste één positief geheel aantal en één kolomlengte, in willekeurige volgorde, en auto reset beide longhands. De geresolvde waarden volgen dezelfde aantal- en breedteregels als de individuele eigenschappen

Kan een multi-column-container images of tabellen bevatten?

Ze kunnen erin, maar HotPDF rendert die container dan in één kolom. Kolomflow wordt alleen toegepast wanneer elke afstammeling kale tekstflow is, want images, tabellen, lists en form controls hebben hun eigen boxgeometrie en pagineringsregels nodig

Waarom toont de laatste pagina gebalanceerde kolommen terwijl eerdere pagina's vol zijn?

Dat is de CSS-regel voor column-fill: balance in paged media: alleen het laatste fragment wordt gebalanceerd. Eerdere pagina's vullen elke kolom tot de paginabodem zodat er halverwege het document geen ruimte verloren gaat

Multi-column-layout, het HTML5 paged-media-profiel en de rest van de HTML-naar-PDF-pipeline maken deel uit van de HotPDF Component for Delphi and C++Builder, die ook direct PDF-tekenwerk, formulieren, versleuteling en tekstextractie dekt