Faites lire une facture générée classique par un lecteur d'écran et écoutez ce qu'il trouve : le total général annoncé avant les lignes, le pied de page qui interrompt un paragraphe, le tableau d'articles aplati en un flux indifférencié de mots. La page semble parfaite et elle est sémantiquement vide, parce que le flux de contenu enregistre l'ordre de dessin, pas le sens. La réponse du PDF est l'arbre de structure défini dans ISO 32000-1 §14.7 — une hiérarchie logique de titres, paragraphes, tableaux et figures posée sur le contenu peint — et l'économie est asymétrique : émettre la structure pendant la génération coûte quelques minutes de code, tandis que l'ajouter à des documents terminés devient un projet de remédiation. losLab PDF Library (PDFlibPas) expose cette mécanique aux applications Delphi et C++Builder via un petit ensemble 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.
PDFlibPas 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.
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); // top-left origin
Lib.SetPDFUAMode('en-US'); // bumps the save version to PDF 1.7
Lib.SetInformation(1, 'Service Manual'); // /Title is mandatory for PDF/UA
Lib.AddRoleMap('ManualTitle', 'H1'); // custom type -> standard role
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'); // page decoration: excluded from reading
// ... draw rules and background tint ...
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'); // valid only while this TH is open
Lib.DrawText(72, 120, 'Part');
Lib.EndTag;
Lib.BeginTagEx2('TH', '', '', '', '', 'col-torque', '');
Lib.SetStructElemScope('Column');
Lib.SetStructElemColSpan(2); // header spans the value and unit columns
Lib.DrawText(200, 120, 'Tightening torque');
Lib.EndTag;
Lib.EndTag;
Lib.BeginTag('TR', '', '');
Lib.BeginTag('TD', '', '');
Lib.SetStructElemHeaders('col-part'); // explicit binding for irregular tables
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.
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.
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 PDFlibPas, 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.