Article technique

Quelle norme ce PDF revendique-t-il ? Le détecter en Delphi

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 :

NormeOù vit la revendicationAccesseur HotPDF
PDF/A (ISO 19005)XMP pdfaid:part et pdfaid:conformance, plus pdfaid:rev en PDF/A-4IsLoadedPDFA, 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érieuresIsLoadedPDFX, GetLoadedPDFXVersion
PDF/UA (ISO 14289)XMP pdfuaid:partIsLoadedPDFUA, GetLoadedPDFUAVersion
PDF/VT (ISO 16612-2)XMP pdfvtid:GTS_PDFVTVersionIsLoadedPDFVT, GetLoadedPDFVTVersion
PDF/E (ISO 24517-1)ISO_PDFEVersion, lu dans le dictionnaire Info, le catalogue ou le XMPIsLoadedPDFE, GetLoadedPDFEVersion
PDF/R-1 (ISO 23504-1)Commentaire %PDF-raster-x.y juste au-dessus du dernier startxrefIsLoadedPDFR1, GetLoadedPDFR1Version
PDF/VCR-1 (ISO 16613-1)GTS_PDFVCRVersion dans le catalogue ou le XMPIsLoadedPDFVCR, GetLoadedPDFVCRVersion
WTPDF 1.0, ISO/TS 32005Entrées pdfd:declarations dans le XMPIsLoadedWTPDF, GetLoadedWTPDFVersion, IsLoadedISO32005
Extensions de développeur PDF 2.0/Extensions /ISO_ dans le catalogueHasLoadedISODeveloperExtension, 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

Où chaque norme PDF écrit son identifiant de conformité, et comment HotPDF pour Delphi fouille le XMP, le dictionnaire Info et les clés de catalogue dans un PDF chargé
Chaque norme range sa revendication là où sa spécification l'a choisi, et le lecteur fouille dans un ordre fixe les endroits entre lesquels une revendication peut déménager

À 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

Deux sérialisations XMP valides d'une même revendication PDF/UA, à côté de la règle d'espace de noms qui laisse un préfixe renommé identifier quand même le PDF/A pour HotPDF en Delphi
Les orthographes élément enfant et attribut sont également valides, et l'URI d'espace de noms identifie le schéma plutôt que le préfixe

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

Queues de fichier avant et après une mise à jour incrémentale : le commentaire PDF/R reste mais perd l'ancre du dernier startxref que HotPDF vérifie en Delphi
Un éditeur qui appose une mise à jour laisse le commentaire PDF/R au-dessus de l'avant-dernier startxref, là où la norme ne le compte plus

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