Article technique

Construire des arbres de structure Tagged PDF en Delphi avec PDF Library for Delphi

Un PDF accessible repose sur une structure que la page visible ne montre jamais : l'arbre de structure défini par ISO 32000-1 §14.7. C'est une hiérarchie logique de titres, de paragraphes, de tableaux et de figures, posée par-dessus le contenu peint et rattachée aux rôles standard par une role map. Un lecteur d'écran lit cet arbre, pas les traces sur la page. Sans lui, une facture générée impeccable à l'œil est sémantiquement vide, parce que le flux de contenu n'enregistre que l'ordre de dessin : le total peut être annoncé avant les lignes de détail, le pied de page peut couper un paragraphe en deux, le tableau des articles peut s'effondrer en un seul bloc de mots indifférenciés. Le rapport coût-bénéfice penche largement de votre côté : émettre la structure pendant que vous dessinez, c'est quelques minutes de code ; la rajouter après coup dans des documents finis, c'est un projet de remédiation. losLab PDF Library (PDF Library for Delphi) expose l'arbre à Delphi et C++Builder via un petit jeu d'appels qui enveloppent chaque opération de dessin dans son rôle logique

Comment le contenu marqué se lie à l'arbre de structure

Deux couches coopèrent. Dans le flux de contenu, les opérations de dessin sont encadrées en séquences de contenu marqué, chacune portant un entier MCID. Dans le catalogue du document, l'arbre de structure mappe ces MCID dans une hiérarchie d'éléments typés — H1, P, Table, Figure — avec des attributs comme le texte alternatif et la langue. Les types d'éléments personnalisés sont autorisés mais doivent se résoudre en rôles standard via la role map (ISO 32000-1 §14.8.4), et le contenu qui n'a aucun sens — filets, fonds, éléments répétés de page — est marqué comme artefact afin que les technologies d'assistance l'ignorent au lieu de le lire au milieu d'une phrase

PDF Library for Delphi maintient les deux couches derrière une même paire de crochets : BeginTag ouvre un élément de structure et démarre la séquence de contenu marqué, les appels de dessin se placent dedans, et EndTag ferme les deux. La tenue des comptes — MCID, arbre parent, références de page — se fait en interne, ce qui supprime la partie la plus sujette aux erreurs du balisage fait main

Diagramme PDF Library for Delphi liant des séquences de contenu marqué portant des MCIDs entiers à un arbre de structure H1, P et Figure via le role map, les artifacts étant exclus de l'ordre de lecture
Les MCID entiers relient les séquences de contenu marqué à une arborescence de structure typée, tandis que la table de rôles résout les rôles personnalisés et que les artefacts restent hors de l'ordre de lecture

Deux commutateurs au niveau document cadrent le travail avant toute ouverture de balise. SetMarkInfo écrit le flag du catalogue qui déclare le document balisé, et IsTaggedPDF le relit — la première sonde peu coûteuse pour décider si un fichier entrant possède une structure à préserver. Pour la langue, SetDocumentLanguage définit seul la langue par défaut du document, tandis que SetPDFUAMode la définit dans le cadre de l'activation de la sortie PDF/UA complète ; un fichier peut être utilement balisé sans revendiquer la conformité PDF/UA, et un déploiement progressif commence souvent exactement là

Baliser pendant le dessin, pas après

Le schéma de génération qui fonctionne consiste à traiter le crochet de balise comme une partie de la signature de chaque appel de dessin, jamais comme une passe ultérieure :

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);                          // origine en haut à gauche
    Lib.SetPDFUAMode('en-US');                 // fait passer la version d'enregistrement à PDF 1.7
    Lib.SetInformation(1, 'Service Manual');   // /Title est obligatoire pour PDF/UA
    Lib.AddRoleMap('ManualTitle', 'H1');       // type personnalisé -> rôle standard
    Lib.AddStandardFont(4);
    Lib.SetTextSize(18);
    Lib.BeginTagEx2('ManualTitle', '', '', 'en-US', '', 'h1-cover', '');
    Lib.DrawText(72, 96, 'Service Manual');
    Lib.EndTag;
    Lib.BeginTag('Figure', 'Exploded view of the gearbox assembly', '');
    Lib.AddImageFromFile('gearbox.png', 0);
    Lib.EndTag;
    Lib.BeginArtifact('Layout');               // décoration de page : exclu de la lecture
    // ... dessiner les filets et le fond teinté ...
    Lib.EndArtifact;
    Lib.SaveToFile('manual.pdf');
  finally
    Lib.Free;
  end;
