Tehnički članak

Flexbox, CSS Grid i fusnote u PDF-u iz Delphija

PDF Library for Delphi renderuje HTML u PDF stranicu sa pravim dvodimenzionalnim rasporedom: display: flex i display: grid se mere i postavljaju umesto da se degradiraju u naslagane blokove, a fusnote se rezervišu na dnu okvira koji nosi njihovu referencu, sa numerisanjem koje ostaje kontinuirano preko kolona i stranica. Ulazne tačke su poznate, DrawHTMLTextBox za jedan okvir i DrawHTMLStory za višekolonski tok

Ovo je bitno jer je HTML način na koji većina sadržaja izveštaja sada stiže. Šablone pišu ljudi koji pišu CSS, kontrolne table se dizajniraju kao kartice, a renderer koji tiho sažme flex red u četiri naslagana bloka proizvodi dokument koji uopšte ne liči na dizajn. Do postojanja ove mogućnosti, jedini dvodimenzionalni kontejner koji je pogon merio bila je tabela, pa je svaki raspored kartica morao biti ručno ponovo napisan kao tabela

Šta se promenilo u modelu rasporeda?

Prethodna glavna petlja je održavala jedan okvir reda i napredovala niz stranicu. Taj model savršeno obrađuje inline sadržaj i naslagane blokove i ne može izraziti kontejner čija se deca dimenzionišu u odnosu jedno na drugo. Tabele su bile jedini izuzetak, sa sopstvenim dvoprolaznim merenjem

Flex i grid svaki dodaju omeđen prolaz merenja preko dece kontejnera, i bitna reč je omeđen. Flex kontejner meri do 256 direktne dece u fiksan niz. Grid koristi matricu zauzetosti od najviše 64 sa 64 ćelije za deterministički automatski raspored. Ti plafoni postoje tako da neprijateljski ili tek generisan stilski list ne može pokrenuti neomeđenu rekurziju ili kvadratnu memoriju rasporeda, što je stvarna briga kada HTML dolazi iz šablona koji kupac uređuje

Kako flex stavke dobijaju svoje veličine

U smeru reda, kontejner sabira osnovu svake stavke zajedno sa njenim težinama rasta i skupljanja, zatim raspoređuje preostali prostor, pozitivan ili negativan, prema tim težinama. Sa flex-wrap, svaka linija se rešava nezavisno, tako da red koji se prelomi u dve linije dodeljuje slobodan prostor po liniji, a ne preko celog kontejnera. U smeru kolone ista raspodela glavne ose radi protiv eksplicitne visine ili visine sadržaja

justify-content, align-items, gap i obrnuti smerovi rade nad geometrijom koja je već izmerena. Pomeraju okvire; nikada ne pokreću ponovno merenje sadržaja stavke. Ta odvojenost je ono što sprečava složenu kontrolnu tablu da meri svoju decu nekoliko puta

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;

Povratna vrednost je string nastavka, kojim svaka ulazna tačka za crtanje HTML-a prijavljuje šta nije stalo. Prosledite ga sledećem okviru ili sledećoj stranici i tok se nastavlja tamo gde je stao

Raspored grida, i šta traka može biti

Trake grida prihvataju fiksne dužine, procente, jedinicu fr, jednostavne izraze repeat() i minmax(). Automatski raspored popunjava matricu zauzetosti deterministički, tako da isti HTML uvek proizvodi isti raspored. Eksplicitne koordinate smeju da se preklapaju, što je namerno: dizajn koji sloji značku preko kartice izražava nameru, ne grešku. Kada je data samo jedna osa eksplicitno, pretraga rasporeda se vrši samo po drugoj osi

Stavke koje pokrivaju nekoliko redova doprinose svoju izmerenu visinu nazad redovima koje pokrivaju, usrednjenu preko njih, što sprečava da visoka stavka koja se proteže stisne jedan red dok ostavlja njene susede niskim:

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

Deca flex-a i grida se renderuju kroz isti HTML renderer kao sve ostalo, što je svojstvo koje čini funkciju upotrebljivom, a ne posebnim svetom. Fontovi, CSS kaskada, veze, slike, tabele i dalji ugnježdeni flex ili grid kontejneri se svi ponašaju unutar flex stavke tačno kao na najvišem nivou, a spoljašnji plan rasporeda beleži konačne komande teksta i pravougaonika tako da ponovljeno crtanje ponovo koristi postojeći keš merenja

Zašto su fusnote problem paginacije?

Fusnota nije sadržaj koji teče nakon pasusa koji sadrži njenu referencu; to je sadržaj koji mora da se pojavi na dnu istog okvira kao njena referenca. To obrće uobičajen redosled merenja, jer prostor dostupan za tekst tela sada zavisi od sadržaja koji još nije raspoređen

Renderer stoga meri belešku kada naiđe na referencu, i oduzima površinu beleške od budžeta visine tela trenutnog omeđenog okvira. Ako referenca, tekst tela dosad i beleška ne mogu svi stati, marker fusnote i sve nakon njega prelaze u string nastavka zajedno. To pravilo je ono što sprečava dva klasična otkaza: belešku koja preklapa tekst tela, i belešku nasukanu na stranici čija je referenca na prethodnoj

U omeđenom okviru površina beleške je pripijena za dno sa razdvojnom linijom iznad nje. U neomeđenom merenju, gde nema visine okvira za koju se pripiti, površina beleške sledi neposredno nakon tela. Numerisanje se nosi u polju proširenja na steku nastavka, tako da DrawHTMLTextBox i DrawHTMLStory drže sekvencu kako teče preko kolona i stranica, a string nastavka proizveden pre nego što je to polje postojalo i dalje se ispravno nastavlja

// Fusnote unutar višekolonske priče drže jednu tekuću sekvencu
Html := LoadTemplate('chapter.html');    // koristi float:footnote markere
Remainder := Lib.DrawHTMLStory(40, 40, 515, 700,
  2,        // kolone
  16,       // razmak u tačkama
  20,       // maksimalan broj stranica za ovu priču
  Html);
if Remainder <> '' then
  Log('story exceeded its page budget');

Praktične smernice za autore šablona

Dizajnirajte unutar dokumentovanih plafona. Flex kontejner sa više od 256 direktne dece je skoro uvek tabela podataka prerušena u flex kostim, a putanja tabele je meri bolje ionako. Grid veći od 64 sa 64 je tabela, i isti savet važi. Za višekolonski tekst tela, ponašanje kolona i deljenja reči opisano u deljenju reči i uravnoteženim tekstualnim kolonama upravlja time kako tok izgleda unutar svake kolone

Merite pre nego što crtate kada raspored mora da stane. GetHTMLTextHeight prijavljuje visinu koja bi data širina trebala, što je jeftin način da se odluči između jednog rasporeda i drugog pre nego što se posveti mastilu. I tretirajte neprazan string nastavka kao normalan, a ne izuzetan: to je mehanizam kojim se dug sadržaj paginira, ne signal greške

Gde HTML dolazi iz mašine za izveštaje, a ne iz ručno pisanih šablona, put vođen skupom podataka u pogonu izveštaja skupa podataka se dobro uklapa sa ovim, generišući markup koji flex i grid onda raspoređuju. A kada isti sadržaj takođe treba da napusti PDF ponovo, semantička putanja izvoza u izvozu PDF-a u Markdown i DOCX zatvara povratni put

Raspored HTML-a, generisanje izveštaja i semantički izvoz su deo jedne biblioteke za Delphi, C++Builder i Free Pascal; kompletna lista funkcija je na stranici PDF Library for Delphi