Teknisk artikkel

Flexbox, CSS Grid og fotnoter i PDF fra Delphi

PDF Library for Delphi gjengir HTML inn i en PDF-side med ekte todimensjonal layout: display: flex og display: grid måles og plasseres i stedet for å degraderes til stablede blokker, og fotnoter reserveres nederst i boksen som bærer referansen deres, med nummerering som forblir løpende på tvers av spalter og sider. Inngangspunktene er de kjente, DrawHTMLTextBox for én enkelt boks og DrawHTMLStory for flerspaltet flyt

Dette betyr noe fordi HTML er hvordan mesteparten av rapportinnhold kommer inn nå. Maler forfattes av folk som skriver CSS, dashbord designes som kort, og en gjengiver som stille kollapser en flex-rad til fire stablede blokker, produserer et dokument som ikke ligner designet i det hele tatt. Før denne muligheten fantes, var den eneste todimensjonale beholderen motoren målte, tabellen, så hver korttlayout måtte forfattes på nytt som en tabell for hånd

Hva endret seg i layoutmodellen?

Den forrige hovedløkken vedlikeholdt én linjeboks og gikk nedover siden. Den modellen håndterer inline-innhold og stablede blokker perfekt og kan ikke uttrykke en beholder hvis barn er dimensjonert i forhold til hverandre. Tabeller var det eneste unntaket, med sin egen totrinns-måling

Flex og grid legger hver til en avgrenset målepassering over barna til en beholder, og det viktige ordet er avgrenset. En flex-beholder måler opptil 256 direkte barn inn i et fast array. Et grid bruker en opptattmatrise på høyst 64 ganger 64 celler for deterministisk automatisk plassering. De takene finnes slik at et fiendtlig eller generert stilark ikke kan drive ubegrenset rekursjon eller kvadratisk plasseringsminne, noe som er en reell bekymring når HTML-en kommer fra en mal en kunde redigerer

Hvordan flex-elementer får sine størrelser

I radretningen summerer beholderen hvert elements basis sammen med sine vekst- og krympevekter, og fordeler deretter overskuddsplassen, positiv eller negativ, i henhold til de vektene. Med flex-wrap løses hver linje uavhengig, så en rad som brytes i to linjer, tildeler ledig plass per linje snarere enn på tvers av hele beholderen. I kolonneretningen kjøres den samme hovedakse-fordelingen mot enten en eksplisitt høyde eller innholdshøyden

justify-content, align-items, gap og de omvendte retningene opererer på geometri som allerede er målt. De flytter bokser; de utløser aldri ny måling av elementinnhold. Det skillet er det som hindrer et komplekst dashbord fra å måle barna sine flere ganger

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;

Returverdien er fortsettelsesstrengen, som er hvordan hvert HTML-tegneinngangspunkt rapporterer hva som ikke fikk plass. Send den til neste boks eller neste side, og flyten gjenopptas der den stoppet

Grid-plassering, og hva et spor kan være

Grid-spor tar imot faste lengder, prosenter, fr-enheten, enkle repeat()-uttrykk og minmax(). Automatisk plassering fyller opptattmatrisen deterministisk, så den samme HTML-en produserer alltid det samme oppsettet. Eksplisitte koordinater får lov til å overlappe, noe som er bevisst: et design som legger et merke over et kort, uttrykker en hensikt, ikke en feil. Når bare én akse er gitt eksplisitt, søker plasseringen bare i den andre aksen

Elementer som spenner over flere rader, bidrar med sin målte høyde tilbake til radene de dekker, gjennomsnittsberegnet på tvers av dem, noe som hindrer et høyt spennende element fra å klemme sammen en enkelt rad mens naboene forblir korte:

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-barn gjengis gjennom den samme HTML-gjengiveren som alt annet, som er egenskapen som gjør funksjonen brukbar snarere enn en egen verden. Fonter, CSS-kaskaden, lenker, bilder, tabeller og ytterligere nøstede flex- eller grid-beholdere oppfører seg alle inni et flex-element nøyaktig som de gjør på toppnivå, og den ytre layoutplanen registrerer de endelige tekst- og rektangelkommandoene, slik at gjentatt tegning gjenbruker den eksisterende målebufferen

Hvorfor er fotnoter et pagineringsproblem?

En fotnote er ikke innhold som flyter etter avsnittet som inneholder referansen dens; det er innhold som må opptre nederst i den samme boksen som referansen. Det snur den vanlige målerekkefølgen på hodet, fordi plassen tilgjengelig for brødtekst nå avhenger av innhold som ennå ikke er lagt ut

Gjengiveren måler derfor noten når den møter referansen, og trekker notearealet fra brødtekstens høydebudsjett for den gjeldende avgrensede boksen. Hvis referansen, brødteksten så langt og noten ikke alle får plass, flytter fotnotemarkøren og alt etter den sammen inn i fortsettelsesstrengen. Den regelen er det som forhindrer de to klassiske feilene: en note som overtrykker brødteksten, og en note strandet på en side hvis referanse er på den forrige

I en avgrenset boks er notearealet festet til bunnen med en skillelinje over det. I ubegrenset måling, der det ikke finnes noen bokshøyde å feste til, følger notearealet umiddelbart etter brødteksten. Nummerering bæres i et utvidelsesfelt på fortsettelsesstakken, slik at DrawHTMLTextBox og DrawHTMLStory holder sekvensen løpende på tvers av spalter og sider, og en fortsettelsesstreng produsert før det feltet fantes, gjenopptas fremdeles korrekt

// Fotnoter inni en flerspaltet story holder én løpende sekvens
Html := LoadTemplate('chapter.html');    // bruker float:footnote-markører
Remainder := Lib.DrawHTMLStory(40, 40, 515, 700,
  2,        // spalter
  16,       // renne i punkter
  20,       // maksimalt antall sider for denne storyen
  Html);
if Remainder <> '' then
  Log('story exceeded its page budget');

Praktisk veiledning for malforfattere

Design innenfor de dokumenterte takene. En flex-beholder med mer enn 256 direkte barn er nesten alltid en datatabell kledd ut som flex, og tabellbanen måler den bedre uansett. Et grid større enn 64 ganger 64 er et regneark, og det samme rådet gjelder. For flerspaltet brødtekst styrer spalte- og orddelingsoppførselen beskrevet i orddeling og balanserte tekstspalter hvordan flyten ser ut inni hver spalte

Mål før du tegner når en layout må passe. GetHTMLTextHeight rapporterer høyden en gitt bredde ville trenge, som er den billige måten å velge mellom én layout og en annen på før du forplikter blekk. Og behandle en ikke-tom fortsettelsesstreng som normalt snarere enn unntaksvis: det er mekanismen som langt innhold pagineres gjennom, ikke et feilsignal

Der HTML-en kommer fra en rapportmotor snarere enn fra håndskrevne maler, samspiller den datasettdrevne ruten i datasett-rapportmotoren godt med dette, og genererer markupen flex og grid deretter arrangerer. Og når det samme innholdet også må forlate PDF-en igjen, lukker den semantiske eksportbanen i eksport av PDF til Markdown og DOCX rundturen

HTML-layout, rapportgenerering og semantisk eksport er del av ett bibliotek for Delphi, C++Builder og Free Pascal; den komplette funksjonslisten finnes på PDF Library for Delphi-siden