Article technique

Mise en page multicolonne CSS en HTML vers PDF avec HotPDF

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

Résolution du nombre de colonnes CSS multicolonne HotPDF pour HTML vers PDF en Delphi : column-count est employé tel que déclaré, column-width dérive N = Trunc((W + gap) / (column-width + gap)), le plus petit lemporte quand les deux sont posés, et toute colonne sous 12pt se replie en une seule colonne
Le moteur applique le pseudo-algorithme CSS à la boîte de contenu, plafonne le compte dérivé avec un compte déclaré, et ajoute un garde-fou de plancher à 12pt pour quun modèle non fiable ne puisse pas produire des milliers de colonnes microscopiques

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 &amp; 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 : &amp; 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

Équilibrage multicolonne HotPDF en Delphi : une recherche binaire entre la plus haute ligne seule et la somme de toutes les hauteurs de lignes trouve la plus petite hauteur de colonne dont le remplissage glouton place chaque ligne, avec des bornes convergeant à moins de 0.05pt
Le prédicat chaque-ligne-tient est monotone en hauteur de colonne, si bien que diviser lintervalle par deux converge vers la bande équilibrée la plus courte et le remplissage glouton final consomme chaque ligne sans orphelines

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

Pagination multicolonne HotPDF pour HTML vers PDF en Delphi : les fragments avant la dernière page remplissent chaque colonne jusquau bas de page, le dernier fragment séquilibre à travers les colonnes, et column-span all scinde un conteneur en bandes équilibrées séparément autour dun élément pleine largeur
Seul le dernier fragment dun conteneur multicol est équilibré ; les pages antérieures se remplissent séquentiellement, et chaque fragment répète équilibre, règles et pagination indépendamment

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