Artykuł techniczny

Siatki rowspan i powtarzane nagłówki tabel w HotPDF

HotPDF renderuje tabele HTML przez swój profil HTML5 paged-media, używając prawdziwej siatki zajętości dla rowspan i colspan, mierzonych wysokości wierszy zamiast szacunków z liczby znaków oraz wierszy nagłówkowych powtarzanych na każdej stronie kontynuacji. Dwie sytuacje sprawiają, że odmawia powtórzenia nagłówka, i znajomość ich z góry jest tańsza niż debugowanie zduplikowanej komórki po fakcie

Klasa dokumentów, która to wymusza, to ta, którą każdy zespół raportowy prędzej czy później wyshipuje: faktura albo raport zgodności, gdzie źródłem prawdy jest HTML, tabela biegnie przez cztery strony, a nagłówek musi być czytelny na każdej z nich. Cokolwiek mniej niż prawdziwy layout tabeli produkuje dwie usterki, które czytelnicy wyłapują natychmiast: nagłówek pojawiający się raz na pierwszej stronie i wiersze o wysokościach zgadywanych z liczby znaków

Dlaczego możliwość tabel przeniosła się do renderera HTML?

Bo alternatywa gubi rich text, a rich text to powód, dla którego zawartość jest HTML-em na dobre. Oczywisty plan wygląda jak reuse: HotPDF ma już obiekt tabeli layoutowego DOM z porządną siatką, więc podłącz parser HTML do niego i dostaniesz spanning gratis. Problem w tym, czym ten obiekt tabeli rysuje. Jego komórki niosą tekst i styl, a ścieżka rysowania emituje goły tekst wyjściowy, więc wszystko, co HTML faktycznie zawierał poza fontem i kolorem — linki, indeksy górne, zmiany rozmiaru inline, kolor per run — przepada, zanim dotrze na stronę

Kierunek, który przeżywa kontakt z prawdziwymi dokumentami, to kierunek odwrotny. Przenieś możliwości silnika tabel — siatkę zajętości, prawdziwy pomiar, powtarzanie nagłówków i ważenie kolumn — do renderera HTML i zostaw renderowanie rich-text tam, gdzie już działa. To większa zmiana niż most i to ta zmiana, dzięki której hyperlink w komórce tabeli pozostaje hyperlinkiem

Rowspan bez union-find

Komórki rozpinające się tworzą atomowe grupy wierszy, ale domknięcie po tych grupach nie potrzebuje ogólnej struktury zbiorów rozłącznych, bo zajętość jest zawsze spójnym przedziałem. Komórka z rowspan="3" zaczynająca się w wierszu K zajmuje wiersze od K do K+2 i nic więcej, więc informacja o grupie redukuje się do znacznika końca per wiersz

Algorytm to dwie linie intencji. Gdy kładziesz komórkę rozpiętą od K do E, zapisz GroupEnd[K] := Max(GroupEnd[K], E). Potem przejdź wiersze raz od tyłu i zastosuj G[R] := G[G[R]], co propaguje każdy koniec wiersza do tyłu przez nachodzące na siebie rozpiętości i daje domknięcie przechodnie w jednym przebiegu. Otrzymujesz, dla każdego wiersza, ostatni wiersz, który musi zostać z nim na tej samej stronie, czyli dokładnie to, czego krok paginacji potrzebuje, by rozstrzygnąć, gdzie może paść złamanie

Rozdzielanie wysokości to druga połowa. Gdy rozpięta komórka potrzebuje więcej miejsca w pionie, niż dają obecnie wiersze, które pokrywa, nadwyżka idzie do ostatniego wiersza rozpiętości, a nie rozkłada się równo. Przetwarzaj komórki rozpięte po ustaleniu zwykłych wysokości wierszy, a potem dobij ostatni wiersz każdej rozpiętości. Równe rozłożenie nadwyżki brzmi sprawiedliwiej i daje widocznie zły wynik: wiersze zawierające tylko krótkie jednolinijkowe komórki puchną, bo jakaś niepowiązana komórka trzy wiersze wyżej akurat była wysoka

