HotPDF Component met en page du texte CSS multicolonne quand THPDFHTMLImporter.RenderHTML5 convertit du HTML en PDF en Delphi. Le moteur HTML paginé résout column-count et column-width contre la boîte de contenu, équilibre le dernier fragment de chaque conteneur multicol par une recherche binaire, remplit les pages antérieures colonne par colonne jusquau bas de page, et prend en charge column-fill, column-span: all et column-rule
La raison habituelle de vouloir cela est ennuyeuse et réelle : un bulletin produit à deux colonnes, une annexe de conditions générales composée en trois colonnes étroites, ou un PDF de notes de version qui devrait ressembler à la newsletter imprimée que léquipe marketing a déjà conçue en HTML. Sans prise en charge des colonnes, limporteur empile chaque paragraphe dans une seule mesure large, la longueur de ligne double, et le document se lit comme un fichier journal. Avec, la même feuille de style qui pilote laperçu navigateur pilote le PDF, et la partie dure devient la pagination, à laquelle les navigateurs navent pour la plupart jamais à penser à lécran
Comment HotPDF décide-t-il du nombre de colonnes à employer ?
HotPDF résout le nombre de colonnes avec les mêmes règles que le pseudo-algorithme de CSS Multi-column Layout Module Level 1 §3.4, appliqué à la largeur W de la boîte de contenu une fois padding et bordures retirés. Un column-count déclaré est pris tel quel. Quand seul column-width est donné, le moteur calcule N = Trunc((W + gap) / (column-width + gap)) avec un plancher dune colonne. Quand les deux sont présents, le résultat est le plus petit des deux, si bien que le compte déclaré peut plafonner le compte dérivé mais jamais le gonfler
Un cas concret : une boîte de contenu de 483pt (A4 avec marges latérales de 56pt, arrondi) et column-width: 140pt; column-gap: 12pt donnent Trunc(495 / 152) = 3 colonnes, chacune large de (483 - 2 * 12) / 3 = 153pt. Ajouter column-count: 2 à la même règle la fait descendre à deux colonnes, parce que le minimum lemporte. Le mot-clé de gap normal se résout en 1em de la taille de police du conteneur, et cette résolution arrive au moment de la mise en page plutôt quau moment de lanalyse, si bien quun font-size déclaré après column-gap dans la même règle produit quand même le bon gap
Un garde-fou ne figure pas dans la spécification, et il est là exprès. Une fois la largeur de colonne calculée, toute valeur sous 12pt replie le conteneur en une seule colonne. Une feuille de style qui dit column-count: 9999, par accident ou parce quelle vient dun modèle non fiable, produirait sinon des milliers de colonnes aux largeurs négatives ou microscopiques et un casseur de lignes qui tente de caser un glyphe par ligne. Le repli sur une colonne garde la sortie lisible et le coût de mise en page borné. Les comptes nuls ou négatifs narrivent jamais jusque-là : le validateur naccepte que auto ou un entier positif pour column-count, et column-width et column-gap prennent respectivement auto ou normal, ou une longueur en points ou en em
Rendre une section HTML multicolonne en PDF en Delphi
La sortie multicolonne nexige aucun interrupteur sur limporteur. THPDFHTMLImporter est créé avec le document THotPDF dans lequel il dessine, BeginDoc doit déjà avoir tourné, et les propriétés de colonnes arrivent par la cascade normale, soit dans le balisage, soit dans la feuille de style auteur passée en second argument de 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;
Notez léchappement doublé dans le dernier paragraphe : & dans la chaîne Pascal est lentité HTML pour une esperluette, que limporteur redécode en un seul caractère. Le reste de la cascade, des budgets et des callbacks de ressources se comporte exactement comme décrit dans la présentation de limport HTML5 paged-media de HotPDF ; les colonnes ne sont quun mode de mise en page de plus dans le même profil
Équilibrer les colonnes par une recherche binaire
HotPDF équilibre les colonnes en cherchant la plus petite hauteur de colonne à laquelle un remplissage glouton place encore chaque ligne restante. La passe gloutonne est simple : faire marcher les lignes dans la colonne un jusquà ce que la ligne suivante dépasse la hauteur candidate, puis continuer dans la colonne deux, et ainsi de suite. La recherche démarre avec la plus haute ligne seule comme borne basse et la somme des hauteurs de toutes les lignes comme borne haute, divise lintervalle par deux à chaque tour, et sarrête une fois les bornes à moins de 0.05pt
Une recherche binaire ne marche que si le prédicat est monotone, et ici il lest : une colonne plus haute ne peut jamais détenir moins de lignes quune plus courte, si bien que "chaque ligne tient" bascule de faux à vrai exactement une fois quand la hauteur croît. Cette propriété est ce qui garantit la convergence et garantit que le remplissage glouton final à la hauteur trouvée consomme chaque ligne, au lieu de laisser une ligne orpheline qui déclencherait une page supplémentaire fallacieuse. Elle explique aussi un détail du résultat : la bande équilibrée est aussi courte que les boîtes de lignes le permettent, pas forcément des colonnes égales, parce que les lignes sont indivisibles et la dernière colonne peut finir une ligne ou deux plus haut que les autres
Que se passe-t-il quand les colonnes traversent un saut de page ?
Les colonnes qui ne tiennent pas sur la page courante sont remplies séquentiellement jusquau bas de page, et seul le fragment de dernière page est équilibré. Cest le comportement que CSS Multi-column Layout Level 1 §7.1 prescrit pour column-fill: balance dans des contextes fragmentés, et cest à quoi ressemble une newsletter imprimée : des colonnes pleines sur chaque page sauf la dernière, où le texte restant est partagé également au lieu de laisser une colonne haute à côté dune vide
Chaque fragment de page passe par le même fragmenteur indépendamment. Si les lignes restantes tiennent dans N colonnes de la hauteur disponible, le fragment est équilibré ; sinon chaque colonne se remplit jusquau bas de page et la boucle démarre une nouvelle page. La hauteur naturelle du conteneur entier, que le moteur emploie pour les décisions break-inside: avoid et la mesure détendue, est la hauteur de bande équilibrée à capacité illimitée, si bien quun conteneur qui tiendrait sur une page fraîche comme bloc équilibré y est déplacé intact plutôt que tranché. Quand une seule ligne est plus haute quune page entière, le moteur relâche la contrainte, place quand même la ligne en haut dune page, et rapporte un diagnostic hdkPaginationConstraint avec le sujet multi-column
column-fill: auto et boîtes à hauteur fixe
Avec column-fill: auto et une height définie, HotPDF remplit les colonnes dans lordre, et chaque hauteur de colonne est le plus petit entre lespace de page restant et la hauteur du conteneur moins son padding et ses bordures. Le minimum compte plus quil ny paraît. Nemployer que lespace de page restant laisserait la première colonne avaler chaque ligne dès que la page a plus de place que la boîte. Sans hauteur définie, column-fill: auto retombe sur léquilibrage, comme les navigateurs, puisquun conteneur multicol sans contrainte na aucune hauteur de colonne à remplir ; la première implémentation a raté ce repli, et un des 27 contrôles de régression la attrapé
column-span: all et paragraphes imbriqués
HotPDF prend en charge column-span: all sur les enfants directs dun conteneur multicolonne, suivant CSS Multi-column Layout Level 1 §6.1. Un enfant sétendant scinde le conteneur en segments : les enfants avant lui forment une bande de colonnes, lélément sétendant se rend sur toute la largeur, et les enfants après lui démarrent une nouvelle bande. Chaque bande est équilibrée, remplie, réglée et paginée pour son compte, si bien quun titre au milieu dune section à trois colonnes produit deux bandes équilibrées avec un titre pleine largeur entre elles, la mise en page que les gens dessinent réellement dans les maquettes
Y arriver a demandé de corriger dabord un problème moins visible. Un conteneur comme <div style="column-count:2"> avec plusieurs enfants <p> rendait chaque paragraphe dans une seule colonne empilée, parce que chaque paragraphe était mis en page comme son propre bloc de texte et la déclaration de colonnes du parent ne sappliquait jamais. HotPDF collecte maintenant le texte des enfants de blocs imbriqués dans un seul flux pour le conteneur. Le collecteur doit insérer une rupture dure à chaque frontière de bloc ce faisant ; la sauter concatène le dernier mot dun paragraphe avec le premier mot du suivant sur la même ligne, un bug facile à rater quand le texte de test finit par hasard en ponctuation
Ce flux fusionné a un périmètre délibéré. Il nest employé que quand tout le sous-arbre est du flux de texte simple. Un conteneur qui détient des contrôles de formulaire, images, tables, listes ou <hr> où que ce soit en lui garde la route de blocs ordinaire et se rend en une seule colonne, parce que le collecteur de texte fusionné ne peut pas exprimer leur géométrie. Les tables en particulier ont leurs propres règles de pagination, couvertes dans larticle sur la gestion de rowspan et les en-têtes de table répétés dans HotPDF, et les mélanger dans une bande de colonnes casserait ces garanties
Où va le column-rule, et comment le tester
HotPDF dessine column-rule sur la ligne médiane de chaque gap, au bord droit dune colonne plus la moitié du gap, du haut au bas de la bande sur chaque fragment de page, comme le décrit CSS Multi-column Layout Level 1 §4. La largeur vaut 1pt par défaut. La couleur vaut par défaut celle du texte de lélément quand elle nest pas déclarée ou vaut currentColor. Labréviation column-rule sans mot-clé de style est traitée comme solid, à la manière dont labréviation border se lit couramment, tandis que none et hidden suppriment la règle
La géométrie des colonnes est observable sans analyser le PDF. Lévénement OnLineLayout de THPDFHTMLImporter tire une fois par ligne dessinée avec un enregistrement THPDFHTMLLineLayout, et comme le moteur dessine chaque colonne séparément, le champ X de chaque ligne est le bord gauche de sa colonne. Des colonnes adjacentes diffèrent exactement de la largeur de colonne plus le gap, ce qui rend facile de distinguer un gap de 12pt dun gap de 20pt dans un test de régression
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 part de zéro ; X est le bord gauche de la colonne de la ligne
Lines.Add(Format('page=%d x=%.1f y=%.1f %s',
[Line.PageIndex, Line.X, Line.Y, string(Line.Text)]));
end;
// Câblage, avant RenderHTML5 :
// Probe := TColumnProbe.Create;
// Importer.OnLineLayout := Probe.LineDrawn;
Deux notes pratiques découlant de lécriture de tests contre cet événement. PageIndex part de zéro, ce qui est la première chose quune assertion écrite à la main a tendance à rater. Et X inclut les décalages dalignement, donc pour du texte centré ou aligné à droite comparez les positions de colonnes sur des fixtures alignées à gauche. La règle elle-même napparaît pas dans lévénement ; pour affirmer sa position, balayez le flux de contenu non compressé à la recherche des opérateurs verticaux m et l
Limites et pièges à connaître
Quelques comportements sont des approximations, et ils sont mieux connus avant une revue de mise en page quaprès. Lespacement de paragraphes dans un flux de colonnes fusionné est approximé par une ligne de la hauteur de ligne du conteneur à chaque frontière de bloc, si bien que les marges par paragraphe dans une bande de colonnes ne sont pas reproduites exactement. Quand column-span: all scinde un conteneur, chaque segment répète le padding du conteneur et la boîte de fond, ce qui garde les segments visuellement continus mais nest pas une boîte unique au sens CSS strict. Toute valeur de column-rule-style autre que none et hidden passe la validation, mais la règle est tracée comme une ligne solid
Les valeurs non prises en charge néchouent pas en silence. Mettre StrictCSS à True transforme chaque diagnostic en exception EHPDFHTMLUnsupportedCSS, tandis que OnDiagnostic laisse un travail par lots les journaliser et continuer. Notez que le mode strict sapplique aussi au diagnostic de pagination, si bien quun document avec une ligne plus haute quune page lèvera sous StrictCSS plutôt que de relâcher la contrainte en silence
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;
// Écriture de modèles : échouer vite sur tout ce que le moteur ignorerait
Importer.StrictCSS := FValidatingTemplate;
Importer.OnDiagnostic := HtmlDiagnostic;
Le mode strict est le bon réglage pendant quun modèle sécrit, puisquune coquille telle que column-span: both est rejetée immédiatement au lieu de laisser en silence le titre couler à lintérieur des colonnes. En production, journaliser via OnDiagnostic est dordinaire le meilleur marché, parce quune déclaration bizarre dans du HTML fourni par un client ne devrait pas coûter le document entier. Les diagnostics identiques (même genre, sujet et valeur) sont rapportés une fois tant quils tiennent dans la liste MaxDiagnostics, qui vaut 256 entrées par défaut, si bien quune déclaration que la cascade visite pour des centaines déléments ne inonde pas le journal
FAQ
HotPDF prend-il en charge labréviation columns ?
Oui. Labréviation columns accepte au plus un compte entier positif et une longueur de colonne dans nimporte quel ordre, et auto réinitialise les deux longhands. Les valeurs résolues suivent les mêmes règles de compte et de largeur que les propriétés individuelles
Un conteneur multicolonne peut-il détenir des images ou des tables ?
Il peut les détenir, mais HotPDF rend alors ce conteneur en une seule colonne. Le flux de colonnes nest appliqué que quand chaque descendant est du flux de texte simple, parce que images, tables, listes et contrôles de formulaire ont besoin de leur propre géométrie de boîte et de règles de pagination
Pourquoi la dernière page montre-t-elle des colonnes équilibrées alors que les pages antérieures sont pleines ?
Cest la règle CSS pour column-fill: balance en média paginé : seul le fragment final est équilibré. Les pages antérieures remplissent chaque colonne jusquau bas de page pour quaucun espace ne soit gaspillé en cours de document
La mise en page multicolonne, le profil HTML5 paged-media et le reste du pipeline HTML vers PDF font partie de HotPDF Component for Delphi and C++Builder, qui couvre aussi le dessin PDF direct, les formulaires, le chiffrement et lextraction de texte