Ouvrez un rapport généré dans un lecteur d'écran et les ratés habituels apparaissent en une minute : « Page 3 sur 40 » est lu à voix haute sur chaque page, le logo de la société est annoncé comme « graphique » sans rien derrière, et une table sort comme une longue suite de chiffres sans noms de colonnes. PDF Library for Delphi évite les trois avec le balisage de structure automatique : appelez SetAutoTagMode une fois, et les appels de dessin ordinaires produisent paragraphes, titres, figures, tableaux et meubles de page ignorables pendant que le document se dessine
La voie manuelle existe toujours. Vous pouvez envelopper chaque appel de dessin dans BeginTag et EndTag, et pour les documents à structure inhabituelle c'est le bon outil. Pour un rapport, une facture ou un relevé ordinaires, pourtant, le balisage à la main fait dépendre l'accessibilité de ce que personne n'oublie jamais une paire, dans chaque chemin de code qui dessine quoi que ce soit, pendant toute la vie du produit. On ne mise pas une exigence de conformité là-dessus
Ce que SetAutoTagMode active
SetAutoTagMode prend un masque de bits et renvoie le mode en vigueur avant l'appel. Chaque bit couvre un type de contenu :
AUTOTAG_TEXT(1) : le texte devient un paragraphe, ou un titre quand un a été enregistréAUTOTAG_FURNITURE(2) : en-têtes, pieds de page et numéros de page courants deviennent des artéfacts qu'un lecteur sauteAUTOTAG_FIGURE(4) : une image dessinée devient une figure, ou un artéfact quand vous l'avez déclarée décorativeAUTOTAG_TABLE(8) : les tableaux dessinés portent leurs lignes et leurs cellules dans l'arborescence de structureAUTOTAG_DEFAULT(15) : les quatre, et la valeur que nous recommandons pour tout nouveau code de rapport
Activer le mode marque aussi le document comme balisé, et cette étape compte plus qu'il n'y paraît. Selon ISO 32000-1 §14.7.1, un lecteur traite un fichier comme non balisé tant que le dictionnaire /MarkInfo du catalogue ne dit pas /Marked true. Un fichier peut porter une arborescence de structure complète et correcte et être annoncé comme n'ayant aucune structure, parce qu'il manque l'unique indicateur qui dit aux technologies d'assistance d'aller la chercher. L'arborescence est là. Rien ne la lit
Le balisage du texte fonctionne par appel. Chaque appel DrawText, DrawWrappedText ou DrawTextBox devient un élément, si bien qu'un paragraphe dessiné ligne à ligne avec cinq appels DrawText arrive au lecteur en cinq paragraphes d'une ligne. Si votre code de mise en page coupe déjà les lignes lui-même, cela mérite un changement : donnez le paragraphe entier à DrawWrappedText et laissez un appel produire un paragraphe
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1); // origine en haut à gauche, y croît vers le bas
Lib.SetDocumentLanguage('en-US');
Lib.SetAutoTagMode(AUTOTAG_DEFAULT); // avant tout appel à RegisterHeading
Lib.AddStandardFont(4);
Lib.SetTextSize(18);
Lib.RegisterHeading(1, 'Annual service report');
Lib.DrawText(72, 96, 'Annual service report'); // balisé H1
Lib.SetTextSize(11);
Lib.DrawWrappedText(72, 130, 450,
'Every unit installed before 2024 was inspected. ' +
'Units that failed the torque check were serviced on site.'); // un seul P
// ... pages restantes ...
// Parcourt toutes les pages existantes, à appeler une fois les pages dessinées
Lib.AddPageFooter('', 'Page &P of &N', 36, 1.0, 1); // artéfact de pagination
Lib.SaveToFile('report.pdf');
finally
Lib.Free;
end;
end;
Le commentaire de la troisième ligne est là pour une bonne raison. RegisterHeading ne met en file un niveau de titre que si le balisage du texte est déjà actif, et SetAutoTagMode vide le niveau en attente quand il s'exécute. Enregistrez le titre d'abord et activez le mode ensuite, et le titre ressort en paragraphe simple, sans aucune erreur nulle part pour vous le dire
Pourquoi un titre attend-il l'image dessinée avant lui ?
RegisterHeading nomme le niveau pour le prochain morceau de texte, et seul le texte peut le consommer. Si une image est dessinée entre-temps, l'image devient une figure et le titre reste en file pour le texte qui suit. L'implémentation évidente laisse n'importe quel tag consommer le niveau en attente, et alors la séquence courante — enregistrer un titre de section, dessiner une image de bannière, dessiner le titre — produit une bannière balisée H2 et un vrai titre rétrogradé en paragraphe. Un lecteur qui navigue par titres tombe sur une image
Deux autres conséquences découlent du fonctionnement de la file. Le niveau de titre s'attache au texte qui vient ensuite, quelle que soit la chaîne passée à RegisterHeading : si votre mise en page dessine un petit surtitre « Section 3 » au-dessus du titre, c'est le surtitre qui devient le titre. Et les niveaux au-delà de six sont écrits en H6, car le lecteur ne connaît rien au-delà. L'argument Title fait toujours son propre travail, il alimente le plan que DrawTOC transforme ensuite en table des matières
Les images reçoivent le même traitement « dépensé sur un élément ». RegisterFigure fournit la description que portera l'image suivante, et RegisterDecoration déclare la suivante comme filet, bordure ou arrière-plan sans signification, écrit ensuite comme artéfact Layout (ISO 32000-1 §14.8.2.2). Chaque déclaration est consommée par exactement une image, si bien qu'une image ultérieure n'hérite jamais d'une description écrite pour une précédente. Un texte alt attaché à la mauvaise image est pire que pas de texte du tout, car le lecteur énonce quelque chose de faux avec une entière assurance
Nous n'avons délibérément pas cherché à deviner quelles images sont décoratives. Une image est soit du contenu que le lecteur doit décrire, soit du contenu qu'il doit sauter, et seul le code appelant sait laquelle des deux. Le cas à éviter est le troisième : une image dessinée sans aucune déclaration devient une figure sans description, ce qui échoue en PDF/UA et fait s'arrêter le lecteur d'écran pour dire « graphique » puis rien. Si vous greppez votre code de rapport pour DrawImage, chaque occurrence devrait avoir un RegisterFigure ou un RegisterDecoration juste au-dessus
Lib.RegisterHeading(2, 'Gearbox inspection'); // en attente pour le prochain texte
Lib.RegisterFigure('Exploded view of the gearbox assembly');
Lib.AddImageFromFile('gearbox.png', 0); // charge et sélectionne l'image
Lib.DrawImage(72, 160, 360, 270); // devient une Figure balisée
Lib.RegisterDecoration; // filet sans signification
Lib.AddImageFromFile('divider.png', 0);
Lib.DrawImage(72, 450, 360, 4); // dessiné dans un artéfact de mise en page
Lib.DrawText(72, 480, 'Gearbox inspection'); // c'est seulement maintenant qu'apparaît le H2
La description mérite plus de soin que n'importe quelle autre chaîne du document. Un lecteur non voyant la reçoit à la place de l'image, et c'est tout ce qu'il reçoit. « Diagramme » ne lui dit rien ; « Chiffre d'affaires trimestriel par région, avec l'est au plus haut au T3 » lui dit ce qu'un lecteur voyant tire de l'image en deux secondes
Tableaux : les lignes d'en-tête se déclarent, elles ne se devinent pas
Avec le bit tableau actif, DrawTableRows écrit la table, ses lignes et ses cellules dans l'arborescence de structure, si bien qu'un lecteur peut dire de quelle colonne vient une valeur au lieu de lire la grille comme un texte sans rapports. SetTableHeaderRowCount dit combien de lignes de tête sont des en-têtes. Ces lignes sont écrites en cellules TH avec une portée de colonne, ce qui permet au lecteur d'annoncer « Torque » avant la valeur vers laquelle l'utilisateur s'est déplacé
Le nombre de lignes d'en-tête vaut zéro par défaut, et c'était un choix. La première ligne d'une table est généralement l'en-tête, donc deviner aurait raison la plupart du temps. Le coût d'une mauvaise devinette est asymétrique, pourtant : une ligne de données prise pour un en-tête est lue à voix haute devant chaque valeur de sa colonne, bien plus déroutant qu'une table sans en-têtes du tout. Une ligne de code de votre côté coûte moins cher que ce mode de défaillance
Déclarer les lignes d'en-tête et les répéter à chaque page sont deux décisions distinctes. SetTableHeaderRowCount est sémantique et ne change rien à la page. La répétition est de la mise en page, elle appartient à l'argument RepeatHeaderRows de DrawTaggedTableRows. Si les deux n'en faisaient qu'un, demander des balises d'en-tête correctes déclencherait aussi le dessin de bandeaux d'en-tête répétés jamais demandés, et une mise en page vérifiée page par page se décalerait. Les lignes passées en RepeatHeaderRows sont balisées comme en-têtes elles aussi, inutile donc de dire deux fois la même chose
var
TableID: Integer;
begin
TableID := Lib.CreateTable(40, 3);
Lib.SetTableHeaderRowCount(TableID, 1); // la ligne 1 sert de bandeau d'en-tête
Lib.SetTableCellContent(TableID, 1, 1, 'Part');
Lib.SetTableCellContent(TableID, 1, 2, 'Torque');
Lib.SetTableCellContent(TableID, 1, 3, 'Unit');
// ... remplir les lignes de données ...
// Dessine les lignes 1..40 dans un bandeau de 600pt, avec une ligne d'en-tête répétée par page
Lib.DrawTaggedTableRows(TableID, 72, 150, 600, 1, 40, 1);
end;
Sections balisées à la main dans un document automatique
Le balisage automatique s'efface tant qu'un tag ouvert à la main reste ouvert. Votre code peut ainsi décrire lui-même une partie du document et laisser le reste à la bibliothèque, sans que les deux s'imbriquent l'un dans l'autre — c'est l'arrangement que veulent la plupart des documents réels. La page de couverture et le bloc de signature ont une structure que vous seul comprenez. Les deux cents pages de corps de texte entre elles ne l'ont pas
Trois règles gardent la sortie valide quel que soit votre mélange. Rien n'est balisé dans un artéfact, car la séquence de contenu marqué d'un artéfact ne peut pas porter aussi un élément de structure, et un fichier qui l'essaie échoue à la validation PDF/UA. Un texte vide n'ouvre aucun élément, si bien qu'un DrawText égaré avec une chaîne vide ne peut pas laisser derrière lui un élément qu'un lecteur annoncerait comme un blanc. Et un appel de bibliothèque qui dessine à travers d'autres appels de dessin en interne ouvre un tag au niveau extérieur, ses morceaux ne se retrouvent donc jamais imbriqués dans un second élément à eux
Après enregistrement, relisez l'arbre avant de lui faire confiance. L'API de lecture est à base 1, cela nous a pris : notre première boucle de contrôle allait de 0 à StructTreeItemCount - 1, recevait un enregistrement vide au départ, n'atteignait jamais le dernier élément, et pendant un moment nous étions persuadés qu'un paragraphe avait disparu. Rien n'avait disparu. La boucle était simplement décalée d'un cran aux deux bouts
var
I: Integer;
begin
// Une citation mise en avant balisée à la main ; le balisage automatique s'en retire
Lib.BeginTag('BlockQuote', '', '');
Lib.DrawWrappedText(72, 520, 450,
'All readings were taken with the unit at operating temperature.');
Lib.EndTag;
Lib.SaveToFile('report.pdf');
// Relisez l'arbre : les index vont de 1 à StructTreeItemCount
if Lib.LoadFromFile('report.pdf', '') = 1 then
for I := 1 to Lib.StructTreeItemCount do
Writeln(Lib.GetStructTreeItemText(I, 2), ' ', // type résolu
Lib.GetStructTreeItemText(I, 4)); // /Alt, le cas échéant
end;
Un listing pareil est un contrôle de bon sens rapide, pas un verdict. Ce que vous voulez voir, c'est un H1 avant le premier paragraphe, chaque figure portant sa description, et aucune figure avec un /Alt vide
Quand l'ordre de dessin et l'ordre de lecture divergent
Le balisage automatique enregistre les éléments dans l'ordre où le contenu est dessiné. Si votre code de mise en page dessine la barre latérale avant le corps, c'est cet ordre que l'arbre garde, et c'est cet ordre que lit un lecteur d'écran. Pour les documents où l'ordre visuel et l'ordre de lecture diffèrent réellement, ou qui ont besoin d'autres types de structure ou d'une langue par élément, l'API de balisage manuelle convient mieux, et le parcours d'PDF balisé et structure d'accessibilité couvre rôles, portées et liaisons d'en-têtes en détail
Pour les pages que vous n'avez pas dessinées vous-même, comme des pages importées sans contenu marqué, AutoTagPages fonctionne a posteriori : il prend la taille de texte médiane sur les pages fournies comme texte de corps, traite les suites nettement plus grandes comme des titres et marque les autres tracés comme artéfacts. C'est le bon outil pour le contenu que vous ne pouvez pas régénérer. Pour tout ce que votre propre code dessine, nous préférons le balisage au moment du dessin, car la bibliothèque se fait alors dire ce qu'est un titre au lieu de deviner d'après la taille de police
Une fois le document terminé, passez-le à un vérificateur au lieu de présumer : les notes sur le preflight PDF/A et PDF/UA montrent comment obtenir un verdict sur la structure produite, et l'article sur l'export de rapports piloté par dataset montre où ces appels s'insèrent dans un moteur de rapport qui construit sa mise en page depuis les données
PDF Library for Delphi étant écrit en Pascal natif pour Delphi, C++Builder et Lazarus, sans runtime PDF externe à livrer, la sortie balisée sort du même code que celui qui dessine la page. La référence API complète et la liste des plateformes sont sur la page produit de PDF Library for Delphi