Siatka tabeli HTML w HotPDF, w której jedna komórka z rowspan 3 zaczynająca się w wierszu 2 zajmuje wiersze od 2 do 4 jako pojedynczy atomowy prostokąt, obok wartości końca grupy per wiersz G z R wyprodukowanych jednym przebiegiem od tyłu, pokazujących wiersze 2, 3 i 4 związane z tą samą stroną
Zajętość rozpiętości to zawsze spójny przedział, więc znaczniki końców per wiersz i jeden przebieg od tyłu zastępują union-find i mówią paginacji dokładnie, gdzie może paść złamanie
var
  Pdf: THotPDF;
  Importer: THPDFHTMLImporter;
  Stats: THPDFHTMLImportStatistics;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'audit-report.pdf';
    Pdf.BeginDoc;
    Importer := THPDFHTMLImporter.Create(Pdf);
    try
      Importer.Margin := 48;
      Importer.BaseFontName := 'Arial';
      Importer.BaseFontSize := 10;
      Importer.MaxDOMNodes := 200000;
      Importer.MaxLayoutOperations := 2000000;
      if Importer.RenderHTML5(SourceHtml, PrintStyleSheet) then
      begin
        Stats := Importer.Statistics;
        Writeln('tables ', Stats.TableCount,
                '  page breaks ', Stats.PageBreakCount);
      end;
    finally
      Importer.Free;
    end;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

RenderHTML5 przyjmuje opcjonalny arkusz stylów autora jako drugi argument i to tam należą reguły druku. Trzymaj ekranowy arkusz stylów z daleka. Profil jest wersjonowany, a HTML5ProfileMilestones raportuje, które grupy możliwości implementuje bieżący build: himParserCascade, himPagedLayout, himTablesForms i himBoundedResources, więc aplikacja może degradować się świadomie, zamiast odkrywać dziurę na produkcji

Pomiar musi zgadzać się z rysowaniem, dokładnie

Wysokość wiersza jest poprawna tylko wtedy, gdy kod mierzący zawijane linie zawija je według tej samej reguły, co kod je rysujący. Brzmi oczywście i jest najczęstszym pojedynczym źródłem tabel, których ramki nie schodzą się z zawartością. HotPDF mierzy zachłannym licznikiem linii i ten licznik musi zgadzać się z semantyką zawijania ścieżki wyjścia rich-text w trzech konkretnych punktach: łamie tylko na spacjach, nigdy nie rozcina słowa, a słowo szersze niż kolumna dostaje własną linię

Drugim wymaganiem jest font. Pomiar musi iść z fontem komórki, ustawionym przez SetFont z rzeczywistą nazwą, zestawem stylu i rozmiarem przed wywołaniem funkcji szerokości, a nie z czymkolwiek, co akurat było aktywne. Tekst bold jest rutynowo ponad dziesięć procent szerszy niż regularny w tym samym rozmiarze, co wystarcza, by zmienić komórkę trójlinijkową w czterolinijkową. Tabela, w której komórki nagłówka są bold, a komórki ciała nie, mierzona jednym fontem, będzie zła dokładnie w wierszach, na które czytelnicy patrzą pierwsi

Zrobienie tego dobrze zmienia to, co możesz asertować w teście. Obserwowalnym efektem dokładnego pomiaru jest odstęp między liniami, nie liczba glifów: jednolinijkowy wiersz ma około 20 punktów wysokości, podczas gdy szacunek z liczby znaków tej samej zawartości przewiduje dwie linie i jakieś 35. Asertuj pionową odległość między wierszami. I pamiętaj, że w przestrzeni użytkownika PDF-a Y rośnie w górę, więc nagłówek siedzący nad wierszem ciała oznacza, że wartość Y nagłówka jest większa — odwrotnie niż podpowiada instynkt współrzędnych ekranowych

Kiedy HotPDF odmawia powtórzenia nagłówka?

