Technisch artikel

Flexbox, CSS Grid en voetnoten in PDF vanuit Delphi

PDF Library for Delphi rendert HTML in een PDF-pagina met echte tweedimensionale lay-out: display: flex en display: grid worden gemeten en geplaatst in plaats van gedegradeerd tot gestapelde blokken, en voetnoten worden gereserveerd onderaan het vak dat hun verwijzing draagt, met nummering die doorlopend blijft over kolommen en pagina's heen. De toegangspunten zijn de vertrouwde: DrawHTMLTextBox voor één vak en DrawHTMLStory voor meerkoloms doorstroming

Dit doet ertoe omdat HTML tegenwoordig de manier is waarop de meeste rapportinhoud binnenkomt. Sjablonen worden opgesteld door mensen die CSS schrijven, dashboards worden ontworpen als kaarten, en een renderer die een flex-rij stilzwijgend laat instorten tot vier gestapelde blokken, produceert een document dat helemaal niet op het ontwerp lijkt. Tot deze mogelijkheid bestond, was de enige tweedimensionale container die de engine mat de tabel, dus elke kaartlay-out moest met de hand worden herschreven als tabel

Wat is er veranderd in het lay-outmodel?

De vorige hoofdlus onderhield één regelvak en werkte de pagina af naar beneden. Dat model handelt inline-inhoud en gestapelde blokken perfect af en kan geen container uitdrukken waarvan de kinderen ten opzichte van elkaar worden geformatteerd. Tabellen waren de enige uitzondering, met hun eigen tweepassmeting

Flex en grid voegen elk een begrensde meetpassage toe over de kinderen van een container, en het belangrijke woord is begrensd. Een flex-container meet tot 256 directe kinderen in een vaste array. Een grid gebruikt een bezettingsmatrix van ten hoogste 64 bij 64 cellen voor deterministische automatische plaatsing. Die plafonds bestaan zodat een vijandig of gegenereerd stylesheet geen onbegrensde recursie of kwadratisch plaatsingsgeheugen kan veroorzaken, wat een reëel zorgpunt is wanneer de HTML afkomstig is uit een sjabloon dat een klant bewerkt

Hoe flex-items hun formaat krijgen

In de rijrichting telt de container de basis van elk item op samen met zijn grow- en shrink-gewichten, en verdeelt vervolgens de overgebleven ruimte, positief of negatief, volgens die gewichten. Met flex-wrap wordt elke regel onafhankelijk opgelost, dus een rij die in twee regels breekt, kent vrije ruimte per regel toe in plaats van over de hele container. In de kolomrichting loopt dezelfde hoofdas-verdeling tegen ofwel een expliciete hoogte ofwel de inhoudshoogte

justify-content, align-items, gap en de omgekeerde richtingen werken op geometrie die al is gemeten. Ze verplaatsen vakken; ze veroorzaken nooit herberekening van de item-inhoud. Die scheiding is wat een complex dashboard ervan weerhoudt zijn kinderen meerdere keren te meten

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;

De retourwaarde is de voortzettingsstring, wat de manier is waarop elk HTML-tekentoegangspunt rapporteert wat niet paste. Geef die door aan het volgende vak of de volgende pagina en de doorstroming hervat waar ze stopte

Grid-plaatsing, en wat een track kan zijn

Grid-tracks accepteren vaste lengtes, percentages, de fr-eenheid, eenvoudige repeat()-expressies en minmax(). Automatische plaatsing vult de bezettingsmatrix deterministisch, dus dezelfde HTML produceert altijd dezelfde indeling. Expliciete coördinaten mogen overlappen, wat bewust is: een ontwerp dat een badge over een kaart legt, drukt intentie uit, geen fout. Wanneer slechts één as expliciet is opgegeven, zoekt de plaatsing alleen op de andere as

