Une passerelle d'ingestion d'archives a rejeté un lot de fichiers « PDF/A-2b » qui s'ouvraient pourtant sans problème dans tous les lecteurs du bureau. Le fournisseur jurait qu'ils étaient conformes. Ils ne l'étaient pas : chacun contenait une action JavaScript enfouie dans le catalogue, le genre de chose qu'un simple coup d'œil ne repère jamais et qu'un validateur PDF/A complet comme veraPDF signale en un instant. Le hic, c'est que personne ne voulait greffer une chaîne d'outils Java sur un service de lot Delphi juste pour répondre à une question oui ou non par fichier. C'est exactement le rôle queValidatePdfACompliance comble PDFium Component, et il vaut la peine de comprendre comment il tranche sans jamais analyser entièrement un flux de contenu
Pourquoi PDFium lui-même ne peut pas répondre à cela
La première chose à dire honnêtement : lepdfium.dll n'a aucune capacité PDF/A. Il n'existe niConvertToPDFA, ni générateur d'OutputIntent, ni API XMP dans l'interface publique. Toute la partie PDF/A de cette bibliothèque, côté écriture comme côté vérification, vit en pur Pascal dansFPdfPdfa.pas et fonctionne par analyse au niveau des octets et par mise à jour incrémentielle. Donc, quand vous appelez le validateur, vous ne demandez rien au moteur de rendu de Chromium. Vous exécutez un analyseur de jetons Pascal sur les octets structurels du fichier
L'API publique est volontairement réduite. Une fonction lit un flux à partir de la position 0 et renvoie une structure :
function ValidatePdfACompliance(Source: TStream): TPdfAValidationResult;
type
TPdfAValidationResult = record
Conformance: TPdfAConformance; // pacUnknown, pacNone, pac1b, pac2u, ...
Issues: TPdfAValidationIssues; // a set of TPdfAValidationIssue
function IsCompliant: Boolean; // True only when level <> unknown/none
end; // AND Issues is empty
IsCompliant encode la règle qui compte dans une passerelle : un fichier passe seulement quand un vrai niveau de conformité a été détecté et que l'ensemble des problèmes est vide. Une analyse réussie mais sans marqueur pdfaid se résout enpacNone, ce qui n'est explicitement pas un passage. C'est le même point que celui que la CLI de rapport de prévalidation par lot exprime de l'extérieur : une liste de résultats vide sur un fichier non reconnu n'est pas un certificat de bonne santé
Retirer les corps de flux avant toute analyse de jetons
Voici le détail d'implémentation le plus important, et celui qu'il est le plus facile de rater si vous écrivez votre propre analyseur. Le détecteur trouve les violations en cherchant des jetons de nom délimités, des éléments comme/JavaScript, /LZWDecode, /BM. Si vous analysez les octets bruts du fichier, les corps binaires des flux intégrés, les images compressées, les profils ICC, les programmes de police, contiendront au hasard des séquences d'octets qui ressemblent à ces jetons. Vous allez signaler/AA ou/3D "trouvés" parce que trois octets à l'intérieur d'un JPEG les ont épelés par hasard. C'est une usine à faux positifs
La solution estPdfStructureBytes: elle parcourt le fichier et remplace par des espaces les octets compris entre chaquestream etendstream mot-clé, tout en laissant intacte la structure du dictionnaire. Ce n'est qu'ensuite que l'analyse s'exécute. Chaque vérification de jeton de nom dans le validateur opère sur cette copie dépouillée. Si vous ne retenez qu'une idée de cet article, retenez celle-là. La même discipline est reprise dans le validateur PDF/UA, qui garde sa propre copie de la routine parce que les deux normes évoluent indépendamment
Les 29 problèmes et ce que chacun signifie
TPdfAValidationIssue est un contrat documenté. Les ordinaux sont figés parce que les tests DUnitX, les démos et la couche de rapport en dépendent, donc les nouveaux constats ne sont ajoutés qu'à la fin. À partir de v1.63.0, il y a 29 membres. Ils se répartissent en quelques familles :
- Métadonnées et identité:
pvaiMissingXmpMetadata,pvaiMissingPdfAIdentifier,pvaiMissingTrailerId(ISO 19005-1 6.1.3),pvaiMissingXmpDates - Couleur et sortie:
pvaiMissingOutputIntent,pvaiMissingIccProfile, etpvaiMixedDeviceColorSpaceslorsque DeviceRGB et DeviceCMYK apparaissent tous les deux (6.2.3.3) - Interdictions absolues pour chaque partie:
pvaiEncryptionPresent(un/Encryptdictionnaire est purement et simplement interdit),pvaiJavaScriptPresent,pvaiForbiddenAction,pvaiAdditionalActions,pvaiLzwUsed,pvaiXfaPresent,pvaiNeedAppearancesTrue,pvaiForbiddenAnnotation - Polices:
pvaiFontNotEmbeddedet la version plus strictepvaiUnembeddedFont, pluspvaiUnicodeMappingMissingpour une revendication de niveau U sans/ToUnicode - Balisage:
pvaiLevelAStructureMissinglorsqu'une revendication de conformité=A ne comporte aucune structure balisée
Les six membres les plus récents, ajoutés aux ordinaux 24 à 29, couvrent les cas subtils dans lesquels les relecteurs trébuchent réellement : pvaiTrappedTrue (un /Trapped /True dans le dictionnaire Info, un « faux ami » puisque la valeur doit être False ou Unknown), pvaiForbiddenActionSubtype (Sound ou Movie utilisé comme action, et pas seulement comme annotation), pvaiTransparentColorSpace (un mode de fusion non Normal ou un /CA//ca différent de 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont, et pvaiMixedDeviceColorSpaces
Filtrage tenant compte de la partie : A-1 est strict, A-2 et A-3 assouplissent
PDF/A n'est pas un seul livre de règles. Trois choses interdites par PDF/A-1 sont explicitement autorisées à partir de PDF/A-2 : la transparence (un /Transparencygroupe ou un masque actif /SMask, 6.4), le contenu optionnel (/OCProperties, 6.1.13), et les fichiers incorporés (/EmbeddedFiles ou /EF, 6.1.11). Un validateur naïf qui signale les trois pour chaque fichier rejetterait en masse des documents PDF/A-2 parfaitement valides
Le validateur lit donc le numéro de partie à partir du marqueur pdfaid via PdfAPartOf et place ces contrôles derrière PartNo = 1. Les vérifications du mode de fusion et de l'alpha d'annotation pour les nouveaux problèmes de transparence ne s'appliquent elles aussi qu'à la partie 1 :
if PartNo = 1 then
begin
if PdfHasName(Struct, '/BM') then
if not PdfHasBMNormal(Struct) then // only /Normal or /Compatible allowed
Include(Result.Issues, pvaiTransparentColorSpace);
if PdfHasCaNotOne(Struct, '/CA') or PdfHasCaNotOne(Struct, '/ca') then
Include(Result.Issues, pvaiTransparentColorSpace);
end;
Une valeur par défaut prudente mérite d'être mentionnée : lorsqu'il n'y a aucun marqueur pdfaid, la partie est traitée comme 1, donc la plus stricte. Le raisonnement est qu'un fichier non identifié doit être soumis aux règles les plus serrées plutôt que laissé passer. JavaScript, les actions interdites, LZW, XFA, NeedAppearances, les annotations interdites et les polices non intégrées restent interdits pour chaque partie, donc ces contrôles ne passent jamais derrière la porte
Étendre les flux d'objets pour que rien ne se cache
PDF 1.5 a introduit le flux de références croisées et le flux d'objets (/Type /ObjStm), et ils créent un angle mort pour un analyseur d'octets naïf. Un catalogue, un OutputIntent, un dictionnaire d'action, tout ce qui n'est pas lui-même un flux, peut être compressé en Flate à l'intérieur d'un ObjStm. Analysez la structure brute et vous n'en verrez rien, puis vous déclarerez propre un fichier qui ne l'est pas du tout
PdfExpandObjectStreams comble cette lacune. Avant tout contrôle, le validateur fait Data := PdfExpandObjectStreams(Data). La routine trouve chaque ObjStm, lit son /N et /First d'en-tête pour récupérer les numéros et les décalages des objets contenus, décompresse le corps avec PdfInflate (la zlib du RTL, System.ZLib sous Delphi et zstream sous FPC), puis ajoute chaque objet contenu comme un N 0 obj ... endobj ordinaire à la fin d'une copie des octets. Les vérifications de jetons existantes retrouvent alors ces objets sans aucun changement de logique
Deux contraintes rendent cela propre plutôt que fragile. Les objets de flux, les métadonnées, le profil ICC et les programmes de police ne peuvent pas vivre dans un flux d'objets, seuls les dictionnaires sans flux le peuvent, donc l'expansion ne traite jamais que des dictionnaires et les objets ajoutés ne portent aucun stream pour perturber la passe de retrait du corps des flux. Et comme le contenu ajouté arrive après %%EOF, la recherche inverse depuis startxref retrouve toujours le trailer d'origine. Le trailer du flux de références croisées lui-même avait déjà été pris en charge plus tôt, en v1.49.3, en lisant Root, Size et ID directement depuis le dictionnaire texte du xref-stream, un sujet exploré dans l'article compagnon sur validation des objets et des flux de références croisées; le travail sur les flux d'objets n'avait qu'à ajouter l'étape de décompression, sans avoir besoin de décoder les entrées xref de type 2 ni de dénouer un prédicteur PNG
Les limites honnêtes d'un vérificateur au niveau des octets
C'est un outil de prévalidation, pas un validateur certifié, et ces limites sont réelles. L'intégration des polices est une heuristique de comptage, et la corriger a demandé un correctif qu'il vaut la peine de connaître. Le contrôle d'origine utilisait PdfCountName('/FontDescriptor'), mais chaque police contribue avec deux /FontDescriptor jetons, une référence depuis le dictionnaire de police et une /Type dans l'objet descripteur lui-même, si bien que le comptage donnait 2N contre N programmes intégrés et que le test était toujours vrai. La correction est PdfCountDescriptorRefs, qui ne compte que la forme de référence /FontDescriptor N G R, une par police, et déclenche pvaiUnembeddedFont seulement lorsque les programmes intégrés sont réellement moins nombreux:
K := PdfCountDescriptorRefs(Struct); // one per font dict
Emb := PdfCountName(Struct, '/FontFile')
+ PdfCountName(Struct, '/FontFile2')
+ PdfCountName(Struct, '/FontFile3');
if (K > 0) and (Emb < K) then
Include(Result.Issues, pvaiUnembeddedFont);
Même corrigé, cela reste grossier : un document mixte où chaque descripteur possède par hasard un FontFile peut encore laisser passer une police individuelle non conforme. L'expansion des flux d'objets a aussi un effet secondaire connu : elle expose les ressources par défaut standard-14 qu'un AcroForm /DR transporte, comme /Helv, et l'heuristique les signale loyalement comme non intégrées, alors que veraPDF les laisse passer parce qu'elles ne sont jamais réellement utilisées pour le rendu. Les vérifications au niveau des opérateurs de flux de contenu (6.2.10) sont entièrement hors périmètre, puisqu'elles exigeraient une analyse complète du contenu plutôt qu'un simple scan d'octets. Traitez le validateur comme une première porte rapide et sans dépendance qui attrape les violations que l'injection de marqueur ne peut pas corriger, et réservez un validateur complet à la certification finale
Voici le versant de vérification de l'histoire. Le versant d'écriture complémentaire, où SaveAsPdfA injecte le XMP, l'OutputIntent et le profil ICC sRGB, et rétrograde honnêtement une demande de niveau A qui n'a aucune structure balisée, repose sur la même mécanique au niveau des octets. Les deux moitiés sont livrées dans le PDFium Component for Delphi, un seul package VCL au-dessus d'une implémentation PDF/A en pur Pascal, sans aucun runtime externe à installer