Article technique

Validation de l'arbre structurel PDF/UA dans Delphi avec PDFium

Votre rapport de précontrôle indique que le fichier est conforme à PDF/UA. veraPDF ouvre le même fichier et signale une Figure sans texte alternatif selon la clause 7.3. Les deux outils ont raison, et l'écart entre eux résume tout le problème d'une vérification de l'accessibilité par balayage des octets. Un passage au niveau des octets confirme que le fichier dit qu'il est balisé : il trouve le /StructTreeRoot, le /MarkInfo /Marked true, le pdfuaid:part dans le paquet XMP, le titre du document, la langue. Ce sont des marqueurs de format, et ils sont nécessaires. Ils ne vous disent rien sur la question de savoir si l'image réelle de la page quatre porte une description qu'un lecteur d'écran peut lire à voix haute. La réponse se trouve dans l'arbre de balises, et pour l'obtenir il faut parcourir l'arbre

PDFium Component est une bibliothèque PDF VCL native pour Delphi et C++Builder, et sa méthode ValidatePdfUa fait les deux vérifications. Le passage au niveau des octets gère les marqueurs de format. Au-dessus de lui se trouve un passage sur l'arbre de structure qui charge l'arbre balisé vivant, parcourt chaque élément et vérifie le petit ensemble de règles de contenu à forte confiance où l'absence d'un attribut signale un véritable défaut d'accessibilité plutôt qu'une préférence de style. Cet article porte sur ce second passage : ce qu'il vérifie, pourquoi la logique de règle est une fonction pure sans DLL en dessous, et où elle s'arrête volontairement

Pourquoi un balayage des octets ne peut pas voir un Alt manquant

ISO 14289-1 (PDF/UA-1) est une couche d'exigences au-dessus d'ISO 32000. Certaines de ces exigences sont structurelles et visibles dans le fichier brut : le catalogue doit déclarer un arbre structurel, les préférences du lecteur doivent définir DisplayDocTitle, les polices doivent être incorporées. Un analyseur de jetons qui supprime le contenu des flux et fait correspondre les jetons de noms avec des bornes de délimiteurs peut tout vérifier, et le ValidatePdfUaCompliance fait exactement cela pour des clauses comme 7.1, 7.18 et 7.21

Mais "every Figure has alternate text" n'est pas une propriété de la syntaxe du fichier. C'est une propriété de la structure logique, l'arbre des éléments balisés qui mappe le contenu au sens. L'entrée Alt d'une Figure peut se trouver dans le dictionnaire de l'élément de structure, être fournie via un /ActualText span, ou provenir d'un type personnalisé role-mappé. Vous ne pouvez pas la trouver de manière fiable en cherchant /Alt dans le flux d'octets, parce que cette chaîne apparaît dans des contextes sans rapport, peut être compressée dans un objet stream, et ne vous dit rien sur quel élément de structure elle appartient. La manière honnête de répondre à la question est de demander à l'arbre de structure du document, élément par élément, comme le font veraPDF et PAC. C'est la ligne autour de laquelle les vérifications Tier-1 de PDFium sont construites : balayage des octets pour le format, parcours de l'arbre pour le contenu

Lecture de l'arbre de balises en direct

La matière première est TPdf.GetStructureElements (également exposée via la StructureElements propriété), qui renvoie un TPdfStructureElements, un tableau plat de TPdfStructureElement enregistrements en ordre de document. Chaque enregistrement est la projection d'un élément de structure au travers des fonctions d'accès de PDFium, avec les champs réellement nécessaires aux règles d'accessibilité :

type
  TPdfStructureElement = record
    Level: Integer;            // depth in the tag tree
    ParentIndex: Integer;      // index of parent element, or -1
    TypeName: WString;         // standard /S name: Figure, Formula, Note...
    Title: WString;            // /T
    AlternateText: WString;    // /Alt   (FPDF_StructElement_GetAltText)
    ActualText: WString;       // /ActualText
    Expansion: WString;        // /E
    ID: WString;               // /ID    (FPDF_StructElement_GetID)
    Language: WString;         // /Lang
    MarkedContentIDs: TPdfIntegerArray;
    // ... child bookkeeping fields
  end;

Le champ TypeName est celui sur lequel le validateur se concentre. Il provient de FPDF_StructElement_GetType, qui renvoie le type de structure standard de l'élément, son /S nom, après que PDFium a résolu le role map. AlternateText provient de FPDF_StructElement_GetAltText, ActualText de FPDF_StructElement_GetActualText, et ID de FPDF_StructElement_GetID. Parce que le tableau est plat et ordonné, le validateur peut raisonner sur l'ensemble du document en une seule fois au lieu de récursiver, ce qui compte pour la seule règle globale plutôt que par élément

Le vérificateur est une fonction pure, et c'est volontaire

La logique des règles ne vit pas dans la méthode qui parle à la DLL. C'est une fonction pure, publique et autonome :