W dwóch przypadkach, z których oba dałyby widocznie zły wynik, gdyby poszła dalej. Pierwszy to blok nagłówka zawierający komórkę rozpiętą poza nagłówek, w wiersze ciała. Powtórzenie nagłówka narysowałoby zawartość tej komórki drugi raz w miejscu, do którego już nie należy, więc nagłówek jest rysowany raz, a tabela jedzie bez niego. Drugi to nagłówek wyższy niż 90 procent użytecznej wysokości strony, gdzie powtórka zostawiłaby prawie zero miejsca na dane i tabela nie robiłaby żadnego postępu

Przepływ decyzyjny HotPDF dla powtarzania nagłówków tabel HTML między stronami: nagłówek, którego rowspan przekracza w wiersze ciała, jest rysowany raz, nagłówek wyższy niż 90 procent użytecznej wysokości strony jest rysowany raz, a każdy inny nagłówek powtarza się na każdej stronie kontynuacji
Dwie odmowy są świadome: powtórzenie nagłówka, który włada rozpiętą komórką ciała albo wypełnia większość strony, narysowałoby zawartość tam, gdzie już nie należy, albo nie zostawiło miejsca na dane

Obie odmowy są świadome i ciche z założenia, bo alternatywa jest gorsza. Jeśli twój nagłówek się nie powtarza, a spodziewałeś się, że będzie, sprawdź w markupie rowspan przekraczający granicę thead, zanim podejrzasz silnik. Ten jeden wzorzec markupu odpowiada za większość niespodzianek

// Wagi kolumn pochodzą z markupu, więc arkusz stylów druku to miejsce,
// gdzie się nimi steruje. Szerokości są traktowane jako wagi, nie piksele
const
  PrintStyleSheet =
    'table { width: 100%; }' +
    'thead th { font-weight: bold; background: #eee; }' +
    'td.amount { text-align: right; }';

// Wiersz nagłówka niosący rowspan przekraczający w ciało wycisza
// powtarzanie nagłówka. Trzymaj rozpiętości w jednej sekcji:
//   <thead><tr><th rowspan="2">Item</th>...</tr></thead>  ok
//   <tr><th rowspan="3">Item</th>...  rozpiętość w tbody, bez powtórzenia

Szerokości kolumn zachowują się jak wagi, a nie absolutne pomiary, i to zachowanie trzyma tabelę użyteczną, gdy zawartość nie zgadza się z szacunkiem autora. Kolumna zadeklarowana na 30 procent dostaje w przybliżeniu 30 procent dostępnej szerokości, ale rozdział respektuje minimalną szerokość, której każda kolumna faktycznie potrzebuje, więc wąska kolumna trzymająca długi nierozbijalny token nie przelewa się po cichu poza box tabeli

Gdzie to się mieści w potoku dokumentów

Praca nad tabelami siedzi wewnątrz szerszego profilu paged-media, a reguły paginacji, budżety zasobów i obsługa CSS opisane w artykule ścieżka importu HTML5 paged-media działają bez zmian dla dokumentów zawierających tabele. Jeśli twoje dane nie startują jako HTML, droga bezpośredniej budowy z budowania tabel prosto do PDF-a omija warstwę parsowania w całości i daje to samo zachowanie siatki przez API. A ponieważ wysokość wiersza ostatecznie zależy od tego, gdzie łamią się linie, rozważania o pomiarze w artykule justowanie tekstu i łamanie linii to lektura towarzysząca dla każdego, kto kalibruje gęste wyjście tabelaryczne

Wynosząca się stąd lekcja w ogóle nie jest o tabelach. Gdy nowy podsystem potrzebuje możliwości, którą stary już ma, zapytaj, który z dwóch włada rzeczą najtrudniejszą do reimplementacji. Arytmetyka siatki to kilkadziesiąt linii i przeprowadza się łatwo. Renderowanie rich-text z linkami inline, indeksami górnymi i stylowaniem per run — już nie, więc siatka się przeprowadziła, a tekst został. HotPDF dostarcza obie ścieżki w ramach komponentu PDF HotPDF dla Delphi, więc wybór między wejściem HTML a bezpośrednią budową to decyzja projektowa, a nie biblioteczna