Teknisk artikel

Flexbox, CSS Grid og fodnoter i PDF fra Delphi

PDF Library for Delphi renderer HTML ind på en PDF-side med reelt todimensionalt layout: display: flex og display: grid måles og placeres frem for at blive degraderet til stablede blokke, og fodnoter reserveres i bunden af den boks, der bærer deres reference, med nummerering, der forbliver fortløbende på tværs af spalter og sider. Indgangspunkterne er de velkendte, DrawHTMLTextBox til en enkelt boks og DrawHTMLStory til flerspaltet flow

Dette betyder noget, fordi HTML er, hvordan det meste rapportindhold ankommer nu. Skabeloner forfattes af folk, der skriver CSS, dashboards designes som kort, og en renderer, der stiltiende kollapser en flex-række til fire stablede blokke, producerer et dokument, der slet ikke ligner designet. Indtil denne evne fandtes, var den eneste todimensionale container, motoren målte, tabellen, så hvert kort-layout skulle genforfattes som en tabel i hånden

Hvad ændrede sig i layoutmodellen?

Den tidligere hovedløkke vedligeholdt én linjeboks og rykkede ned ad siden. Den model håndterer inline-indhold og stablede blokke fint og kan ikke udtrykke en container, hvis børn dimensioneres i forhold til hinanden. Tabeller var den eneste undtagelse, med deres eget to-pass-mål

Flex og grid tilføjer hver et afgrænset målegennemløb over en containers børn, og det vigtige ord er afgrænset. En flex-container måler op til 256 direkte børn ind i et fast array. Et grid bruger en belægningsmatrix på højst 64 gange 64 celler til deterministisk automatisk placering. De lofter findes, så et fjendtligt eller genereret stylesheet ikke kan drive ubegrænset rekursion eller kvadratisk placeringshukommelse, hvilket er en reel bekymring, når HTML'en kommer fra en skabelon, en kunde redigerer

Hvordan flex-elementer får deres størrelser

I rækkeretningen summerer containeren hvert elements basis sammen med dets vækst- og krympevægte, og fordeler derefter det resterende plads, positivt eller negativt, i henhold til de vægte. Med flex-wrap løses hver linje uafhængigt, så en række, der brydes i to linjer, tildeler fri plads pr. linje frem for på tværs af hele containeren. I kolonneretningen kører den samme hovedakse-fordeling mod enten en eksplicit højde eller indholdshøjden

justify-content, align-items, gap og de omvendte retninger opererer på geometri, der allerede er blevet målt. De flytter bokse; de udløser aldrig genmåling af elementindhold. Den adskillelse er, hvad der forhindrer et komplekst dashboard i at måle sine børn flere gange

uses
  PDFlibrary;

var
  Lib: TPDFlib;
  Html, Remainder: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.NewDocument;
    Lib.SetPageSize('A4');
    Lib.NewPage;

    Html :=
      '<div style="display:flex; gap:12px;">' +
      '  <div style="flex:2 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Revenue</b><br/>EUR 4,182,300</div>' +
      '  <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Margin</b><br/>18.4%</div>' +
      '  <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Backlog</b><br/>92 days</div>' +
      '</div>';

    Remainder := Lib.DrawHTMLTextBox(40, 40, 515, 120, Html);
    if Remainder <> '' then
      Log('content did not fit - carry the remainder to the next box');

    Lib.SaveToFile('dashboard.pdf');
  finally
    Lib.Free;
  end;
end;

Returværdien er fortsættelsesstrengen, hvilket er, hvordan hvert HTML-tegningsindgangspunkt rapporterer, hvad der ikke passede. Send den til den næste boks eller den næste side, og flowet genoptages, hvor det stoppede

Grid-placering, og hvad et spor kan være

Grid-spor accepterer faste længder, procentdele, fr-enheden, simple repeat()-udtryk og minmax(). Automatisk placering udfylder belægningsmatrixen deterministisk, så den samme HTML altid producerer den samme opstilling. Eksplicitte koordinater tillades at overlappe, hvilket er bevidst: et design, der lægger et badge oven på et kort, udtrykker en hensigt, ikke en fejl. Når kun én akse er angivet eksplicit, søger placering kun den anden akse

