Article technique

Mise en page PDF déclarative dans Delphi avec sortie balisée

HotPDF peut construire un document paginé à partir d'un arbre déclaratif plutôt que de coordonnées. Vous assemblez un THPDFDOMDocument à partir de sections, de piles, de texte, de listes et de tableaux, vous le confiez à THPDFDOMRenderer, et le moteur de rendu mesure, pagine, dessine les éléments de mise en page récurrents et, sur demande, génère l'arbre de structure PDF/UA qui rend le résultat accessible. Le code de mise en page ne calcule jamais de coordonnée y

Quiconque a entretenu un générateur de rapports piloté par coordonnées sait pourquoi cela compte. La première version fonctionne. Puis une adresse client passe à trois lignes, un tableau gagne des lignes, un titre localisé se retrouve sur plusieurs lignes, et chaque position y en aval devient fausse. Les correctifs s'accumulent sous forme de vérifications manuelles de sauts de page dispersées dans la logique métier, et l'exigence de PDF balisé qui arrive deux ans plus tard ne peut pas être ajoutée après coup à un code qui n'a aucune idée de ce qu'est un paragraphe

Ce que possède l'arbre, et pourquoi la propriété est stricte

Le DOM impose une propriété unique à chaque niveau : le document possède ses sections, une section possède son corps, son en-tête et son pied de page, et les piles, conteneurs et tableaux possèdent leurs enfants. La réutilisation se fait via Clone ou via une fabrique enregistrée, jamais en rattachant le même objet à deux parents. Cette règle n'est pas une formalité. Un composant qui apparaît deux fois dans l'arbre serait mesuré deux fois avec des contraintes différentes et libéré deux fois lors du démontage

La conséquence pratique pour le code appelant est que les fonctions d'aide renvoient de nouvelles instances. Enregistrer une fabrique avec RegisterComponent et appeler CreateComponent vous donne une recette nommée qui produit un composant neuf à chaque fois, ce qui est la façon dont un élément récurrent tel qu'un bloc de signature ou un pied de page légal trouve sa place dans l'arbre

uses
  HPDFDoc, HPDFLayoutDOM;

var
  Doc: THPDFDOMDocument;
  Section: THPDFDOMSection;
  Table: THPDFDOMTable;
  Row: THPDFDOMTableRow;
  I: Integer;
begin
  Doc := THPDFDOMDocument.Create;
  Doc.GenerateStructure := True;        // générer l'arbre de structure PDF/UA
  Doc.Language := 'en-US';

  Section := Doc.AddSection;
  Section.PageWidth := 595;           // A4 en points
  Section.PageHeight := 842;
  Section.MarginLeft := 56;
  Section.MarginTop := 56;
  Section.MarginRight := 56;
  Section.MarginBottom := 56;
  Section.Style.FontName := 'Helvetica';
  Section.Style.FontSize := 10;

  Section.Body.AddHeading('Annual maintenance report', 1);
  Section.Body.AddText('Every asset inspected during the reporting ' +
    'period is listed below, grouped by site.');
  Section.Body.AddSpacer(12);

  Table := THPDFDOMTable.Create('assets');
  Table.AddColumn(3);                 // poids, pas des largeurs absolues
  Table.AddColumn(1);
  Table.AddColumn(1);
  Table.RepeatHeaders := True;
  Row := Table.AddRow(18, True);      // ligne d'en-tête
  Row[0].Text := 'Asset';
  Row[1].Text := 'Last service';
  Row[2].Text := 'Status';
  for I := 0 to High(Assets) do
  begin
    Row := Table.AddRow(16);
    Row[0].Text := Assets[I].Name;
    Row[1].Text := Assets[I].ServiceDate;
    Row[2].Text := Assets[I].Status;
  end;
  Section.Body.Add(Table);
end;

Comment la pagination évite-t-elle un coût quadratique ?

