Article technique

PDF/A Archival Compliance in Delphi with PDFium VCL

Vous livrez un convertisseur qui marque chaque fichier en PDF/A-1b, le système d'archives du client les ingère pendant un an, puis un audit passe tout le lot dans veraPDF et un tiers revient non conforme. Rien n'a planté, aucune exception n'a été levée, les fichiers s'ouvrent sans problème dans chaque visionneuse de votre bureau. Ils n'étaient simplement pas le standard que vous leur aviez attribué. C'est le mode d'échec normal du PDF d'archivage, et c'est pourquoi « nous avons posé le drapeau » n'est jamais la même affirmation que « ça valide »

La première chose à comprendre à propos de PDFium et de PDF/A, c'est que le moteur n'y est pour rien. PDFium rend, analyse et écrit des PDF, mais son interface publique n'a pas de ConvertToPDFA, ni de générateur d'OutputIntent, ni d'API XMP. Chaque partie de la conformité d'archivage, le paquet XMP, l'OutputIntent et son profil ICC, les marqueurs du catalogue, la validation, vit dans PDFiumPas lui-même, dans une unité pure Pascal d'environ 2 000 lignes (FPdfPdfa.pas) qui analyse les octets sauvegardés et les réécrit via une mise à jour incrémentale. Savoir où le travail se fait vous indique où se cachent les bogues, et ils ne se cachent pas dans PDFium

Ce que PDF/A exige vraiment, et où ça fait mal

PDF/A n'est pas un format unique. L'ISO 19005 définit trois parties (PDF/A-1, -2, -3) et, dans chacune, des niveaux de conformité qui promettent des choses différentes. Le niveau B (de base) garantit seulement que l'apparence visuelle est reproductible. Le niveau A (accessible) ajoute une arborescence de structure balisée et le mappage Unicode par-dessus B. Le niveau U, qui n'existe que pour les parties 2 et 3, se situe entre les deux : du texte Unicode fiable sans l'arborescence complète. L'ISO 19005-1 n'a pas de niveau U, une contrainte que la bibliothèque encode directement

Quelques règles du format sont celles qui mordent réellement en pratique. Le chiffrement est interdit sans ambiguïté (ISO 19005-1 §6.1.3 et ses successeurs) : un fichier PDF/A ne peut pas contenir un /Encrypt dictionnaire. Le document doit déclarer une condition de rendu de sortie via un OutputIntent dont la destination est un profil ICC valide (§6.2.3.2). La revendication de conformité elle-même doit apparaître comme métadonnées XMP sous le schéma d'identification PDF/A. Le niveau A exige en plus la structure logique de §6.8, l'arbre de balises qui rend le document lisible par machine. Omettez l'un de ces éléments et un vérificateur de conformité rejette le fichier même s'il s'affiche parfaitement

L'unique appel qui produit une archive

PDFiumPas expose tout le pipeline derrière TPdf.SaveAsPdfA. La surcharge simple prend une conformité cible et retient par défaut PDF/A-1b, ce qui est le bon choix par défaut pour le cas courant « rendre ceci lisible à vie »

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    // Default conformance is pac1b (PDF/A-1b)
    if Pdf.SaveAsPdfA('invoice_archive.pdf') then
      // file now carries XMP, sRGB OutputIntent, and catalog markers
    else
      raise Exception.Create('PDF/A save failed');
  finally
    Pdf.Free;
  end;
end;

Sous le capot, c'est une opération en deux temps. SaveAsPdfA commence par demander à PDFium de sérialiser le document avec FPDF_SaveAsCopy, puis confie ce flux d'octets à InjectPdfAMarkers, qui ajoute les métadonnées XMP, l'OutputIntent sRGB avec son profil ICC intégré, et un catalogue réécrit sous forme de mise à jour incrémentale. La source est lue à partir de la position zéro et la destination est écrite à partir de la position zéro ; l'arborescence d'objets d'origine reste intacte et les marqueurs arrivent après le %%EOF. Si vous avez besoin des octets plutôt que d'un fichier, SaveAsPdfAToStream prend un TStream et les mêmes options

Choisir la conformité avec l'enregistrement d'options

Pour cibler une partie et un niveau précis, passez un TPdfASaveOptions enregistrement. Son Conformance champ prend une TPdfAConformance valeur. L'énumération couvre toutes les combinaisons valides et rien d'autre : pac1b, pac1a pour la partie 1; pac2b, pac2u, pac2a pour la partie 2; pac3b, pac3u, pac3a pour la partie 3, plus pacUnknown et pacNone pour le côté validation. Il n'existe pas de pac1u, parce que ce niveau n'existe pas dans la norme

