Artykuł techniczny

Flexbox, CSS Grid i przypisy dolne w PDF z Delphi

PDF Library for Delphi renderuje HTML na stronę PDF z prawdziwym dwuwymiarowym układem: display: flex i display: grid są mierzone i rozmieszczane, a nie degradowane do ułożonych w stos bloków, a przypisy dolne są rezerwowane u dołu ramki niosącej ich odwołanie, z numeracją, która pozostaje ciągła między kolumnami i stronami. Punkty wejścia są te znane: DrawHTMLTextBox dla pojedynczej ramki i DrawHTMLStory dla wielokolumnowego przepływu

Ma to znaczenie, ponieważ HTML to sposób, w jaki teraz przychodzi większość treści raportów. Szablony są tworzone przez osoby piszące CSS, dashboardy są projektowane jako karty, a renderer, który po cichu zwija wiersz flex w cztery ułożone w stos bloki, produkuje dokument w ogóle niepodobny do projektu. Zanim ta możliwość zaistniała, jedynym dwuwymiarowym kontenerem, który mierzył silnik, była tabela, więc każdy układ karty trzeba było ręcznie przepisać jako tabelę

Co się zmieniło w modelu układu?

Poprzednia główna pętla utrzymywała jedną ramkę wiersza i posuwała się w dół strony. Ten model doskonale obsługuje treść inline i ułożone w stos bloki, a nie potrafi wyrazić kontenera, którego elementy podrzędne są wymiarowane względem siebie nawzajem. Jedynym wyjątkiem były tabele, z własnym dwuprzebiegowym pomiarem

Zarówno flex, jak i grid dodają ograniczony przebieg pomiaru elementów podrzędnych kontenera, a ważnym słowem jest „ograniczony”. Kontener flex mierzy do 256 bezpośrednich elementów podrzędnych do tablicy o stałym rozmiarze. Grid używa macierzy zajętości co najwyżej 64 na 64 komórki do deterministycznego automatycznego rozmieszczania. Te pułapy istnieją po to, by wrogi albo wygenerowany arkusz stylów nie mógł wywołać nieograniczonej rekurencji ani kwadratowego zużycia pamięci rozmieszczania, co jest realnym zmartwieniem, gdy HTML pochodzi z szablonu edytowanego przez klienta

Jak elementy flex otrzymują swoje rozmiary

W kierunku wiersza kontener sumuje bazę każdego elementu wraz z jego wagami wzrostu i kurczenia, a następnie rozdziela pozostałą przestrzeń, dodatnią albo ujemną, zgodnie z tymi wagami. Przy flex-wrap każda linia jest rozwiązywana niezależnie, więc wiersz, który łamie się na dwie linie, przydziela wolną przestrzeń per linia, a nie w obrębie całego kontenera. W kierunku kolumny ten sam rozkład osi głównej działa wobec jawnej wysokości albo wysokości treści

justify-content, align-items, gap i kierunki odwrotne działają na geometrii, która już została zmierzona. Przesuwają ramki; nigdy nie wywołują ponownego pomiaru treści elementu. To rozdzielenie właśnie zapobiega temu, by złożony dashboard mierzył swoje elementy podrzędne wielokrotnie

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;

Wartość zwracana to ciąg kontynuacji, czyli sposób, w jaki każdy punkt wejścia rysowania HTML zgłasza, co się nie zmieściło. Przekaż go do kolejnej ramki albo kolejnej strony, a przepływ wznowi się tam, gdzie się zatrzymał

Rozmieszczanie w grid i czym może być ścieżka

Ścieżki grid akceptują stałe długości, procenty, jednostkę fr, proste wyrażenia repeat() i minmax(). Automatyczne rozmieszczanie wypełnia macierz zajętości deterministycznie, więc ten sam HTML zawsze daje ten sam układ. Jawne współrzędne mogą się nakładać, co jest celowe: projekt nakładający odznakę na kartę wyraża intencję, a nie błąd. Gdy jawnie podano tylko jedną oś, rozmieszczanie przeszukuje tylko drugą oś

Elementy rozciągnięte na kilka wierszy wnoszą swoją zmierzoną wysokość z powrotem do wierszy, które obejmują, uśrednioną między nimi, co zapobiega temu, by wysoki rozciągnięty element ściskał pojedynczy wiersz, zostawiając sąsiednie niskie:

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

