Technický článek

Flexbox, CSS Grid a poznámky pod čarou v PDF z Delphi

PDF Library for Delphi vykresluje HTML do stránky PDF se skutečným dvourozměrným rozvržením: display: flex a display: grid se měří a umísťují místo toho, aby se degradovaly na naskládané bloky, a poznámky pod čarou se rezervují ve spodní části boxu, který nese jejich odkaz, s číslováním, jež zůstává průběžné napříč sloupci a stránkami. Vstupní body jsou ty známé, DrawHTMLTextBox pro jeden box a DrawHTMLStory pro vícesloupcový tok

Na tom záleží, protože HTML je dnes způsob, jakým dorazí většina obsahu sestav. Šablony píší lidé, kteří umí CSS, dashboardy jsou navrženy jako karty, a vykreslovač, který tiše zhroutí řádek flex do čtyř naskládaných bloků, vytvoří dokument, který návrhu vůbec neodpovídá. Než tato schopnost existovala, jediným dvourozměrným kontejnerem, který motor měřil, byla tabulka, takže každé rozvržení karet muselo být ručně přepsáno jako tabulka

Co se změnilo v modelu rozvržení?

Předchozí hlavní smyčka udržovala jediný řádkový box a postupovala po stránce dolů. Tento model bezvadně zvládá inline obsah a naskládané bloky a neumí vyjádřit kontejner, jehož potomci jsou velikostně vztaženi jeden k druhému. Jedinou výjimkou byly tabulky, s vlastním dvouprůchodovým měřením

Flex i grid přidávají omezený průchod měřením přes potomky kontejneru, a důležité slovo je omezený. Flex kontejner měří až 256 přímých potomků do pole pevné velikosti. Grid používá matici obsazenosti nejvýše 64 krát 64 buněk pro deterministické automatické umístění. Tyto stropy existují proto, aby nepřátelský nebo generovaný styl nemohl vyvolat neomezenou rekurzi nebo kvadratickou paměť pro umístění, což je reálná obava, když HTML pochází ze šablony, kterou upravuje zákazník

Jak flex položky získávají svou velikost

Ve směru řádku kontejner sečte základ každé položky spolu s jejími váhami růstu a zmenšení, poté rozdělí zbývající prostor, kladný nebo záporný, podle těchto vah. S flex-wrap se každý řádek řeší nezávisle, takže řádek rozdělený do dvou řádků přiřadí volný prostor podle řádku, ne napříč celým kontejnerem. Ve směru sloupce běží stejné rozdělení podél hlavní osy proti buď explicitní výšce, nebo výšce obsahu

justify-content, align-items, gap a obrácené směry operují na geometrii, která už byla změřena. Pohybují boxy; nikdy nevyvolávají opětovné měření obsahu položky. Právě toto oddělení brání tomu, aby složitý dashboard měřil své potomky vícekrát

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;

Návratová hodnota je řetězec pokračování, což je způsob, jakým každý vstupní bod pro kreslení HTML hlásí, co se nevešlo. Předejte jej dalšímu boxu nebo další stránce a tok pokračuje tam, kde se zastavil

Umístění v gridu a co může být trať

Tratě gridu přijímají pevné délky, procenta, jednotku fr, jednoduché výrazy repeat() a minmax(). Automatické umístění vyplňuje matici obsazenosti deterministicky, takže stejné HTML vždy vytvoří stejné rozvržení. Explicitní souřadnice se smí překrývat, a to záměrně: návrh, který vrství odznak přes kartu, vyjadřuje záměr, ne chybu. Když je explicitně zadaná jen jedna osa, umístění prohledává pouze tu druhou

Položky, které se rozpínají přes několik řádků, vracejí svou naměřenou výšku zpět do řádků, které pokrývají, zprůměrovanou mezi nimi, což brání tomu, aby vysoká rozpínající se položka stlačila jediný řádek, zatímco její sousedé zůstanou nízcí:

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);