var
  Pdf: TPdf;
  Opts: TPdfASaveOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('report.pdf');
    Opts := TPdfASaveOptions.Default;
    Opts.Conformance := pac2u;           // PDF/A-2u: reliable Unicode text
    Opts.Title := 'Quarterly Report 2026';
    Opts.Author := 'Finance';
    // Leave IccProfileData empty to use the built-in sRGB IEC61966-2.1 profile
    if not Pdf.SaveAsPdfA('report_a2u.pdf', Opts) then
      raise Exception.Create('PDF/A-2u save failed');
  finally
    Pdf.Free;
  end;
end;

La plupart de l'enregistrement peut rester vide. Laissez Title, Author, Subject, Keywords, Creator, and Producer vides et SaveAsPdfA les complète automatiquement à partir du dictionnaire Info du document via FPDF_GetMetaText. Laissez CreationDate et ModDate vides et il utilise l'heure UTC courante pour les deux dates XMP. Laissez DocumentId et InstanceId vides et la bibliothèque les préremplit à partir de FPDF_GetFileIdentifier, en retombant sur un ID déterministe dérivé des octets sources. Le seul champ que vous voudrez peut-être remplacer volontairement est IccProfileData : vide signifie le profil sRGB IEC61966-2.1 fourni, mais un flux de travail CMJN ou niveaux de gris devrait fournir le sien

Pourquoi le niveau A se dégrade, et pourquoi c'est le choix honnête

Voici une subtilité qui piège ceux qui pensent qu'un drapeau est une garantie. Vous pouvez demander pac1a sur un document qui n'a aucune arborescence de balises, mais PDF/A-1a exige la structure logique de §6.8, et la bibliothèque ne peut pas fabriquer une arborescence de structure à partir d'un PDF non balisé. Plutôt que d'émettre un fichier qui prétend être au niveau A tout en l'échouant, SaveAsPdfA vérifie la présence d'une vraie structure balisée (/StructTreeRoot plus /MarkInfo avec /Marked true) et, si elle est absente, rabaisse la revendication : pac1a devient pac1b, pac2a devient pac2b, et ainsi de suite pour les trois parties. Les assistants internes sont PdfAIsLevelA et PdfADowngradeToLevelB

La logique mérite d'être dite clairement : un fichier qui déclare honnêtement le niveau qu'il atteint est plus utile qu'un fichier qui ment sur un niveau qu'il n'atteint pas. Le niveau U est traité différemment. Détecter une véritable couverture Unicode voudrait dire un test naïf du type "a-t-il /ToUnicode" qui sur-dégrade des documents légitimes (WinAnsi et les encodages similaires sont exemptés), donc le côté sauvegarde émet la revendication U telle que l'appelant l'a déclarée et laisse l'écart être signalé du côté validation à la place. Si vous avez besoin d'une archive Level A garantie, balisez le document avant de le convertir ; le convertisseur n'inventera pas une structure qui n'est pas là

Le piège ICC que seul un vrai validateur détecte

C'est l'échec qui a donné la leçon la plus dure, parce que le vérificateur intégré de la bibliothèque le passait alors que veraPDF, le validateur de référence ISO 19005, le rejetait. PDF/A exige que le profil de destination de l'OutputIntent soit un flux ICCBased valide, et §6.2.3.2 demande à un vérificateur de valider ce flux comme un espace colorimétrique. Un flux ICCBased doit déclarer /N, le nombre de composantes de couleur. Une première version de l'injecteur écrivait le dictionnaire du flux ICC avec seulement /Length et aucun /N, et veraPDF rejetait le résultat avec "The N entry (value null)... is missing"

Ce qui rendait le problème pernicieux, c'est que le rejet ne se produisait que pour PDF/A-1b et -1a. Les modèles de conformité des parties 2 et 3 ne lançaient pas ce contrôle particulier sur le profil de destination, si bien que la structure injectée identique se validait sous pac2b, pac3b et pac2u mais échouait sous pac1b sur rien de plus que la valeur pdfaid:part. Un test unitaire n'aurait jamais pu le voir, parce que la vérification intégrée de la bibliothèque ValidatePdfACompliance ne vérifiait que le /DestOutputProfile existait, pas ce qu'il y avait à l'intérieur du dictionnaire de flux. Les tests internes sont restés au vert ; la validation archivage réelle a échoué

