Techninis straipsnis

Flexbox, CSS Grid ir išnašos PDF faile iš Delphi

PDF Library for Delphi atvaizduoja HTML į PDF puslapį su tikru dvimačiu maketu: display: flex ir display: grid yra matuojami ir talpinami, o ne sumenkinami iki suskrautų blokų, o išnašos rezervuojamos dėžutės, nešančios jų nuorodą, apačioje, numeracijai išliekant nepertraukiamai per stulpelius ir puslapius. Įėjimo taškai yra pažįstami – DrawHTMLTextBox vienai dėžutei ir DrawHTMLStory daugiastulpiam srautui

Tai svarbu todėl, kad dabar dauguma ataskaitų turinio atkeliauja kaip HTML. Šablonus kuria žmonės, rašantys CSS, prietaisų skydeliai suprojektuoti kaip kortelės, o atvaizduotojas, kuris tyliai suploja flex eilutę į keturis suskrautus blokus, sukuria dokumentą, visiškai nepanašų į dizainą. Kol šios galimybės nebuvo, vienintelis dvimatis konteineris, kurį variklis matavo, buvo lentelė, todėl kiekvieną kortelės maketą reikėdavo rankiniu būdu perkurti kaip lentelę

Kas pasikeitė maketo modelyje?

Ankstesnis pagrindinis ciklas palaikė vieną eilutės dėžutę ir stūmėsi žemyn per puslapį. Šis modelis puikiai apdoroja įterptinį turinį ir suskrautus blokus, tačiau negali išreikšti konteinerio, kurio vaikiniai elementai matuojami vieni kitų atžvilgiu. Lentelės buvo vienintelė išimtis, su savo pačių dviejų perėjimų matavimu

Flex ir grid kiekvienas prideda apribotą matavimo perėjimą per konteinerio vaikinius elementus, ir svarbiausias žodis yra „apribotą“. Flex konteineris matuoja iki 256 tiesioginių vaikinių elementų fiksuotame masyve. Grid naudoja daugiausiai 64 iš 64 langelių užimtumo matricą deterministiniam automatiniam patalpinimui. Šios lubos egzistuoja tam, kad priešiškas ar sugeneruotas stiliaus lapas negalėtų sukelti neribotos rekursijos ar kvadratinės patalpinimo atminties, o tai reali problema, kai HTML atkeliauja iš šablono, kurį redaguoja klientas

Kaip flex elementai gauna savo dydžius

Eilutės kryptimi konteineris susumuoja kiekvieno elemento bazę kartu su jo augimo ir traukimosi svoriais, tada paskirsto likusią erdvę, teigiamą ar neigiamą, pagal tuos svorius. Su flex-wrap, kiekviena eilutė sprendžiama nepriklausomai, todėl eilutė, kuri lūžta į dvi eilutes, laisvą erdvę skiria kiekvienai eilutei atskirai, o ne visam konteineriui. Stulpelio kryptimi ta pati pagrindinės ašies paskirstymo logika taikoma arba aiškiam aukščiui, arba turinio aukščiui

justify-content, align-items, gap ir atvirkštinės kryptys veikia su jau pamatuota geometrija. Jos perkelia dėžutes; jos niekada nesukelia pakartotinio elemento turinio matavimo. Būtent šis atskyrimas apsaugo, kad sudėtingas prietaisų skydelis nebūtų matuojamas kelis kartus

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;

Grąžinama reikšmė yra tęsinio eilutė – taip kiekvienas HTML piešimo įėjimo taškas praneša, kas netilpo. Perduokite ją kitai dėžutei ar kitam puslapiui, ir srautas tęsiasi nuo tos vietos, kur sustojo

Grid patalpinimas ir kuo gali būti takelis

Grid takeliai priima fiksuotus ilgius, procentus, fr vienetą, paprastas repeat() išraiškas ir minmax(). Automatinis patalpinimas deterministiškai užpildo užimtumo matricą, todėl tas pats HTML visada duoda tą patį išdėstymą. Aiškioms koordinatėms leidžiama persidengti, ir tai sąmoninga: dizainas, uždedantis ženkliuką ant kortelės sluoksniu, išreiškia ketinimą, o ne klaidą. Kai aiškiai nurodyta tik viena ašis, patalpinimo paieška vyksta tik kitoje ašyje

