Votre code de détection rapporte qu'un fichier PDF/UA d'un autre producteur ne revendique rien, et un balayage PDF/R cesse d'être PDF/R dès qu'un éditeur appose une note adhésive sur le fichier. HotPDF lit ces revendications depuis un document chargé via des accesseurs tels que IsLoadedPDFUA, GetLoadedPDFR1Version et GetLoadedPDFDeclarations, dont chacun regarde là où sa propre norme range l'identifiant
Où chaque norme PDF écrit son identifiant
Les normes subsidiaires n'ont jamais convenu d'un endroit unique pour la revendication. La plupart utilisent des propriétés XMP dans le flux /Metadata du catalogue, quelques-unes plus anciennes comptent toujours sur des clés dans le dictionnaire d'informations du document, PDF/R met une ligne de commentaire juste à côté du trailer du fichier, et la génération PDF 2.0 ajoute deux mécanismes de plus : des dictionnaires d'extensions de développeur dans le catalogue et des PDF Declarations à l'intérieur du XMP. Un identifiant est la déclaration que le fichier fait de lui-même ; de savoir si le contenu est à la hauteur, c'est l'affaire d'un validateur, et produire une sortie PDF/A, PDF/X et PDF/UA conforme est un sujet à part entière. Pour le routage (file d'impression pour le PDF/X, archive pour le PDF/A, pipeline d'accessibilité pour le WTPDF), la revendication est ce dont vous avez besoin, lue au bon endroit :
| Norme | Où vit la revendication | Accesseur HotPDF |
|---|---|---|
| PDF/A (ISO 19005) | XMP pdfaid:part et pdfaid:conformance, plus pdfaid:rev en PDF/A-4 | IsLoadedPDFA, GetLoadedPDFAVersion, GetLoadedPDFARevision |
| PDF/X (ISO 15930) | GTS_PDFXVersion dans le dictionnaire Info pour PDF/X-1a et PDF/X-3, en XMP pour les parties ultérieures | IsLoadedPDFX, GetLoadedPDFXVersion |
| PDF/UA (ISO 14289) | XMP pdfuaid:part | IsLoadedPDFUA, GetLoadedPDFUAVersion |
| PDF/VT (ISO 16612-2) | XMP pdfvtid:GTS_PDFVTVersion | IsLoadedPDFVT, GetLoadedPDFVTVersion |
| PDF/E (ISO 24517-1) | ISO_PDFEVersion, lu dans le dictionnaire Info, le catalogue ou le XMP | IsLoadedPDFE, GetLoadedPDFEVersion |
| PDF/R-1 (ISO 23504-1) | Commentaire %PDF-raster-x.y juste au-dessus du dernier startxref | IsLoadedPDFR1, GetLoadedPDFR1Version |
| PDF/VCR-1 (ISO 16613-1) | GTS_PDFVCRVersion dans le catalogue ou le XMP | IsLoadedPDFVCR, GetLoadedPDFVCRVersion |
| WTPDF 1.0, ISO/TS 32005 | Entrées pdfd:declarations dans le XMP | IsLoadedWTPDF, GetLoadedWTPDFVersion, IsLoadedISO32005 |
| Extensions de développeur PDF 2.0 | /Extensions /ISO_ dans le catalogue | HasLoadedISODeveloperExtension, GetLoadedISODeveloperExtensionLevels |
uses
System.SysUtils, HPDFDoc;
procedure ReportClaims(const FileName: string; const Password: AnsiString);
var
Pdf: THotPDF;
PDFX, PDFXLevel: AnsiString;
begin
Pdf := THotPDF.Create(nil); // une instance dédiée, réservée à l'inspection
try
// La surcharge à mot de passe déchiffre les flux, paquet XMP compris ;
// passez '' pour les fichiers qui s'ouvrent sans invite
if Pdf.LoadFromFile(FileName, Password) <= 0 then
raise Exception.CreateFmt('Cannot open %s', [FileName]);
if Pdf.IsLoadedPDFA then
Writeln('PDF/A: ', Pdf.GetLoadedPDFAVersion); // p. ex. PDF/A-2b
PDFX := Pdf.GetLoadedPDFXVersion; // XMP, puis Info
if PDFX <> '' then
begin
PDFXLevel := Pdf.GetLoadedInfoKeyValue('GTS_PDFXConformance');
if PDFXLevel <> '' then
Writeln('PDF/X: ', PDFX, ', conformance ', PDFXLevel)
else
Writeln('PDF/X: ', PDFX);
end;
if Pdf.IsLoadedPDFUA then
Writeln('PDF/UA: ', Pdf.GetLoadedPDFUAVersion); // p. ex. PDF/UA-1
if Pdf.IsLoadedPDFVT then
Writeln('PDF/VT: ', Pdf.GetLoadedPDFVTVersion);
if Pdf.IsLoadedPDFE then
Writeln('PDF/E: ', Pdf.GetLoadedPDFEVersion);
if Pdf.IsLoadedPDFR1 then
Writeln('PDF/R: ', Pdf.GetLoadedPDFR1Version); // p. ex. PDF/R-1.0
if Pdf.IsLoadedWTPDF then
Writeln('WTPDF: ', Pdf.GetLoadedWTPDFVersion);
finally
Pdf.Free;
end;
end;
Utilisez une instance THotPDF séparée pour l'inspection, comme le fait l'exemple. Les fonctions IsLoaded… répondent pour le document que le composant détient, y compris celui qu'il est en train de construire : IsLoadedPDFUA vaut aussi True quand PDFUACompliance a été activé pour la sortie, et IsLoadedPDFX concorde avec un PDFXCompliance non vide. Pratique en pleine construction, mais sur un composant réutilisé pour les deux métiers, cela mêle vos réglages de sortie au verdict portant sur le fichier de quelqu'un d'autre
PDF/X est la seule famille dont l'identifiant a déménagé entre les parties. PDF/X-1a et PDF/X-3 sont des profils PDF 1.3 qui mettent GTS_PDFXVersion dans le dictionnaire d'informations du document et n'ont pas besoin de XMP du tout, tandis que PDF/X-4 et les parties ultérieures exigent la propriété XMP pdfx:GTS_PDFXVersion. GetLoadedPDFXVersion regarde d'abord dans le XMP, sous le préfixe pdfx ou pdfxid, puis dans le dictionnaire Info, puis dans les clés de catalogue que certains producteurs anciens écrivent, et renvoie la valeur déclarée telle quelle. PDF/X-1a a une bizarrerie qui mérite d'être codée : sa valeur de version est PDF/X-1:2001, et le niveau de conformité PDF/X-1a:2001 vit dans une clé à part, /GTS_PDFXConformance, que l'exemple lit avec GetLoadedInfoKeyValue quand il faut distinguer X-1a du X-1 simple
À quoi ressemble une revendication XMP valide ?
Le XMP est du RDF sérialisé en XML, et RDF/XML permet d'écrire une propriété simple de deux manières à l'intérieur d'un rdf:Description : comme élément enfant, <pdfuaid:part>1</pdfuaid:part>, ou comme attribut de la description elle-même, pdfuaid:part="1", entre guillemets simples ou doubles. L'ISO 16684-1 accepte les deux, et un paquet peut répartir ses propriétés sur plusieurs éléments rdf:Description. La répartition n'est pas hypothétique. HotPDF écrit la forme élément enfant pour PDF/UA, PDF/VT, PDF/E et PDF/X, tandis que le corpus de test veraPDF et beaucoup de producteurs externes écrivent la forme abrégée, avec des descriptions telles que pdfuaid:part="1" ou pdfvtid:GTS_PDFVTVersion="PDF/VT-1" portant la revendication entière en attributs. Un détecteur bâti autour de Pos('<pdfuaid:part>', Xmp) et exercé seulement sur la production maison passe tous les contrôles locaux puis déclare non revendiqué le premier échantillon veraPDF. Le lecteur XMP de HotPDF gère les deux formes pour chaque accesseur du tableau. GetLoadedPDFAVersion combine pdfaid:part et pdfaid:conformance en identifiants tels que PDF/A-1b, PDF/A-2u, PDF/A-4 ou PDF/A-4f, et GetLoadedPDFARevision renvoie pdfaid:rev, l'année d'édition que PDF/A-4 exige (2020) et que les parties 1 à 3 ne définissent pas :
procedure ReportPDFALevel(Pdf: THotPDF);
var
Level, Revision: AnsiString;
begin
Level := Pdf.GetLoadedPDFAVersion; // 'PDF/A-1b', 'PDF/A-2u', 'PDF/A-4f', ...
if Level = '' then
Exit;
Revision := Pdf.GetLoadedPDFARevision; // '2020' pour PDF/A-4, vide pour les parties 1-3
if Revision <> '' then
Writeln(Level, ', edition ', Revision)
else
Writeln(Level);
// Attendu : "PDF/A-2b" que le producteur ait écrit
// <pdfaid:part>2</pdfaid:part> ou pdfaid:part="2"
end;
Les préfixes sont l'autre pièce mobile. En XML le préfixe n'est qu'un alias local ; ce qui identifie le schéma PDF/A est l'URI d'espace de noms http://www.aiim.org/pdfa/ns/id/, que des errata approuvés de l'ISO 19005-4 admettent aussi dans son orthographe https://, et un producteur peut le lier au préfixe qui lui plaît. GetLoadedPDFAVersion cherche le préfixe que le paquet déclare pour cet URI avant de lire la partie et le niveau, si bien qu'un paquet qui écrit pdfa:part au lieu de pdfaid:part s'identifie quand même. Toute autre propriété XMP que vous lisez vous-même depuis le paquet décodé que renvoie GetLoadedXMPMetadata mérite le même soin : les deux sérialisations, le préfixe résolu depuis sa déclaration xmlns, et les blancs admis autour du signe égal
Les fichiers chiffrés cachent la revendication aux balayages d'octets
Un balayage du fichier brut à la recherche de pdfaid ou d'ISO_PDFEVersion saute l'analyse et casse sur deux cas ordinaires. La plupart des flux XMP sont compressés en Flate, si bien que les noms de propriétés n'apparaissent jamais comme texte sur disque. Et dans un document chiffré le flux de métadonnées est du texte chiffré : l'ISO 32000-2 §7.6.4.2 définit EncryptMetadata avec true par défaut, si bien que, sauf désactivation explicite du producteur, le paquet est chiffré avec tous les autres flux. Un balayage d'octets sur un fichier PDF/E-1 chiffré rapporte donc la revendication ISO_PDFEVersion comme absente alors qu'elle est là. L'ordre fiable est charger, déchiffrer, décoder, puis chercher. Ouvrez le fichier par la surcharge à mot de passe, LoadFromFile(FileName, Password), et passez une chaîne vide pour les fichiers qui ne portent qu'un mot de passe propriétaire et s'ouvrent dans n'importe quel visionneur sans invite. Le LoadFromFile(FileName) à un seul argument charge la structure du document sans déchiffrer ses flux, ce qui laisse le paquet XMP illisible ; après la surcharge à mot de passe, GetLoadedXMPMetadata et tous les accesseurs ci-dessus travaillent sur le paquet déchiffré et décompressé
Pourquoi un fichier PDF/R perd-il son identité après une édition ?
PDF/R-1 s'identifie d'une manière qu'aucune autre norme subsidiaire ne fait. La clause 5 de l'ISO 23504-1 exige une ligne de commentaire lisant %PDF-raster-1.0 (ou la version applicable) sur la ligne immédiatement au-dessus de la dernière ligne portant startxref. Un analyseur PDF jette les commentaires pendant qu'il lit les données de références croisées, si bien qu'un modèle d'objets n'a rien à rapporter ; la preuve n'existe que dans les octets bruts à la fin du fichier. GetLoadedPDFR1Version retourne à la source depuis laquelle le document a été chargé, lit au plus les 64 derniers KiB, trouve le dernier startxref et vérifie la ligne au-dessus. Suivez maintenant une mise à jour incrémentale. Un éditeur qui ajoute une annotation appose de nouveaux objets, une nouvelle section de références croisées, un nouveau trailer et un nouveau startxref après le %%EOF d'origine. La ligne %PDF-raster-1.0 est toujours dans le fichier, mais elle siège désormais au-dessus de l'avant-dernier startxref, et la norme est sans ambiguïté sur celui qui compte. Attendu : IsLoadedPDFR1 renvoie False après l'édition, sauf si l'outil d'édition réémet le commentaire dans sa propre mise à jour. Un détecteur qui balaye le fichier entier à la recherche de %PDF-raster- répondrait oui, et au vu de la règle de la clause 5 cette réponse est fausse. Un flux de capture qui doit garder ses fichiers PDF/R place les annotations dans un fichier sidecar ou dans un outil qui réécrit le fichier entier en PDF/R
Extensions de développeur : niveaux entiers, noms et tableaux
Les documents PDF 2.0 annoncent les spécifications techniques ISO dont ils dépendent (hachages SHA-3 de l'ISO/TS 32001, signatures ECC de l'ISO/TS 32002, actifs glTF de l'ISO/TS 32007) par le dictionnaire /Extensions du catalogue, défini en ISO 32000-2 §7.12. Sous le préfixe enregistré /ISO_, le tableau 48 autorise soit un unique dictionnaire d'extensions de développeur soit, nouveauté de PDF 2.0, un tableau de tels dictionnaires, et le tableau 49 avec le §7.12.5 fait de /ExtensionLevel un entier. Un lecteur qui ne gère qu'une des deux formes de conteneur rate tout fichier écrit de l'autre manière. HasLoadedISODeveloperExtension et GetLoadedISODeveloperExtensionLevels acceptent les deux et résolvent les références indirectes en chemin, bien que le §7.12.1 dise que ces dictionnaires doivent être des objets directs :
procedure ReportISOExtensions(Pdf: THotPDF);
var
Levels: THPDFIntegerArray;
Revision, URL: AnsiString;
I: Integer;
begin
Levels := Pdf.GetLoadedISODeveloperExtensionLevels;
for I := 0 to High(Levels) do
Writeln('ISO_ extension level ', Levels[I]);
if Pdf.HasLoadedISODeveloperExtension(32001, Revision, URL) then
Writeln('ISO/TS 32001 revision ', Revision, ', documented at ', URL);
// Attendu pour un fichier écrit avec EnsureISO32001Extensions :
// ISO/TS 32001 revision :2022, documenté sur https://www.iso.org/standard/45874.html
if Pdf.HasLoadedISODeveloperExtension(32007) then
Writeln('glTF 3D assets per ISO/TS 32007');
end;
La première édition de l'ISO/TS 32007:2024 comporte en §4.8 un exemple qui écrit /ExtensionLevel /32007 comme nom ; l'issue #835 des PDF Errata l'a corrigé en l'entier 32007, mais un producteur qui a copié l'exemple original écrit encore le nom. Le lecteur de HotPDF retombe donc sur l'analyse d'une valeur nom ou chaîne, avec ou sans la barre oblique initiale, quand la recherche d'entier revient vide, si bien que HasLoadedISODeveloperExtension(32007) trouve les deux orthographes. L'autre erratum concerne /URL : le tableau 49 de l'ISO 32000-2:2020 liste /URL comme facultative, et l'issue #732 la promeut au rang de requise. Côté écriture, HotPDF émet la forme entière et un /URL dans chaque appel EnsureISO…Extensions ; côté lecture, un paramètre out URL vide est normal pour les fichiers écrits selon le texte de 2020, traitez-le donc comme une information et jamais comme un motif de rejet de l'extension
Quel URI WTPDF le producteur a-t-il écrit ?
Les revendications WTPDF 1.0 et ISO/TS 32005 sont des PDF Declarations : des entrées dans un sac pdfd:declarations en XMP, sous l'espace de noms http://pdfa.org/declarations/ défini par la note technique PDF Declarations de la PDF Association. Chaque entrée porte un URI conformsTo et des champs facultatifs claimBy, claimDate, claimCredentials et claimReport, que GetLoadedPDFDeclarations renvoie sous forme d'un tableau d'enregistrements THPDFDeclarationEntry ; côté écriture, AddPDFDeclaration met les mêmes cinq champs dans le paquet XMP, à côté des espaces de noms d'identification PDF 2.0, PDF/A-4 et PDF/UA-2. Couper le sac en entrées par une simple recherche de texte a son propre écueil : coupez à chaque occurrence de conformsTo et la balise fermante </pdfd:conformsTo> de la forme élément enfant compte comme le début d'une nouvelle entrée, ce qui tronque la première déclaration avant sa propre fin. Le lecteur de HotPDF enjambe les balises fermantes et accepte les deux sérialisations
Les URI eux-mêmes sont la partie la plus corsée. La même revendication vous parvient en http:// ou https://, et l'issue #395 de WTPDF 1.0 a changé si une / siège avant le #, si bien que les fichiers écrits avant et après l'erratum diffèrent sur ce seul caractère ; une comparaison de chaînes simple les prend pour des normes différentes. HasLoadedPDFDeclaration compare les URI sans tenir compte de la casse, rabat https:// sur http:// et supprime une / placée directement avant un # des deux côtés, si bien que la requête ci-dessous correspond à l'orthographe que le producteur a choisie. Le fragment reste intact : #accessibility1.0 et #accessibility1.0-validated sont des revendications différentes, la seconde affirmant qu'un validateur a vérifié le fichier, et vous devriez demander celle dont votre flux se soucie. GetLoadedWTPDFVersion ramasse les deux en 'WTPDF 1.0 Accessibility' ou 'WTPDF 1.0 Reuse' pour le routage
procedure ReportDeclarations(Pdf: THotPDF);
var
Decls: THPDFDeclarationEntryArray;
I: Integer;
begin
Decls := Pdf.GetLoadedPDFDeclarations;
for I := 0 to High(Decls) do
Writeln(Decls[I].ConformsTo, ' claimed by ', Decls[I].ClaimBy,
' on ', Decls[I].ClaimDate);
// Correspond à http ou https, avec ou sans la barre oblique avant '#'
if Pdf.HasLoadedPDFDeclaration(
'https://pdfa.org/declarations/wtpdf#accessibility1.0') then
Writeln('WTPDF 1.0 Accessibility (unvalidated claim)');
// Couvre les variantes simple et -validated de Reuse et Accessibility
Writeln('WTPDF profile: ', Pdf.GetLoadedWTPDFVersion);
end;
Pour un routeur de capture ou d'impression, l'ordre des opérations importe plus que n'importe quel accesseur isolé : testez IsLoadedPDFR1 sur le fichier tel qu'il est arrivé, avant qu'une étape d'annotation ou de tamponnage n'appose sa mise à jour, et routez le PDF/X sur GetLoadedPDFXVersion, qui couvre le dictionnaire Info des travaux PDF/X-1a et PDF/X-3 aussi bien que le XMP des travaux PDF/X-4, en lisant /GTS_PDFXConformance seulement là où il faut distinguer X-1a du X-1. Les deux contrôles tournent sur tout document chargé dans le HotPDF Delphi PDF component, y compris les fichiers chiffrés ouverts avec leur mot de passe