end;

Trois appels de cette séquence ont un poids de conformité. SetPDFUAMode active la sortie PDF/UA et élève silencieusement la version d'enregistrement à PDF 1.7 — ce qui entre en collision avec le verrouillage de version : un document bloqué en PDF 1.4 avec LockSaveVersion refuse l'enregistrement avec le code d'erreur 602 dès que le mode UA est actif, combinaison qui apparaît lorsque les profils d'archivage et les exigences d'accessibilité sont configurés par des équipes différentes. SetInformation(1, ...) écrit le titre du document, qu'ISO 14289 attend que les visualiseurs affichent à la place du nom de fichier ; son absence fait partie des constats PDF/UA les plus courants. Et AddRoleMap enregistre le type personnalisé ManualTitle comme H1 — l'omettre, et les diagnostics ci-dessous signalent le rôle non mappé

La discipline des titres mérite une politique explicite plutôt que des niveaux ad hoc. Les utilisateurs de lecteurs d'écran naviguent par raccourcis de titres ; un modèle qui saute de H1 à H3 parce que le niveau intermédiaire semblait trop grand dans la conception visuelle casse la navigation d'une manière qu'aucune revue visuelle ne voit — et c'est précisément le défaut nommé par le diagnostic HEADING-LEVEL-SKIP. Mapper une fois, à un seul endroit, les styles visuels de chaque modèle vers une échelle fixe de titres empêche cette dérive

Des tableaux qu'un lecteur d'écran peut réellement parcourir

Les lignes de grille dessinées ne signifient rien hors écran. Ce que les lecteurs d'écran parcourent, ce sont les relations structurelles : quelles cellules sont des en-têtes, ce que chaque en-tête gouverne, et comment les cellules de données se lient aux en-têtes dans les dispositions irrégulières. Les appels d'attributs d'éléments de structure gèrent les trois :

Lib.BeginTag('Table', '', '');
Lib.BeginTag('TR', '', '');
Lib.BeginTagEx2('TH', '', '', '', '', 'col-part', '');
Lib.SetStructElemScope('Column');          // valide uniquement tant que ce TH est ouvert
Lib.DrawText(72, 120, 'Part');
Lib.EndTag;
Lib.BeginTagEx2('TH', '', '', '', '', 'col-torque', '');
Lib.SetStructElemScope('Column');
Lib.SetStructElemColSpan(2);               // l'en-tête couvre les colonnes valeur et unité
Lib.DrawText(200, 120, 'Tightening torque');
Lib.EndTag;
Lib.EndTag;
Lib.BeginTag('TR', '', '');
Lib.BeginTag('TD', '', '');
Lib.SetStructElemHeaders('col-part');      // liaison explicite pour les tableaux irréguliers
Lib.DrawText(72, 140, 'M8 flange bolt');
Lib.EndTag;
Lib.EndTag;
Lib.EndTag; // Table

La règle d'ordre est stricte et silencieusement appliquée : chaque appel SetStructElem* s'applique à la balise actuellement ouverte — entre son BeginTag et son EndTag — et renvoie 0 sans lever quoi que ce soit lorsqu'aucune balise n'est ouverte ou lorsque l'attribut ne s'applique pas. Un appel mal placé disparaît simplement. Encapsuler les valeurs de retour dans des assertions pendant le développement attrape la dérive tôt ; sur le terrain, une portée manquante n'apparaît qu'au moment où un audit d'accessibilité fait passer un vrai lecteur d'écran sur le tableau. Les ID d'éléments passés via BeginTagEx2 alimentent l'arbre d'ID (ISO 32000-1 §14.7.4), ce qui rend résoluble la liaison SetStructElemHeaders

La même famille d'attributs couvre les autres structures dont les technologies d'assistance dépendent. SetStructElemListNumbering déclare comment les éléments de liste sont libellés, afin qu'un lecteur d'écran puisse annoncer la position dans la liste plutôt que réciter des glyphes de puce ; SetStructElemBBox enregistre la bounding box des figures et tableaux, utilisée par les vues reflow pour placer le contenu ; SetStructElemActualText fournit un texte de remplacement pour les passages dont les glyphes ne correspondent pas à des caractères lisibles, comme des lettrines assemblées à partir d'art vectoriel. Chacun suit la même règle : l'appel se lie à la balise ouverte, ou disparaît