La façon naïve de paginer un arbre consiste à cloner tout ce qui n'a pas tenu et à le reporter sur la page suivante. Sur un tableau de dix mille lignes, cela clone les lignes restantes une fois par page et transforme un document linéaire en un document quadratique

HotPDF découpe au plus près à la place. Le moteur de rendu de premier niveau parcourt les enfants du corps par index et ne clone jamais une section ou un corps entier. Seuls les piles et conteneurs imbriqués qui chevauchent réellement une limite de page voient leur sous-arbre affecté cloné, et les deux types de feuilles lourdes portent un curseur plutôt qu'une copie : une continuation de texte stocke la plage de caractères source qu'elle doit encore, et une continuation de tableau stocke la tranche de lignes qu'elle doit encore placer. Les documents longs restent linéaires, et les longs paragraphes coûtent la même chose qu'ils se coupent une fois ou cinq fois

La mesure reste honnête quant aux effets de bord. THPDFLayoutElement.Measure doit être exempte de tout effet de bord de dessin, et le placement effectif passe toujours par THotPDF.PlaceLayoutElement, la même routine centrale qui remesure le fragment placé, établit la propriété du débordement et enregistre les diagnostics. Le moteur de rendu du DOM ne décide que de la politique de nouvelle page, des éléments de mise en page récurrents, de l'espacement et de la durée de vie des continuations

Les règles d'en-tête de tableau qui évitent un document infini

Répéter les en-têtes de tableau d'une page à l'autre paraît simple et cache deux modes de défaillance. HotPDF exige que les lignes d'en-tête n'apparaissent que dans la première série de lignes consécutives, et que la première coupure fasse tenir toutes les lignes d'en-tête plus au moins une ligne de corps. Sans cette seconde règle, un en-tête plus haut que l'espace restant produit une page ne contenant rien d'autre que l'en-tête, suivie d'une autre page identique, indéfiniment

Les pages de continuation redessinent l'en-tête, et cette copie redessinée est marquée comme un artefact plutôt que comme du contenu, ce qui est la bonne réponse tant pour l'accessibilité que pour l'extraction de texte. La ligne d'en-tête d'origine ne figure qu'une seule fois dans la structure logique du tableau. Négligez cela et un lecteur d'écran annonce à nouveau les titres de colonnes au milieu des données, tandis qu'un extracteur de texte insère une ligne d'en-tête en double entre les lignes de corps

Il existe également un plafond défensif sur la profondeur de continuation, car un composant personnalisé est libre d'implémenter Split de manière à toujours renvoyer une queue équivalente. Le moteur de rendu vérifie la limite après avoir détaché la queue et avant de démarrer la page suivante, et l'itération en cours libère la queue dans son propre bloc finally, de sorte qu'un composant tiers mal élevé échoue avec une erreur diagnosticable au lieu de remplir un disque

Un élément logique, plusieurs fragments de page

Le balisage automatique est l'endroit où le modèle de pagination et le modèle de structure doivent s'accorder. Un paragraphe scindé sur deux pages est un seul paragraphe logique, donc il doit rester un seul élément de structure. Mais les identifiants de contenu marqué sont propres à chaque page, donc chaque fragment visible a besoin de son propre MCID sur la page où il apparaît

HotPDF résout cela en conservant un seul élément de structure et en ajoutant une référence de contenu marqué à son tableau /K pour chaque fragment, la paire /Pg et /MCID identifiant la page et l'identifiant. L'emplacement du ParentTree pour ce MCID pointe en retour vers le même élément. C'est exactement ce qu'exige la norme ISO 14289, et c'est la raison pour laquelle les clones de continuation se distinguent des clones ordinaires : un Clone ordinaire signifie un nouveau contenu logique et reçoit une nouvelle identité sémantique, tandis que le clone de continuation interne hérite de l'identité du composant qu'il prolonge

La réutilisation des éléments est recherchée via un index d'identités sémantiques triées par pointeur de composant et parcourues par comparaison binaire, ce qui garde la recherche logarithmique sur les grands arbres. L'index ne conserve que des références non propriétaires ; la durée de vie des objets de structure eux-mêmes reste liée au graphe d'objets du PDF