La correction est IccComponentCount, qui lit la signature de l'espace colorimétrique des données à l'offset 16 de l'en-tête ICC et la mappe à un nombre de composantes : GRAY est 1, RGB , Lab , et XYZ sont 3, CMYK est 4, avec un profil inconnu qui retombe sur 3. Ce nombre entre dans le dictionnaire du flux comme /N. Il est calculé, pas codé en dur à 3, afin qu'un appelant qui fournit un profil CMJN ou niveaux de gris via IccProfileData obtienne encore la bonne valeur. La leçon plus large est méthodologique : le vérificateur intégré et un validateur faisant autorité ont chacun des angles morts, et la sortie PDF/A doit être testée de bout en bout contre une implémentation de référence comme veraPDF plutôt que d'être confiée aux auto-vérifications. La même discipline de mise à jour incrémentale derrière les archives propres est couverte dans la validation des flux d'objets compressés et des flux xref, ce qui importe parce que les PDF modernes que l'injecteur consomme sont souvent construits sur des flux de références croisées

Chiffrement, flux xref et autres cas limites

Comme l'ISO 19005 interdit le chiffrement, le chemin de sauvegarde le retire avant l'écriture. SaveAsPdfA applique FPDF_REMOVE_SECURITY lors de la sérialisation, donc une source chiffrée (chargée avec son mot de passe) est déchiffrée en route vers l'archive. Sur un document non chiffré, c'est sans effet et cela ne change rien. La conséquence est la même contrainte que HotPDF impose dans l'autre sens : un seul fichier ne peut pas être à la fois chiffré et PDF/A. Quand un flux de travail a besoin des deux, la réponse est deux artefacts, une copie chiffrée pour la distribution et une copie propre distincte pour l'archive

Un autre cas limite reste invisible jusqu'à ce qu'il morde : les documents PDF 1.5+ qui utilisent un flux de références croisées pur et ne portent aucun mot-clé trailer. L'injecteur lit le trailer pour trouver la source /Info et ajouter sa mise à jour incrémentale, et il doit accepter la forme de flux xref, sinon un tel document serait recopié avec les marqueurs silencieusement supprimés. L'ISO 32000-1 §7.5.6 autorise explicitement une mise à jour incrémentale classique du trailer à suivre un document à flux xref, avec /Prev pointant vers l'offset du flux xref, ce qui est exactement la structure que l'injecteur émet. Le FPDF_SaveAsCopy de PDFium écrit toujours un trailer classique, donc dans le pipeline normal l'injecteur ne rencontre jamais une source purement en flux xref, mais le chemin de lecture l'accepte pour les documents qui arrivent d'ailleurs

Vérifier avant de croire la revendication

La bibliothèque fournit un vérificateur au niveau des octets, TPdf.ValidatePdfA, qui renvoie un TPdfAValidationResult. Son Conformance champ signale le niveau détecté et Issues est un ensemble de TPdfAValidationIssue valeurs ; la méthode pratique IsCompliant est vraie seulement lorsqu'un vrai niveau a été détecté et que l'ensemble des problèmes est vide. Exécutez-la comme premier filtre rapide dans un lot

var
  Pdf: TPdf;
  Res: TPdfAValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('invoice_archive.pdf');
    Res := Pdf.ValidatePdfA;
    if Res.IsCompliant then
      Writeln('Conformant: detected level ', Ord(Res.Conformance))
    else
      Writeln('Issues found: ', SizeOf(Res.Issues), ' flags set');
  finally
    Pdf.Free;
  end;
end;

Soyez honnête sur ce que cela vous apporte. Le vérificateur au niveau des octets détecte les problèmes structurels (un OutputIntent manquant, une action interdite, un /Encrypt présent, la transparence là où la partie 1 l'interdit) avec une forte confiance, et la détection d'intégration des polices utilise une heuristique de comptage qui ne rapporte volontairement qu'un signal à forte confiance plutôt que de courir après la couverture glyphe par glyphe. Ce qu'il ne fait pas, c'est l'analyse des opérateurs du flux de contenu, qui exigerait un analyseur de contenu complet et sort du périmètre par conception. Pour un garde-fou de publication, associez le vérificateur intégré à veraPDF : le vérificateur est instantané et fonctionne partout sans DLL, veraPDF fait autorité. Brancher cette paire dans un traitement par lot fait l'objet du CLI du rapport de précontrôle par lot, qui est l'endroit où cette validation doit vivre dans un vrai flux de travail d'archivage

Les SaveAsPdfA, InjectPdfAMarkers et ValidatePdfA API présentées ici sont livrées avec PDFium Component pour Delphi, C++Builder et Lazarus/FPC. La page produit renvoie vers la référence complète de l'API, y compris l'énumération complète des conformités et l'enregistrement d'options derrière ces exemples