Diagramme de tableau PDF Library for Delphi montrant un scope TH, un colspan de deux et un attribut headers lié aux cellules de données, à côté de la règle que les appels d'attributs ne lient que tant que leur balise est ouverte
Les lecteurs d'écran suivent les liaisons scope, colspan et headers des TH plutôt que les filets dessinés, et les appels d'attribut ne lient que tant que leur balise est ouverte

Artefacts, langue et barrière de diagnostics avant enregistrement

Les éléments répétés de page — en-têtes courants, marques de pliage, filigranes, teintes de fond — doivent être placés entre BeginArtifact et EndArtifact afin de ne jamais entrer dans le flux de lecture. La langue s'hérite : la valeur par défaut du document vient de l'argument SetPDFUAMode, et les passages dans une autre langue la remplacent par élément via BeginTagEx ou SetStructElemLang, ce qui rend une citation française dans un manuel anglais prononçable

Avant l'enregistrement, GetPDFUADiagnostics exécute les contrôles structurels de la bibliothèque sur le document en mémoire et renvoie les constats sous forme de texte — une chaîne vide signifie qu'aucun problème n'a été trouvé. Les diagnostics nomment directement les erreurs d'auteur classiques : FIGURE-NO-ALT pour les images sans texte alternatif, HEADING-LEVEL-SKIP pour un H3 suivant un H1, ROLEMAP-UNMAPPED pour les types personnalisés jamais enregistrés. Le câbler dans le build — générer le jeu de documents, échouer sur diagnostics non vides — transforme les régressions d'accessibilité en échecs de type compilation plutôt qu'en constats d'audit. Le verdict complet de conformité reste du ressort du preflight sur le fichier enregistré, traité dans le preflight PDF/A et PDF/UA en Delphi, car certaines normalisations sont appliquées pendant la sérialisation

Diagramme PDF Library for Delphi de GetPDFUADiagnostics retournant une chaîne vide ou des constats nommés comme FIGURE-NO-ALT qui font échouer le build avant que le preflight ne juge le fichier sauvegardé
GetPDFUADiagnostics signale des constats tels que FIGURE-NO-ALT avant la sauvegarde, et un résultat non vide branché dans le build fait échouer l'étape immédiatement

La navigation dans les annotations a son propre bouton. PDF/UA attend que le parcours clavier des champs de formulaire et des liens suive l'ordre de structure, et SetTabOrderMode écrit l'entrée d'ordre de tabulation au niveau page que les visualiseurs honorent, avec GetTabOrderMode disponible pour auditer les fichiers entrants. C'est le genre d'exigence que personne ne remarque avant qu'un utilisateur au clavier seul signale le bug, et un appel par document suffit à la respecter

Les arbres de structure ne survivent pas à toutes les fusions

Les documents balisés restent balisés seulement si chaque étape ultérieure préserve l'arbre. Dans PDF Library for Delphi, le bord tranchant est la famille de listes de fusion : MergeFileListFast échange la préservation de l'arbre de structure contre de la vitesse, bon compromis pour des lots d'images scannées et exactement mauvais pour des rapports balisés — la sortie s'ouvre, se rend à l'identique et a perdu toute sa couche d'accessibilité. Utilisez MergeFileList par défaut ou la variante stricte dès qu'une entrée est balisée, et intégrez IsTaggedPDF aux assertions post-assemblage afin qu'un lot aplati silencieusement ne puisse pas partir. Les pipelines d'assemblage de grands jeux de documents portent d'autres compromis de ce type, explorés dans la fusion, la scission et l'accès direct de grands PDF

La boucle de vérification se ferme hors de la bibliothèque : ouvrez la sortie dans Acrobat, inspectez le panneau des balises, et faites lire au moins un document par famille de modèles avec un vrai lecteur d'écran. Les diagnostics attrapent les erreurs structurelles ; seule une oreille humaine attrape un ordre de lecture techniquement valide et pratiquement déroutant. Les builds d'évaluation et la référence complète de l'API de balisage sont sur la page produit losLab PDF Library pour Delphi