Elementy podrzędne flex i grid są renderowane przez ten sam renderer HTML co wszystko inne, i to właśnie ta właściwość czyni tę funkcję użyteczną, a nie osobnym światem. Fonty, kaskada CSS, łącza, obrazy, tabele i kolejne zagnieżdżone kontenery flex albo grid zachowują się wewnątrz elementu flex dokładnie tak samo jak na najwyższym poziomie, a zewnętrzny plan układu zapisuje ostateczne polecenia tekstu i prostokątów, dzięki czemu powtórne rysowanie ponownie wykorzystuje istniejący bufor pomiarów

Dlaczego przypisy dolne są problemem paginacji?

Przypis dolny to nie treść, która przepływa po akapicie zawierającym jego odwołanie; to treść, która musi pojawić się u dołu tej samej ramki co jej odwołanie. To odwraca zwykłą kolejność pomiaru, ponieważ przestrzeń dostępna dla tekstu głównego zależy teraz od treści, która jeszcze nie została ułożona

Renderer mierzy więc przypis w chwili napotkania odwołania i odejmuje obszar przypisu od budżetu wysokości tekstu głównego bieżącej ograniczonej ramki. Jeśli odwołanie, dotychczasowy tekst główny i przypis nie mieszczą się razem, znacznik przypisu i wszystko po nim przenosi się razem do ciągu kontynuacji. Ta reguła zapobiega dwóm klasycznym awariom: przypisowi nadrukowującemu się na tekście głównym oraz przypisowi osadzonemu na stronie, której odwołanie znajduje się na poprzedniej

W ograniczonej ramce obszar przypisu jest przypięty do dołu z linią rozdzielającą nad nim. W pomiarze nieograniczonym, gdzie nie ma wysokości ramki, do której można by przypiąć, obszar przypisu następuje bezpośrednio po treści głównej. Numeracja jest niesiona w polu rozszerzenia na stosie kontynuacji, więc DrawHTMLTextBox i DrawHTMLStory utrzymują ciągłą sekwencję między kolumnami i stronami, a ciąg kontynuacji wygenerowany przed istnieniem tego pola nadal wznawia się poprawnie

// Przypisy dolne wewnątrz wielokolumnowej historii utrzymują jedną ciągłą sekwencję
Html := LoadTemplate('chapter.html');    // wykorzystuje znaczniki float:footnote
Remainder := Lib.DrawHTMLStory(40, 40, 515, 700,
  2,        // kolumny
  16,       // rynna w punktach
  20,       // maksymalna liczba stron dla tej historii
  Html);
if Remainder <> '' then
  Log('story exceeded its page budget');

Praktyczne wskazówki dla autorów szablonów

Projektuj w granicach udokumentowanych pułapów. Kontener flex z więcej niż 256 bezpośrednimi elementami podrzędnymi to niemal zawsze tabela danych przebrana za flex, a ścieżka tabeli i tak zmierzy ją lepiej. Grid większy niż 64 na 64 to arkusz kalkulacyjny, i ta sama rada się stosuje. Dla wielokolumnowego tekstu głównego zachowanie kolumn i dzielenia wyrazów opisane w dzieleniu wyrazów i wyważonych kolumnach tekstu rządzi tym, jak wygląda przepływ wewnątrz każdej kolumny

Mierz przed rysowaniem, gdy układ musi się zmieścić. GetHTMLTextHeight zgłasza wysokość, jakiej potrzebowałaby dana szerokość, co jest tanim sposobem na wybór między jednym układem a drugim, zanim zaangażuje się tusz. I traktuj niepusty ciąg kontynuacji jako coś normalnego, a nie wyjątkowego: to mechanizm, przez który dzieli się na strony długa treść, a nie sygnał błędu

Gdy HTML pochodzi z silnika raportów, a nie z ręcznie pisanych szablonów, ścieżka sterowana zbiorem danych w silniku raportów opartym na zbiorze danych dobrze się z tym komponuje, generując znaczniki, które flex i grid następnie rozmieszczają. A gdy ta sama treść musi też ponownie opuścić PDF, semantyczna ścieżka eksportu w eksporcie PDF do Markdown i DOCX zamyka pętlę

Układ HTML, generowanie raportów i eksport semantyczny są częścią jednej biblioteki dla Delphi, C++Builder i Free Pascal; pełna lista funkcji znajduje się na stronie PDF Library for Delphi