function ValidatePdfUaStructureElements(
  const Elements: TPdfStructureElements): TPdfUaValidationIssues;

Elle prend un tableau plat d'éléments et renvoie un ensemble de problèmes. Elle n'appelle aucune fonction PDFium, n'ouvre aucun document, ne touche à aucun état global. Cette séparation est voulue, et elle rapporte deux fois. D'abord, la testabilité : vous pouvez construire un TPdfStructureElements tableau synthétique dans un test unitaire, par exemple une Figure sans Alt, une Formula dont le seul texte accessible est dans ActualText, deux Notes qui partagent un ID, et vérifier le jeu de résultats sans pdfium.dll présent du tout. La logique des règles est vérifiée hors ligne ; la traversée de la DLL est vérifiée séparément par un test de fumée sur document réel qui saute quand la bibliothèque est absente

Deuxièmement, la clarté des responsabilités. TPdf.ValidatePdfUa prend en charge la partie sale, chargement de chaque page, extraction de ses éléments, accumulation, puis remet un tableau propre au vérificateur pur. "Get the data" (DLL, effets de bord, durée de vie) et "judge the rules" (pur, déterministe) ne se mélangent jamais. Lorsqu'une règle doit changer, vous modifiez une fonction qui n'a aucune E/S

Ce que vérifient réellement les trois règles

Le passage sur l'arbre de structure renvoie trois valeurs d'erreur, ajoutées à la fin de TPdfUaValidationIssues pour que l'énumération reste stable au niveau ABI pour les appelants existants : pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt, et pvuaiNoteMissingId. Le corps est assez petit pour être entièrement compris :

for I := 0 to High(Elements) do
begin
  T := string(Elements[I].TypeName);
  if T = 'Figure' then
  begin
    // §7.3 — a Figure needs an alternate representation:
    // an Alt entry OR ActualText. Flag only when BOTH are empty.
    if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
      Include(Result, pvuaiFigureMissingAlt);
  end
  else if T = 'Formula' then
  begin
    // §7.7 — same rule as Figure: Alt OR ActualText.
    if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
      Include(Result, pvuaiFormulaMissingAlt);
  end
  else if T = 'Note' then
  begin
    // §7.9 — every Note must have a unique ID.
    NoteId := string(Elements[I].ID);
    if NoteId = '' then
      Include(Result, pvuaiNoteMissingId)
    else
      for J := 0 to I - 1 do
        if (string(Elements[J].TypeName) = 'Note') and
           (string(Elements[J].ID) = NoteId) then
        begin
          Include(Result, pvuaiNoteMissingId);
          Break;
        end;
  end;
end;

La clause 7.3 régit les figures : une Figure doit fournir une alternative textuelle. La première version de cette vérification ne regardait que l'entrée Alt, ce qui la rendait plus stricte que les validateurs de référence. PDF/UA accepte une figure dont le texte accessible est fourni via ActualText à la place, le texte de remplacement étant une représentation alternative valide, donc la règle signale une Figure seulement lorsque les deux Alt et ActualText sont vides. La clause 7.7 couvre les formules, et après la même correction elle utilise le même test Alt ou ActualText ; un échantillon du corpus de conformité qui donnait le texte accessible d'une Formula uniquement via ActualText était rejeté à tort jusqu'à ce que la branche Formula soit alignée sur la branche Figure

La clause 7.9 est différente par nature. Une Note doit avoir un /ID, et cet ID doit être unique dans tout le document. Un ID manquant est une défaillance par élément. Un ID en double est une relation entre deux éléments, ce qui explique pourquoi le tableau plat compte : pour chaque Note, le vérificateur remonte les éléments déjà vus et signale une collision avec toute Note antérieure portant le même ID. Le coût est le O(n²) évident sur le nombre de Notes, ce qui est sans importance pour n'importe quel document réel et garde la fonction sous la forme d'une seule boucle lisible sans index auxiliaire à maintenir en synchronisation

Accumuler sur plusieurs pages pour que l'unicité soit globale

PDFium expose les éléments de structure par page, et non par document, donc l'orchestration dans ValidatePdfUa doit les rassembler avant que les règles s'exécutent. Elle parcourt chaque page avec FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, indépendamment de la page actuellement ouverte dans le composant, et ajoute les éléments de chaque page dans un seul tableau. Ce n'est qu'ensuite qu'elle appelle le vérificateur pur :

// inside TPdf.ValidatePdfUa, after the byte-level pass
if (FDocument <> nil) and
   (not (pvuaiMissingStructTreeRoot in Result.Issues)) then
begin
  AllElems := nil;
  PageTotal := FPDF_GetPageCount(FDocument);
  for I := 0 to PageTotal - 1 do
  begin
    Page := FPDF_LoadPage(FDocument, I);
    if Page = nil then Continue;
    try
      PageElems := GetStructureElementsForPage(Page);
    finally
      FPDF_ClosePage(Page);
    end;
    // append PageElems into AllElems ...
  end;
  Result.Issues := Result.Issues + ValidatePdfUaStructureElements(AllElems);