Items die zich over meerdere rijen uitstrekken, dragen hun gemeten hoogte terug bij aan de rijen die ze bestrijken, gemiddeld verdeeld erover, wat voorkomt dat een hoog, overspannend item één rij samenperst terwijl zijn buren te kort blijven:

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- en grid-kinderen worden gerenderd via dezelfde HTML-renderer als al het andere, wat de eigenschap is die de functie bruikbaar maakt in plaats van een aparte wereld. Lettertypen, de CSS-cascade, links, afbeeldingen, tabellen en verder geneste flex- of grid-containers gedragen zich binnen een flex-item precies zoals op het hoogste niveau, en het buitenste lay-outplan legt de uiteindelijke tekst- en rechthoekopdrachten vast, zodat herhaald tekenen de bestaande meetcache hergebruikt

Waarom zijn voetnoten een pagineringsprobleem?

Een voetnoot is geen inhoud die volgt na de alinea die de verwijzing ernaar bevat; het is inhoud die moet verschijnen onderaan hetzelfde vak als zijn verwijzing. Dat keert de gebruikelijke meetvolgorde om, omdat de beschikbare ruimte voor de hoofdtekst nu afhangt van inhoud die nog niet is uitgelijnd

De renderer meet de noot daarom zodra hij de verwijzing tegenkomt, en trekt het notitiegebied af van het hoogtebudget van de hoofdtekst van het huidige begrensde vak. Als de verwijzing, de hoofdtekst tot dusver en de noot niet allemaal passen, verhuizen de voetnootmarkering en alles erna samen naar de voortzettingsstring. Die regel is wat de twee klassieke fouten voorkomt: een noot die over de hoofdtekst heen drukt, en een noot die gestrand is op een pagina waarvan de verwijzing op de vorige staat

In een begrensd vak wordt het notitiegebied onderaan vastgezet met een scheidingslijn erboven. Bij onbegrensde meting, waar geen vakhoogte is om aan vast te zetten, volgt het notitiegebied onmiddellijk na de hoofdtekst. Nummering wordt gedragen in een uitbreidingsveld op de voortzettingsstack, dus DrawHTMLTextBox en DrawHTMLStory houden de reeks doorlopend over kolommen en pagina's heen, en een voortzettingsstring die is geproduceerd voordat dat veld bestond, hervat nog steeds correct

// Voetnoten binnen een meerkoloms story houden één doorlopende reeks aan
Html := LoadTemplate('chapter.html');    // gebruikt float:footnote-markeringen
Remainder := Lib.DrawHTMLStory(40, 40, 515, 700,
  2,        // kolommen
  16,       // gutter in punten
  20,       // maximum aantal pagina's voor deze story
  Html);
if Remainder <> '' then
  Log('story exceeded its page budget');

Praktisch advies voor sjabloonmakers

Ontwerp binnen de gedocumenteerde plafonds. Een flex-container met meer dan 256 directe kinderen is bijna altijd een datatabel in flex-kostuum, en het tabelpad meet die toch beter. Een grid groter dan 64 bij 64 is een spreadsheet, en hetzelfde advies geldt. Voor meerkoloms hoofdtekst bepaalt het kolom- en afbrekingsgedrag beschreven in woordafbreking en gebalanceerde tekstkolommen hoe de doorstroming er binnen elke kolom uitziet

Meet voordat u tekent wanneer een lay-out moet passen. GetHTMLTextHeight rapporteert de hoogte die een gegeven breedte nodig zou hebben, wat de goedkope manier is om tussen de ene lay-out en de andere te kiezen voordat u inkt vastlegt. En behandel een niet-lege voortzettingsstring als normaal in plaats van uitzonderlijk: het is het mechanisme waarmee lange inhoud pagineert, geen foutsignaal

Wanneer de HTML afkomstig is van een rapportmotor in plaats van van handgeschreven sjablonen, sluit de datasetgestuurde route in de dataset-rapportmotor hier goed op aan, en genereert de markup die flex en grid vervolgens ordenen. En wanneer dezelfde inhoud ook weer uit de PDF moet komen, sluit het semantische exportpad in PDF exporteren naar Markdown en DOCX de kringloop

HTML-lay-out, rapportgeneratie en semantische export maken deel uit van één bibliotheek voor Delphi, C++Builder en Free Pascal; de volledige functielijst staat op de PDF Library for Delphi-pagina