Article technique

Flexbox, CSS Grid et notes de bas de page en PDF depuis Delphi

PDF Library for Delphi restitue du HTML dans une page PDF avec une véritable mise en page bidimensionnelle : display: flex et display: grid sont mesurés et placés plutôt que dégradés en blocs empilés, et les notes de bas de page sont réservées en bas de la boîte qui porte leur référence, avec une numérotation qui reste continue à travers les colonnes et les pages. Les points d'entrée sont les habituels, DrawHTMLTextBox pour une seule boîte et DrawHTMLStory pour un flux multi-colonnes

Cela compte car le HTML est aujourd'hui la façon dont arrive la majeure partie du contenu de rapport. Les modèles sont rédigés par des personnes qui écrivent du CSS, les tableaux de bord sont conçus comme des cartes, et un moteur de rendu qui réduit silencieusement une ligne flex à quatre blocs empilés produit un document qui ne ressemble en rien à la conception. Avant l'existence de cette capacité, le seul conteneur bidimensionnel que le moteur mesurait était le tableau, donc chaque mise en page en cartes devait être réécrite à la main sous forme de tableau

Qu'est-ce qui a changé dans le modèle de mise en page ?

La boucle principale précédente maintenait une seule boîte de ligne et avançait le long de la page. Ce modèle gère parfaitement le contenu en ligne et les blocs empilés, mais ne peut pas exprimer un conteneur dont les enfants sont dimensionnés les uns par rapport aux autres. Les tableaux étaient la seule exception, avec leur propre mesure en deux passes

Flex et grid ajoutent chacun une passe de mesure bornée sur les enfants d'un conteneur, et le mot important est borné. Un conteneur flex mesure jusqu'à 256 enfants directs dans un tableau fixe. Une grille utilise une matrice d'occupation d'au plus 64 sur 64 cellules pour un placement automatique déterministe. Ces plafonds existent afin qu'une feuille de style malveillante ou générée ne puisse pas provoquer une récursion non bornée ou une mémoire de placement quadratique, ce qui est une préoccupation réelle lorsque le HTML provient d'un modèle qu'un client modifie

Comment les éléments flex obtiennent leurs tailles

Dans la direction ligne, le conteneur additionne la base de chaque élément ainsi que ses poids de croissance et de rétrécissement, puis distribue l'espace restant, positif ou négatif, selon ces poids. Avec flex-wrap, chaque ligne est résolue indépendamment, de sorte qu'une ligne qui se scinde en deux lignes attribue l'espace libre par ligne plutôt que sur l'ensemble du conteneur. Dans la direction colonne, la même distribution sur l'axe principal s'exécute par rapport à une hauteur explicite ou à la hauteur du contenu

justify-content, align-items, gap et les directions inversées opèrent sur une géométrie déjà mesurée. Ils déplacent des boîtes ; ils ne déclenchent jamais de remesure du contenu des éléments. Cette séparation est ce qui empêche un tableau de bord complexe de mesurer ses enfants plusieurs fois de suite

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;

La valeur de retour est la chaîne de continuation, la façon dont chaque point d'entrée de dessin HTML signale ce qui n'a pas tenu. Transmettez-la à la boîte suivante ou à la page suivante et le flux reprend là où il s'était arrêté

Placement de grille, et ce qu'une piste peut être

Les pistes de grille acceptent des longueurs fixes, des pourcentages, l'unité fr, des expressions repeat() simples et minmax(). Le placement automatique remplit la matrice d'occupation de manière déterministe, de sorte que le même HTML produit toujours le même agencement. Les coordonnées explicites peuvent se chevaucher, ce qui est délibéré : une conception qui superpose un badge sur une carte exprime une intention, pas une erreur. Lorsqu'un seul axe est donné explicitement, le placement ne recherche que sur l'autre axe

Les éléments qui s'étendent sur plusieurs lignes reversent leur hauteur mesurée aux lignes qu'ils couvrent, moyennée entre elles, ce qui empêche un élément en fusion haut de compresser une seule ligne tout en laissant ses voisines trop basses :

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