Les règles de structure que le moteur de rendu impose en amont

Lorsque GenerateStructure est activé, plusieurs règles PDF/UA sont vérifiées pendant le rendu de l'arbre plutôt qu'après l'existence du fichier. Les titres commencent au niveau 1 et ne peuvent pas sauter de niveau. LI ne peut apparaître qu'à l'intérieur de L, et Lbl ainsi que LBody uniquement à l'intérieur de LI. TR appartient à un tableau, et TH ainsi que TD à une ligne. Une figure sans texte alternatif est rejetée en mode PDF/UA

Rejeter tôt est le choix délibéré ici. Un validateur qui signale un texte alternatif manquant après l'écriture du document vous indique qu'un lot de dix mille relevés doit être régénéré ; un moteur de rendu qui refuse le composant vous indique quel composant, alors que les données qui l'ont produit sont encore dans la portée. La vérification de conformité a toujours sa place dans le pipeline en tant qu'étape distincte, et son fonctionnement est traité dans la validation PDF/A, PDF/X et PDF/UA

var
  Pdf: THotPDF;
  Renderer: THPDFDOMRenderer;
  Stats: THPDFDOMRenderStatistics;
begin
  Pdf := THotPDF.Create(nil);
  Renderer := THPDFDOMRenderer.Create;
  try
    Pdf.FileName := 'maintenance-report.pdf';
    Pdf.BeginDoc;
    Stats := Renderer.Render(Doc, Pdf);
    Pdf.EndDoc;

    Writeln(Format('%d page(s), %d placement(s), %d split(s)',
      [Stats.PageCount, Stats.PlacementCount, Stats.SplitCount]));
    Writeln(Format('structure elements=%d marked content=%d artifacts=%d',
      [Stats.StructureElementCount, Stats.MarkedContentCount,
       Stats.ArtifactCount]));
    Writeln(Format('deepest continuation chain: %d',
      [Stats.MaximumContinuationDepth]));
  finally
    Renderer.Free;
    Doc.Free;
    Pdf.Free;
  end;
end;

L'enregistrement de statistiques est plus utile qu'il n'y paraît de prime abord. Une forte hausse de SplitCount après un changement de gabarit signifie généralement qu'un composant a commencé à se mesurer plus haut que son conteneur. Une hausse progressive de MaximumContinuationDepth est le signal d'alerte précoce d'un composant dont le Split ne progresse pas assez par page. Et comparer ArtifactCount au nombre de pages de continuation confirme que les en-têtes répétés ont bien été balisés comme des artefacts

Où se situe le DOM par rapport à l'API directe

Le DOM ne remplace pas le dessin direct ; il se superpose aux mêmes objets de page. Tout ce que le moteur de rendu place peut être entrelacé avec des appels directs sur THotPDF, ce qui compte lorsqu'un rapport a besoin d'un élément positionné manuellement, comme une image de signature à un emplacement précis. La fermeture de page reste sous le contrôle d'AddPage et d'EndDoc, de sorte que le mode de vidage immédiat ne conserve aucune page terminée en mémoire et que la mémoire résidente reste régie par les continuations en cours, les ressources de police et le graphe d'objets habituel du document

Choisissez le DOM lorsque le contenu est piloté par les données et la mise en page par des règles, et conservez le dessin direct pour les visuels fixes. Si votre difficulté actuelle concerne spécifiquement la pagination de tableaux, l'approche plus ciblée décrite dans la génération de tableaux en PDF vaut la peine d'être lue en premier, et le comportement au niveau du texte tel que la justification est décrit dans la justification de texte

La mise en page déclarative, le balisage automatique et l'API de dessin direct sont livrés dans le même composant pour Delphi et C++Builder ; la liste complète des fonctionnalités se trouve sur la page du composant PDF Delphi HotPDF