Potomci flexu a gridu se vykreslují přes stejný vykreslovač HTML jako všechno ostatní, což je vlastnost, díky které je funkce použitelná, ne oddělený svět. Fonty, kaskáda CSS, odkazy, obrázky, tabulky a další vnořené kontejnery flex nebo grid se uvnitř flex položky chovají přesně tak, jako na nejvyšší úrovni, a vnější plán rozvržení zaznamenává finální příkazy textu a obdélníků, takže opakované kreslení znovu použije stávající mezipaměť měření

Proč jsou poznámky pod čarou problémem stránkování?

Poznámka pod čarou není obsah, který teče za odstavcem obsahujícím její odkaz; je to obsah, který se musí objevit ve spodní části stejného boxu jako její odkaz. To obrací obvyklé pořadí měření, protože prostor dostupný pro text těla teď závisí na obsahu, který ještě nebyl rozvržen

Vykreslovač proto změří poznámku ve chvíli, kdy narazí na odkaz, a odečte plochu poznámky z rozpočtu výšky těla aktuálního omezeného boxu. Pokud se odkaz, dosavadní text těla i poznámka společně nevejdou, značka poznámky pod čarou a vše po ní se přesune do řetězce pokračování společně. Právě toto pravidlo brání dvěma klasickým selháním: poznámce přetiskující text těla a poznámce uvízlé na stránce, jejíž odkaz je na předchozí

V omezeném boxu je plocha poznámky připnuta ke spodní straně s oddělovací linkou nad ní. V neomezeném měření, kde není žádná výška boxu, ke které by se dalo připnout, plocha poznámky následuje bezprostředně za tělem. Číslování se nese v rozšiřujícím poli na zásobníku pokračování, takže DrawHTMLTextBox a DrawHTMLStory udržují sekvenci běžící napříč sloupci a stránkami, a řetězec pokračování vytvořený předtím, než toto pole existovalo, se přesto správně obnoví

// Poznámky pod čarou uvnitř vícesloupcového příběhu udržují jednu běžící sekvenci
Html := LoadTemplate('chapter.html');    // používá značky float:footnote
Remainder := Lib.DrawHTMLStory(40, 40, 515, 700,
  2,        // sloupce
  16,       // mezera v bodech
  20,       // maximální počet stránek pro tento příběh
  Html);
if Remainder <> '' then
  Log('story exceeded its page budget');

Praktické rady pro autory šablon

Navrhujte v rámci zdokumentovaných stropů. Flex kontejner s více než 256 přímými potomky je téměř vždy datová tabulka v kostýmu flexu, a cesta pro tabulky ji stejně změří lépe. Grid větší než 64 krát 64 je tabulkový sešit a platí stejná rada. Pro vícesloupcový text těla řídí chování sloupců a dělení slov popsané v článku dělení slov a vyvážené textové sloupce to, jak tok vypadá uvnitř každého sloupce

Když se rozvržení musí vejít, měřte ještě před kreslením. GetHTMLTextHeight hlásí výšku, kterou by daná šířka potřebovala, což je levný způsob, jak se rozhodnout mezi jedním a druhým rozvržením ještě předtím, než vynaložíte inkoust. A s neprázdným řetězcem pokračování zacházejte jako s běžným stavem, ne výjimečným: je to mechanismus, kterým se dlouhý obsah stránkuje, ne signál chyby

Tam, kde HTML pochází z generátoru sestav místo z ručně psaných šablon, se s tím dobře doplňuje cesta řízená datovou sadou v článku generátor sestav nad datovou sadou, který vytváří kód, jenž flex a grid poté uspořádají. A když stejný obsah musí z PDF opět odejít, uzavírá zpětnou cestu sémantický export popsaný v článku export PDF do Markdown a DOCX

Rozvržení HTML, generování sestav a sémantický export jsou součástí jedné knihovny pro Delphi, C++Builder a Free Pascal; kompletní seznam funkcí je na stránce PDF Library for Delphi