Elementai, apimantys kelias eilutes, savo pamatuotą aukštį grąžina eilutėms, kurias apima, jį vidurkindami tarp jų – tai apsaugo, kad aukštas apimantis elementas nesuspaustų vienos eilutės, palikdamas kaimynines žemas:

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 ir grid vaikiniai elementai atvaizduojami per tą patį HTML atvaizduotoją kaip ir viskas kita – būtent ši savybė daro funkciją naudojamą, o ne atskirą pasaulį. Šriftai, CSS kaskada, nuorodos, vaizdai, lentelės ir toliau įdėti flex ar grid konteineriai flex elemento viduje elgiasi lygiai taip pat, kaip ir aukščiausiame lygyje, o išorinis maketo planas įrašo galutines teksto ir stačiakampio komandas, todėl pakartotinis piešimas naudoja jau esamą matavimo talpyklą

Kodėl išnašos yra puslapiavimo problema?

Išnaša nėra turinys, sekantis po pastraipos, turinčios jos nuorodą; tai turinys, kuris privalo atsirasti tos pačios dėžutės, kurioje yra jos nuoroda, apačioje. Tai apverčia įprastą matavimo tvarką, nes vietos kiekis, prieinamas pagrindiniam tekstui, dabar priklauso nuo turinio, kuris dar nebuvo sumaketuotas

Todėl atvaizduotojas pamatuoja išnašą, kai sutinka nuorodą, ir atima išnašos plotą iš dabartinės ribotos dėžutės turinio aukščio biudžeto. Jei nuoroda, iki šiol buvęs pagrindinis tekstas ir išnaša visi kartu netelpa, išnašos žymuo ir viskas po jo kartu perkeliama į tęsinio eilutę. Būtent ši taisyklė apsaugo nuo dviejų klasikinių gedimų: išnašos, užspausdinančios pagrindinį tekstą, ir išnašos, palikta puslapyje, kurio nuoroda yra ankstesniame

Ribotoje dėžutėje išnašos plotas prisegtas prie apačios su skiriamąja linija virš jo. Neribotame matavime, kur nėra dėžutės aukščio, prie kurio prisegti, išnašos plotas seka tiesiai po pagrindinio turinio. Numeracija nešama tęsinio dėklo išplėtimo lauke, todėl DrawHTMLTextBox ir DrawHTMLStory palaiko seką bėgant per stulpelius ir puslapius, o tęsinio eilutė, sukurta prieš atsirandant šiam laukui, vis tiek teisingai tęsiasi

// Išnašos daugiastulpio srauto viduje palaiko vieną bėgančią seką
Html := LoadTemplate('chapter.html');    // naudoja float:footnote žymenis
Remainder := Lib.DrawHTMLStory(40, 40, 515, 700,
  2,        // stulpeliai
  16,       // tarpas taškais
  20,       // maksimalus puslapių skaičius šiam srautui
  Html);
if Remainder <> '' then
  Log('story exceeded its page budget');

Praktiniai patarimai šablonų kūrėjams

Projektuokite pagal dokumentuotas lubas. Flex konteineris su daugiau nei 256 tiesioginiais vaikiniais elementais beveik visada yra duomenų lentelė, apsirengusi flex kostiumu, o lentelės kelias vis tiek ją matuoja geriau. Grid, didesnis nei 64 iš 64, yra skaičiuoklė, ir tas pats patarimas taikomas ir jai. Daugiastulpio pagrindinio teksto atveju, stulpelio ir žodžių skaidymo elgsena, aprašyta straipsnyje žodžių skaidymas ir subalansuoti teksto stulpeliai, valdo, kaip srautas atrodo kiekvieno stulpelio viduje

Matuokite prieš piešdami, kai maketas turi tilpti. GetHTMLTextHeight praneša aukštį, kurio prireiktų tam tikram pločiui – tai pigus būdas apsispręsti tarp vieno maketo ir kito, prieš pradedant piešti. Ir traktuokite netuščią tęsinio eilutę kaip normalią, o ne kaip išimtinę: tai mechanizmas, kuriuo ilgas turinys puslapiuojamas, o ne klaidos signalas

Kai HTML atkeliauja iš ataskaitų variklio, o ne iš rankiniu būdu rašytų šablonų, duomenų rinkiniu grįstas kelias straipsnyje duomenų rinkinio ataskaitų variklis gerai dera su šia funkcija, generuodamas žymėjimą, kurį tada sutvarko flex ir grid. O kai tą patį turinį reikia vėl išvesti iš PDF, semantinio eksporto kelias straipsnyje PDF eksportavimas į Markdown ir DOCX uždaro grįžtamąjį ciklą

HTML maketavimas, ataskaitų generavimas ir semantinis eksportas yra vienos bibliotekos dalys Delphi, C++Builder ir Free Pascal aplinkoms; pilnas funkcijų sąrašas pateikiamas PDF Library for Delphi puslapyje