HotPDF Component łamie wielokolumnowy tekst CSS, gdy THPDFHTMLImporter.RenderHTML5 konwertuje HTML do PDF w Delphi. Silnik stronicowanego HTML rozwiązuje column-count i column-width względem pudełka treści, wyważa ostatni fragment każdego kontenera multicol wyszukiwaniem binarnym, wypełnia wcześniejsze strony kolumna po kolumnie do dołu strony i wspiera column-fill, column-span: all oraz column-rule
Zwykły powód, żeby tego chcieć, jest nudny i prawdziwy: dwukolumnowy biuletyn produktowy, załącznik regulaminu złożony w trzech wąskich kolumnach albo PDF not wydania, który ma wyglądać jak drukowany newsletter, który dział marketingu już zaprojektował w HTML. Bez wsparcia kolumn importer stackuje każdy akapit w jednej szerokiej miarze, długość wiersza się podwaja, a dokument czyta się jak plik loga. Z nimi ten sam arkusz stylów, który napędza podgląd w przeglądarce, napędza PDF, a trudną częścią staje się paginacja, o której przeglądarki na ekranie prawie nigdy nie muszą myśleć
Jak HotPDF decyduje, ile kolumn użyć?
HotPDF rozwiązuje liczbę kolumn tymi samymi regułami co pseudoalgorytm z CSS Multi-column Layout Module Level 1 §3.4, nałożonymi na szerokość pudełka treści W po usunięciu paddingu i ramek. Zadeklarowane column-count jest brane jak leci. Gdy podano tylko column-width, silnik liczy N = Trunc((W + gap) / (column-width + gap)) z podłogą jednej kolumny. Gdy są oba, wynikiem jest mniejsze z nich, więc zadeklarowana liczba może uciąć wyliczoną, ale nigdy jej nie nadmuchać
Konkretny przypadek: pudełko treści 483pt (A4 z marginesami bocznymi 56pt, po zaokrągleniu) i column-width: 140pt; column-gap: 12pt daje Trunc(495 / 152) = 3 kolumny, każda o szerokości (483 - 2 * 12) / 3 = 153pt. Dodanie column-count: 2 do tej samej reguły zbij ją do dwóch kolumn, bo wygrywa minimum. Słowo kluczowe odstępu normal rozwiązuje się do 1em rozmiaru fontu kontenera i to rozwiązanie dzieje się w czasie łamania, a nie parsowania, więc font-size zadeklarowany po column-gap w tej samej regule wciąż daje właściwy odstęp
Jedna straż nie jest w specyfikacji i jest tam celowo. Po policzeniu szerokości kolumny każda wartość poniżej 12pt zapada kontener z powrotem do jednej kolumny. Arkusz stylów mówiący column-count: 9999, czy to z przypadku, czy dlatego, że przyszedł z niezaufanego szablonu, wyprodukowałby inaczej tysiące kolumn o ujemnych albo mikroskopijnych szerokościach i łamacz wierszy próbujący wcisnąć jeden glif na wiersz. Cofnięcie do jednej kolumny trzyma wyjście czytelnym i koszt łamania ograniczonym. Zero i liczby ujemne nigdy nie dochodzą tak daleko: walidator przyjmuje dla column-count tylko auto albo dodatnią liczbę całkowitą, a column-width i column-gap biorą odpowiednio auto albo normal, albo długość w punktach albo em
Renderowanie wielokolumnowej sekcji HTML do PDF w Delphi
Wyjście wielokolumnowe nie potrzebuje żadnego przełącznika w importerze. THPDFHTMLImporter jest tworzony z dokumentem THotPDF, do którego rysuje, BeginDoc musi już mieć bieg, a własności kolumn przychodzą zwykłą kaskadą, w markupie albo w arkuszu autora przekazanym jako drugi argument RenderHTML5
uses
SysUtils, HPDFDoc, HPDFHTMLImport;
procedure BuildBulletin(const FileName: string);
var
Pdf: THotPDF;
Importer: THPDFHTMLImporter;
Html, Css: WideString;
begin
Pdf := THotPDF.Create(nil);
Importer := THPDFHTMLImporter.Create(Pdf);
try
Pdf.FileName := FileName;
Pdf.BeginDoc;
Importer.Margin := 56;
Importer.BaseFontName := 'Arial';
Importer.BaseFontSize := 10;
Css := 'section.news { column-width: 140pt; column-gap: 12pt; ' +
' column-rule: 0.5pt solid #999999 } ' +
'section.news h2 { column-span: all }';
Html := '<section class="news">' +
'<p>Release 4.2 ships the new export pipeline...</p>' +
'<p>Licensing changes take effect on 1 March...</p>' +
'<h2>Known issues</h2>' +
'<p>Import of legacy archives & templates...</p>' +
'</section>';
if not Importer.RenderHTML5(Html, Css) then
raise Exception.Create('Nothing was emitted');
Pdf.EndDoc;
finally
Importer.Free;
Pdf.Free;
end;
end;
Zauważ podwójne escapowanie w ostatnim akapicie: & wewnątrz napisu Pascala to encja HTML dla ampersanda, którą importer dekoduje z powrotem do pojedynczego znaku. Reszta kaskady, budżety i callbacki zasobów zachowują się dokładnie jak opisano w przeglądzie importu HTML5 paged-media w HotPDF; kolumny to tylko jeden więcej tryb łamania wewnątrz tego samego profilu
Wyważanie kolumn wyszukiwaniem binarnym
HotPDF wyważa kolumny, szukając najmniejszej wysokości kolumny, przy której zachłanne wypełnienie wciąż umieszcza każdy pozostały wiersz. Przebieg zachłanny jest prosty: idź wierszami do kolumny pierwszej, aż następny wiersz przekroczyłby kandydata wysokości, potem kontynuuj w kolumnie drugiej i tak dalej. Wyszukiwanie startuje z najwyższego pojedynczego wiersza jako dolną granicą i sumą wszystkich wysokości wierszy jako górną granicą, połowuje przedział w każdej rundzie i stopuje, gdy granice zmieszczą się w 0,05pt
Wyszukiwanie binarne działa tylko, gdy predykat jest monotoniczny, a tutaj jest: wyższa kolumna nigdy nie pomieści mniej wierszy niż niższa, więc "każdy wiersz się mieści" przeskakuje z fałszu na prawdę dokładnie raz, gdy wysokość rośnie. Ta własność gwarantuje zbieżność i gwarantuje, że końcowe zachłanne wypełnienie na znalezionej wysokości konsumuje każdy wiersz, zamiast zostawić sierotę, która odpaliłaby fałszywą dodatkową stronę. Wyjaśnia też detal wyniku: wyważony pas jest tak krótki, na ile pozwalają pudełka wierszy, niekoniecznie równe kolumny, bo wiersze są niepodzielne i ostatnia kolumna może się kończyć wiersz-dwa wyżej niż pozostałe
Co się dzieje, gdy kolumny przecinają złamanie strony?
Kolumny, które nie mieszczą się na bieżącej stronie, są wypełniane sekwencyjnie do dołu strony, i tylko końcowy fragment strony jest wyważany. To zachowanie, które CSS Multi-column Layout Level 1 §7.1 przepisuje dla column-fill: balance w kontekstach fragmentowanych, i tak wygląda drukowany newsletter: pełne kolumny na każdej stronie poza ostatnią, gdzie pozostały tekst jest dzielony równo, zamiast zostawić jedną wysoką kolumnę obok pustej
Każdy fragment strony przechodzi przez ten sam fragmentator niezależnie. Jeśli pozostałe wiersze mieszczą się w N kolumnach dostępnej wysokości, fragment jest wyważany; w przeciwnym razie każda kolumna wypełnia się do dołu strony i pętla zaczyna nową stronę. Naturalna wysokość całego kontenera, której silnik używa do decyzji break-inside: avoid i mierzenia rozmiaru, to wysokość wyważonego pasa z nieograniczoną pojemnością, więc kontener, który zmieściłby się na świeżej stronie jako wyważony blok, jest przenoszony tam w całości, zamiast być krajony. Gdy pojedynczy wiersz jest wyższy niż cała strona, silnik rozluźnia ograniczenie, umieszcza wiersz mimo wszystko na górze strony i zgłasza diagnostykę hdkPaginationConstraint z podmiotem multi-column
column-fill: auto i pudełka o ustalonej wysokości
Z column-fill: auto i określonym height HotPDF wypełnia kolumny po kolei, a wysokość każdej kolumny to mniejsze z pozostałego miejsca na stronie i wysokości kontenera minus jego padding i ramki. Minimum znaczy więcej, niż wygląda. Użycie tylko pozostałego miejsca na stronie pozwoliłoby pierwszej kolumnie połknąć każdy wiersz, ilekroć strona ma więcej miejsca niż pudełko. Bez określonej wysokości column-fill: auto cofa się do wyważania, jak przeglądarki, bo nieograniczony kontener multicol nie ma wysokości kolumny, do której można wypełniać; pierwsza implementacja przegapiła ten fallback i jeden z 27 testów regresji go złapał
column-span: all i zagnieżdżone akapity
HotPDF wspiera column-span: all na bezpośrednich dzieciach kontenera wielokolumnowego, za CSS Multi-column Layout Level 1 §6.1. Dziecko rozpinające dzieli kontener na segmenty: dzieci przed nim tworzą jeden pas kolumn, element rozpinający renderuje się na całą szerokość, a dzieci po nim zaczynają nowy pas. Każdy pas jest wyważany, wypełniany, kreslony i paginowany na własną rękę, więc nagłówek w środku sekcji o trzech kolumnach produkuje dwa wyważone pasy z nagłówkiem pełnej szerokości pomiędzy, czyli układ, jaki ludzie faktycznie rysują w mokapach
Droga tam wymagała najpierw naprawienia mniej widocznego problemu. Kontener jak <div style="column-count:2"> z kilkorgiem dzieci <p> renderował kiedyś każdy akapit w jednej ułożonej kolumnie, bo każdy akapit był łamany jako własny blok tekstu, a deklaracja kolumn na rodzicu nigdy nie była nakładana. HotPDF zbiera teraz tekst zagnieżdżonych dzieci blokowych w jeden przepływ dla kontenera. Kolektor musi przy tym wstawić twardy koniec wiersza na każdej granicy bloku; pominięcie tego skleja ostatnie słowo jednego akapitu z pierwszym słowem następnego w tym samym wierszu, błąd łatwy do przegapienia, gdy tekst testowy akurat kończy się interpunkcją
Ten scalony przepływ ma celowy zakres. Jest używany tylko, gdy całe poddrzewo to zwykły przepływ tekstu. Kontener, który trzyma kontrolki formularzy, obrazy, tabele, listy albo <hr> gdziekolwiek w środku, trzyma zwykłą drogę blokową i renderuje w jednej kolumnie, bo scalony kolektor tekstu nie umie wyrazić ich geometrii. Tabele szczególnie mają własne reguły paginacji, opisane w artykule o obsłudze rowspan i powtarzanych nagłówkach tabel w HotPDF, i wmieszanie ich w pas kolumn złamałoby te gwarancje
Dokąd idzie linia kolumn i jak ją testować
HotPDF rysuje column-rule na osi każdego odstępu, przy prawej krawędzi kolumny plus połowa odstępu, od góry do dołu pasa na każdym fragmencie strony, jak opisuje CSS Multi-column Layout Level 1 §4. Szerokość domyślnie to 1pt. Kolor domyślnie to kolor tekstu elementu, gdy nie jest zadeklarowany albo jest currentColor. Skrót column-rule bez słowa kluczowego stylu jest traktowany jako solid, co odpowiada temu, jak zwykle się czyta skrót border, a none i hidden wyciszają linię
Geometria kolumn jest obserwowalna bez parsowania PDF. Zdarzenie OnLineLayout na THPDFHTMLImporter odpala się raz na narysowany wiersz z rekordem THPDFHTMLLineLayout, a ponieważ silnik rysuje każdą kolumnę osobno, pole X każdego wiersza to lewa krawędź jego kolumny. Sąsiednie kolumny różnią się dokładnie o szerokość kolumny plus odstęp, co ułatwia odróżnienie odstępu 12pt od 20pt w teście regresji
type
TColumnProbe = class
public
Lines: TStringList;
constructor Create;
destructor Destroy; override;
procedure LineDrawn(Sender: TObject; const Line: THPDFHTMLLineLayout);
end;
constructor TColumnProbe.Create;
begin
inherited Create;
Lines := TStringList.Create;
end;
destructor TColumnProbe.Destroy;
begin
Lines.Free;
inherited;
end;
procedure TColumnProbe.LineDrawn(Sender: TObject;
const Line: THPDFHTMLLineLayout);
begin
// PageIndex jest liczony od zera; X to lewa krawędź kolumny linii
Lines.Add(Format('page=%d x=%.1f y=%.1f %s',
[Line.PageIndex, Line.X, Line.Y, string(Line.Text)]));
end;
// Podpięcie, przed RenderHTML5:
// Probe := TColumnProbe.Create;
// Importer.OnLineLayout := Probe.LineDrawn;
Dwie praktyczne uwagi z pisania testów pod to zdarzenie. PageIndex jest liczony od zera, co jest pierwszą rzeczą, którą zwykle psuje ręcznie pisana asercja. A X zawiera przesunięcia wyrównania, więc dla tekstu centrowanego albo wyrównanego do prawej porównuj pozycje kolumn na fixturach wyrównanych do lewej. Sama linia nie pojawia się w zdarzeniu; żeby aserjonować jej pozycję, przeskanuj nieskompresowany strumień treści za pionowymi operatorami m i l
Limity i pułapki warte znajomości
Nieliczne zachowania są przybliżeniami i lepiej je znać przed przeglądem łamania niż po. Odstępy akapitów wewnątrz scalonego przepływu kolumn są przybliżane jednym wierszem wysokości wiersza kontenera na każdej granicy bloku, więc marginesy per akapit wewnątrz pasa kolumn nie są odtwarzane dokładnie. Gdy column-span: all dzieli kontener, każdy segment powtarza padding kontenera i pudełko tła, co trzyma segmenty wizualnie ciągłymi, ale nie jest jednym pudełkiem w ścisłym sensie CSS. Każda wartość column-rule-style poza none i hidden przechodzi walidację, ale linia jest kreślona jako solidna
Niewspierane wartości nie zawodzą po cichu. Ustawienie StrictCSS na True zamienia każdą diagnostykę w wyjątek EHPDFHTMLUnsupportedCSS, a OnDiagnostic pozwala zadaniu wsadowemu je logować i iść dalej. Uwaga: tryb ścisły dotyczy też diagnostyki paginacji, więc dokument z wierszem wyższym niż strona podniesie wyjątek pod StrictCSS, zamiast cicho rozluźnić ograniczenie
procedure TReportJob.HtmlDiagnostic(Sender: TObject;
const Diagnostic: THPDFHTMLDiagnostic);
begin
case Diagnostic.Kind of
hdkUnsupportedProperty, hdkUnsupportedValue:
FLog.Add('CSS ' + Diagnostic.Subject + ': ' + Diagnostic.MessageText);
hdkPaginationConstraint:
FLog.Add('Layout relaxed (' + Diagnostic.Subject + '): ' +
Diagnostic.MessageText);
else
FLog.Add(Diagnostic.MessageText);
end;
end;
// Autoring szablonów: zawodź szybko na wszystko, co silnik zignoruje
Importer.StrictCSS := FValidatingTemplate;
Importer.OnDiagnostic := HtmlDiagnostic;
Tryb ścisły to właściwe ustawienie, dopóki szablon jest pisany, bo literówka jak column-span: both jest odrzucana natychmiast, zamiast cicho puścić nagłówek w przepływ wewnątrz kolumn. W produkcji logowanie przez OnDiagnostic to zwykle lepszy układ, bo jedna dziwna deklaracja w HTMLu od klienta nie powinna kosztować całego dokumentu. Identyczne diagnostyki (ten sam rodzaj, podmiot i wartość) są zgłaszane raz, dopóki mieszczą się na liście MaxDiagnostics, która domyślnie ma 256 wpisów, więc deklaracja, którą kaskada odwiedza dla setek elementów, nie zalewa loga
FAQ
Czy HotPDF wspiera skrót columns?
Tak. Skrót columns przyjmuje co najwyżej jedną dodatnią liczbę całkowitą i jedną długość kolumny w dowolnej kolejności, a auto resetuje obie własności długie. Rozwiązane wartości idą za tymi samymi regułami liczby i szerokości co pojedyncze własności
Czy kontener wielokolumnowy pomieści obrazy albo tabele?
Pomieści, ale HotPDF renderuje wtedy ten kontener w jednej kolumnie. Przepływ kolumnowy jest nakładany tylko, gdy każdy potomek to zwykły przepływ tekstu, bo obrazy, tabele, listy i kontrolki formularzy potrzebują własnej geometrii pudełka i reguł paginacji
Dlaczego ostatnia strona pokazuje wyważone kolumny, a wcześniejsze są pełne?
To reguła CSS dla column-fill: balance w mediach stronicowanych: wyważany jest tylko końcowy fragment. Wcześniejsze strony wypełniają każdą kolumnę do dołu strony, więc nie traci się miejsca w środku dokumentu
Wielokolumnowe łamanie, profil HTML5 paged-media i reszta potoku HTML-to-PDF są częścią HotPDF Component for Delphi and C++Builder, która pokrywa też bezpośrednie rysowanie PDF, formularze, szyfrowanie i ekstrakcję tekstu