Elementer, der spænder over flere rækker, bidrager deres målte højde tilbage til de rækker, de dækker, gennemsnitligt fordelt over dem, hvilket forhindrer et højt spændende element i at klemme en enkelt række, mens dets naboer forbliver lave:

Html :=
  '<div style="display:grid; grid-template-columns:repeat(3, 1fr); ' +
  '            gap:10px;">' +
  '  <div style="grid-row:span 2; background:#eef;">Site plan</div>' +
  '  <div>Inspector</div>' +
  '  <div>Date</div>' +
  '  <div style="grid-column:2 / span 2;">Findings summary</div>' +
  '</div>';

Remainder := Lib.DrawHTMLTextBox(40, 180, 515, 260, Html);

Flex- og grid-børn renderes gennem den samme HTML-renderer som alt andet, hvilket er den egenskab, der gør funktionen brugbar frem for en separat verden. Skrifttyper, CSS-kaskaden, links, billeder, tabeller og yderligere indlejrede flex- eller grid-containere opfører sig alle inde i et flex-element præcis som på øverste niveau, og den ydre layoutplan registrerer de endelige tekst- og rektangelkommandoer, så gentagen tegning genbruger den eksisterende målingscache

Hvorfor er fodnoter et pagineringsproblem?

En fodnote er ikke indhold, der flyder efter det afsnit, der indeholder dens reference; det er indhold, der skal optræde i bunden af den samme boks som dens reference. Det vender den sædvanlige målerækkefølge om, fordi den plads, der er til rådighed for brødtekst, nu afhænger af indhold, der endnu ikke er layoutet

Rendereren måler derfor noten, når den møder referencen, og trækker noteområdet fra brødtekstens højdebudget for den aktuelle afgrænsede boks. Hvis referencen, brødteksten hidtil og noten ikke alle kan være der, flytter fodnotemarkøren og alt derefter ind i fortsættelsesstrengen sammen. Den regel er, hvad der forhindrer de to klassiske fejl: en note, der overtrykker brødteksten, og en note strandet på en side, hvis reference er på den forrige

I en afgrænset boks er noteområdet fastgjort i bunden med en skillelinje over det. I ubegrænset måling, hvor der ikke er nogen bokshøjde at fastgøre til, følger noteområdet umiddelbart efter brødteksten. Nummerering bæres i et udvidelsesfelt på fortsættelsesstakken, så DrawHTMLTextBox og DrawHTMLStory holder sekvensen kørende på tværs af spalter og sider, og en fortsættelsesstreng produceret før det felt eksisterede genoptages stadig korrekt

// Fodnoter inde i en flerspaltet story holder én løbende sekvens
Html := LoadTemplate('chapter.html');    // bruger float:footnote-markører
Remainder := Lib.DrawHTMLStory(40, 40, 515, 700,
  2,        // spalter
  16,       // rendegang i punkter
  20,       // maksimalt antal sider til denne story
  Html);
if Remainder <> '' then
  Log('story exceeded its page budget');

Praktisk vejledning til skabelonforfattere

Design inden for de dokumenterede lofter. En flex-container med mere end 256 direkte børn er næsten altid en datatabel klædt ud som flex, og tabelstien måler den bedre alligevel. Et grid større end 64 gange 64 er et regneark, og samme råd gælder. Til flerspaltet brødtekst styrer spalte- og orddelingsadfærden beskrevet i orddeling og balancerede tekstspalter, hvordan flowet ser ud inde i hver spalte

Mål før du tegner, når et layout skal passe. GetHTMLTextHeight rapporterer den højde, en given bredde ville have brug for, hvilket er den billige måde at vælge mellem ét layout og et andet, før man forpligter sig med blæk. Og behandl en ikke-tom fortsættelsesstreng som normal frem for exceptionel: det er mekanismen, hvormed langt indhold paginerer, ikke et fejlsignal

Hvor HTML'en kommer fra en rapportmotor frem for fra håndskrevne skabeloner, samvirker den datasæt-drevne rute i datasæt-rapportmotoren godt med dette og genererer den markup, flex og grid derefter arrangerer. Og når det samme indhold også skal forlade PDF'en igen, lukker den semantiske eksportsti i eksport af PDF til Markdown og DOCX rundturen

HTML-layout, rapportgenerering og semantisk eksport er en del af ét bibliotek til Delphi, C++Builder og Free Pascal; den komplette funktionsliste findes på siden PDF Library til Delphi