end;

L'accumulation est ce qui rend correcte la vérification d'unicité de 7.9. Deux Notes sur des pages différentes peuvent partager un ID ; si vous validiez page par page, vous ne verriez jamais la collision, car l'ensemble des éléments de chaque page semble cohérent de l'intérieur. Construire un seul tableau couvrant tout le document est la seule façon de rendre le doublon visible. Le garde-fou à l'entrée vaut aussi la peine d'être noté : la traversée de l'arbre ne s'exécute que lorsque le passage au niveau des octets n'a pas signalé pvuaiMissingStructTreeRoot. Un document non balisé n'a pas d'arbre à parcourir et a déjà été signalé pour la racine de structure manquante, donc les chargements page par page sont entièrement ignorés. Le passage profond ne coûte rien sur les documents qui ne peuvent pas en bénéficier

Conservateur par conception : manquer discrètement, ne jamais crier au loup

La propriété la plus importante de ce validateur est ce qu'il refuse de faire. Il ne correspond qu'aux noms de type standard /S, ceux que FPDF_StructElement_GetType renvoie directement : Figure, Formula, Note. Un document qui définit un type personnalisé et le role-mappe vers Figure va, selon la manière dont PDFium résout le type, renvoyer son propre nom. Quand cela arrive, le vérificateur ne le reconnaît pas et reste silencieux. C'est un faux négatif, et c'est le comportement voulu. La règle de conception consiste à manquer plutôt que produire jamais un faux positif, parce qu'un outil de précontrôle qui crie au loup sur des fichiers conformes apprend à ses utilisateurs à l'ignorer, et un validateur ignoré vaut pire que rien. Les images décoratives vivent dans le flux d'artéfacts, pas dans l'arbre de structure, donc elles n'apparaissent jamais comme Figures au départ ; vous n'obtiendrez pas de plainte "Alt manquant" pour une règle de fond correctement marquée comme artéfact

C'est aussi pour cela que le périmètre est limité à trois règles. L'imbrication des niveaux de titre (clause 7.4), la portée des en-têtes de tableau (7.5) et la détection des cycles dans la role map (7.1) sont toutes des exigences PDF/UA légitimes, mais bien les vérifier demande une vraie analyse de graphe et d'attributs, et les vérifier naïvement produit exactement les faux positifs que la conception interdit, car PDF/UA autorise des séquences de titres comme H1, H2, H3, H3 qu'une règle simple "doit strictement augmenter" rejetterait à tort. Ces vérifications sont laissées à des outils de conformité dédiés. Le jeu Tier-1 est le sous-ensemble où l'absence d'un attribut ne laisse aucune ambiguïté

La limite, énoncée clairement

Deux limites méritent d'être connues avant de raccorder cela à une barrière de publication. D'abord, le vérificateur ne vaut que ce que PDFium peut lire dans l'élément de structure. Quelques fichiers du corpus de conformité que les validateurs de référence acceptent utilisent un mécanisme de texte alternatif que PDFium n'expose pas, donc FPDF_StructElement_GetAltText renvoie vide alors que le fichier est réellement conforme. Le checker pur signale alors à juste titre un Alt manquant sur des données incomplètes, une fausse alerte qui provient de la couverture des accesseurs de la DLL, pas de la logique de règle. Assouplir la règle pour absorber ces cas la rendrait aussi aveugle aux vraies erreurs qu'elle doit détecter, donc ils sont documentés comme une limitation connue de PDFium plutôt que masqués

Deuxièmement, il s'agit d'un précontrôle, pas d'une certification. Tier-1 attrape les erreurs de contenu à forte confiance qu'un balayage des octets ne peut structurellement pas attraper, et il le fait sans fausses alertes, mais la conformité PDF/UA complète, y compris la sémantique des titres, la structure des tableaux et la correction de l'ordre de lecture, relève toujours d'un validateur complet et, au final, d'un relecteur humain. Utilisez ValidatePdfUa pour faire échouer rapidement et à faible coût les défauts évidents dans votre propre pipeline, puis laissez veraPDF ou PAC avoir le dernier mot. La même traversée de l'arbre de structure sert à construire un lecteur PDF accessible dans Delphi, où l'arbre de balises pilote l'ordre de lecture et le texte lu à voix haute, et elle complète le travail au niveau des métadonnées dans la revue des annotations PDF depuis Delphi

Les API de l'arbre de structure et le ValidatePdfUa validateur présenté ici sont livrés avec le PDFium Component pour Delphi et C++Builder (VCL) ainsi que Lazarus/FPC (LCL). La page produit renvoie vers la référence complète de l'API, y compris la structure complète de l'enregistrement TPdfStructureElement et l'énumération des problèmes derrière ces vérifications