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