Les enfants flex et grid sont restitués par le même moteur de rendu HTML que tout le reste, ce qui est la propriété qui rend la fonctionnalité utilisable plutôt qu'un monde à part. Les polices, la cascade CSS, les liens, les images, les tableaux et d'autres conteneurs flex ou grid imbriqués se comportent tous à l'intérieur d'un élément flex exactement comme au niveau supérieur, et le plan de mise en page externe enregistre les commandes finales de texte et de rectangle afin qu'un dessin répété réutilise le cache de mesure existant

Pourquoi les notes de bas de page posent-elles un problème de pagination ?

Une note de bas de page n'est pas un contenu qui s'écoule après le paragraphe contenant sa référence ; c'est un contenu qui doit apparaître en bas de la même boîte que sa référence. Cela inverse l'ordre de mesure habituel, car l'espace disponible pour le texte du corps dépend désormais d'un contenu qui n'a pas encore été mis en page

Le moteur de rendu mesure donc la note lorsqu'il rencontre la référence, et soustrait la zone de la note du budget de hauteur du corps de la boîte bornée courante. Si la référence, le texte du corps rassemblé jusque-là et la note ne peuvent pas tous tenir, le marqueur de note de bas de page et tout ce qui suit basculent ensemble dans la chaîne de continuation. Cette règle est ce qui évite les deux échecs classiques : une note qui écrase le texte du corps, et une note isolée sur une page dont la référence se trouve sur la précédente

Dans une boîte bornée, la zone de note est fixée en bas avec une règle de séparation au-dessus. En mesure non bornée, où il n'y a aucune hauteur de boîte à laquelle se fixer, la zone de note suit immédiatement après le corps. La numérotation est portée dans un champ d'extension sur la pile de continuation, de sorte que DrawHTMLTextBox et DrawHTMLStory maintiennent la séquence en cours à travers les colonnes et les pages, et une chaîne de continuation produite avant l'existence de ce champ reprend quand même correctement

// Les notes de bas de page à l'intérieur d'un récit multi-colonnes maintiennent une seule séquence continue
Html := LoadTemplate('chapter.html');    // utilise des marqueurs float:footnote
Remainder := Lib.DrawHTMLStory(40, 40, 515, 700,
  2,        // colonnes
  16,       // gouttière en points
  20,       // nombre maximal de pages pour ce récit
  Html);
if Remainder <> '' then
  Log('story exceeded its page budget');

Conseils pratiques pour les auteurs de modèles

Concevez dans les limites des plafonds documentés. Un conteneur flex avec plus de 256 enfants directs est presque toujours un tableau de données déguisé en flex, et le chemin tableau le mesure de toute façon mieux. Une grille plus grande que 64 sur 64 est une feuille de calcul, et le même conseil s'applique. Pour le texte du corps sur plusieurs colonnes, le comportement de colonne et de coupure de mots décrit dans la coupure de mots et les colonnes de texte équilibrées régit l'apparence du flux à l'intérieur de chaque colonne

Mesurez avant de dessiner lorsqu'une mise en page doit tenir dans un espace donné. GetHTMLTextHeight indique la hauteur qu'une largeur donnée nécessiterait, ce qui est le moyen économique de choisir entre une mise en page et une autre avant d'engager de l'encre. Et traitez une chaîne de continuation non vide comme normale plutôt qu'exceptionnelle : c'est le mécanisme par lequel un contenu long se pagine, pas un signal d'erreur

Lorsque le HTML provient d'un moteur de rapports plutôt que de modèles écrits à la main, la voie pilotée par jeu de données décrite dans le moteur de rapports piloté par jeu de données se combine bien avec ceci, en générant le balisage que flex et grid organisent ensuite. Et lorsque le même contenu doit aussi ressortir du PDF, le chemin d'export sémantique dans l'export de PDF vers Markdown et DOCX boucle l'aller-retour

La mise en page HTML, la génération de rapports et l'export sémantique font partie d'une même bibliothèque pour Delphi, C++Builder et Free Pascal ; la liste complète des fonctionnalités se trouve sur la page PDF Library for Delphi