Article technique

Fichiers associés au niveau page en PDF 2.0 avec PDFlibPas

PDFlibPas rattache un fichier embarqué à une page précise plutôt qu'au document entier, en écrivant un tableau /AF dans le dictionnaire de page pendant que la charge utile elle-même reste enregistrée dans l'arbre de noms EmbeddedFiles du document. C'est cette séparation que décrit ISO 32000-2 §14.13, et c'est elle qui permet à un lecteur de répondre à la question à laquelle une pièce jointe de niveau document ne peut pas répondre : à quelle page ces données appartiennent-elles

Les cas d'usage sont plus spécifiques que les pièces jointes générales. Un rapport d'enquête où chaque page porte la série de mesures brutes derrière son graphique. Un lot numérisé où chaque page conserve le résultat OCR qui a produit sa couche de texte. Un jeu de plans où chaque feuille transporte l'extraction CAD depuis laquelle elle a été rendue. Dans chaque cas, une liste de pièces jointes de niveau document serait un tas de fichiers dont les noms encodent les numéros de page, ce qui est une convention plutôt qu'une structure

Une charge utile, deux endroits qui la référencent

Le point structurel important est que l'association au niveau page ne crée aucune seconde copie. Le fichier est embarqué une seule fois et enregistré dans l'arbre de noms EmbeddedFiles exactement comme une pièce jointe de niveau document, avec le même mécanisme de spécification de fichier. Ce qui change, c'est l'endroit où la référence et sa clé de relation sont écrites : dans le dictionnaire de page au lieu du catalogue du document

Deux conséquences en découlent. D'abord, un lecteur qui ne connaît que les pièces jointes de niveau document trouve quand même la charge utile, car elle se trouve dans l'arbre de noms où ce lecteur cherche. Ensuite, supprimer l'association de page retire la liaison, pas le fichier. ClearPageAssociatedFiles détache la page de ses fichiers associés et laisse les charges utiles accessibles via l'arbre de noms, ce qui est le comportement conservateur : une opération qui dit de supprimer l'association ne doit pas détruire silencieusement des données qu'une autre partie du document référence peut-être

Structure d'un fichier associé au niveau page dans un document PDF 2.0 écrit par PDFlibPas : la charge utile est embarquée une seule fois et enregistrée dans l'arbre de noms EmbeddedFiles sous le catalogue du document, tandis que le dictionnaire de page porte un tableau /AF référençant la même spécification de fichier avec une clé AFRelationship, si bien que ClearPageAssociatedFiles détache la liaison sans détruire les données
L'association au niveau page ajoute une seconde référence, pas une seconde copie : les lecteurs qui ne connaissent que les pièces jointes de niveau document trouvent quand même la charge utile dans l'arbre de noms, et la suppression de la liaison de page laisse le flux embarqué accessible

Cette fonction a une condition de succès délibérément étroite qu'il vaut la peine de connaître. Elle ne signale le succès que lorsque la page portait réellement une clé /AF. Une page qui n'a jamais eu d'associations renvoie un échec plutôt qu'une confirmation joyeuse, si bien qu'un appelant ne peut pas confondre une opération sans effet avec un nettoyage accompli

var
  Lib: TPDFlib;
  Idx, I: Integer;
begin
  Lib := TPDFlib.Create(nil);
  try
    Lib.LoadFromFile('survey-report.pdf');

    // Attacher la série de mesures qui a produit le graphique de la page 3
    Idx := Lib.AddPageAssociatedFileFromFile(3,
      'series-03.csv',            // fichier sur disque
      'measurements.csv',         // nom d'affichage dans le PDF
      'text/csv',                 // type MIME
      'Raw measurement series for figure 3',
      'Data');                    // AFRelationship, ISO 32000-2 14.13

    if Idx < 0 then
      raise Exception.Create('page association refused');

    for I := 0 to Lib.GetPageAssociatedFileCount(3) - 1 do
      Writeln('page 3 associated file, embedded index ',
        Lib.GetPageAssociatedFileEmbeddedIndex(3, I));

    Lib.SaveToFile('survey-report-with-data.pdf');
  finally
    Lib.Free;
  end;
end;

La chaîne de relation n'est pas du texte libre en pratique. ISO 32000-2 définit un vocabulaire, Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema et Unspecified, et les consommateurs s'y accrochent. Data pour les nombres derrière un graphique, Source pour le document depuis lequel une page a été générée, Alternative pour une représentation équivalente. Piochez dans le vocabulaire même si rien dans votre chaîne de traitement ne le lit encore, car le prochain outil de la chaîne le fera peut-être

Pourquoi une même recherche a-t-elle besoin de FollowRef dans les deux sens ?

Parce que le suivi des références répond à deux questions différentes, et le code doit savoir laquelle il pose. Une recherche par clé qui suit les références indirectes renvoie l'objet vers lequel la référence pointe. Une recherche qui ne suit pas renvoie la référence elle-même. Les deux sont correctes, et utiliser la mauvaise produit un dysfonctionnement silencieux plutôt qu'une erreur

La lecture d'un fichier associé illustre le premier sens. Pour obtenir le numéro d'objet du flux embarqué derrière les clés /EF et /F de la spécification de fichier, la recherche ne doit pas suivre, parce que suivre résout la référence en objet flux et le numéro d'objet est perdu. La règle se généralise : tout chemin de code qui a besoin d'une identité d'objet plutôt que du contenu d'un objet doit prendre la référence brute

Le contenu optionnel montre le sens opposé, et il a coûté plus cher à trouver. Le dictionnaire de propriétés du contenu optionnel est écrit dans le catalogue comme objet indirect, donc le code qui le relit sans suivre obtient une référence plutôt qu'un dictionnaire. Un contrôle de type sur cette valeur échoue alors, et la branche de repli naturelle, s'il n'y a pas de configuration, en créer une, s'exécute et écrase la configuration déjà présente. Rien ne lève d'exception. Les calques décrits dans les groupes de contenu optionnel et les calques perdent simplement leur état de visibilité par défaut

La leçon se généralise au-delà des deux cas. Quand une recherche peut renvoir aussi bien une référence que l'objet, un contrôle de type nu n'est pas de la gestion d'erreurs : c'est une branche qui finira par être prise pour la mauvaise raison. Décidez explicitement de ce dont chaque site d'appel a besoin, et préférez l'API publique qui répond directement à la question, comme une propriété de compte de contenu optionnel, à une incursion dans un accesseur protégé pour le dictionnaire du catalogue

Carte de décision du suivi de références dans les recherches PDF telle qu'implémentée dans PDFlibPas : lire /EF et /F sous une spécification de fichier ne doit pas suivre la référence car le numéro d'objet du flux embarqué est la réponse, tandis que le dictionnaire indirect /OCProperties du catalogue doit être suivi sinon un contrôle de type échoué écrase silencieusement la configuration de contenu optionnel existante
La même recherche répond à deux questions différentes : l'identité exige la référence brute, le contenu exige l'objet résolu, et un contrôle de type nu à la place de cette décision finit par exécuter la mauvaise branche sans lever d'exception
// Les pièces jointes de niveau document et les associations de niveau page coexistent.
// Un fichier embarqué peut aussi être marqué associé au niveau document
if Lib.IsEmbeddedFileAssociated(0) = 0 then
  Lib.SetEmbeddedFileAssociated(0, 1, 'Supplement');

Writeln('document associated files: ', Lib.GetAssociatedFileCount);
Writeln('page 3 associated files  : ',
        Lib.GetPageAssociatedFileCount(3));

// La suppression détache la liaison de page ; la charge utile reste dans l'arbre de noms
if Lib.ClearPageAssociatedFiles(3) > 0 then
  Writeln('page 3 associations removed, payloads still reachable');

Ce que font les modes de conformité aux pièces jointes

Les profils d'archivage restreignent ce qui peut être embarqué, et la restriction est appliquée au point d'entrée plutôt qu'à la sauvegarde. PDF/A-1 interdit totalement les fichiers embarqués, PDF/A-2 n'admet que des documents PDF/A embarqués, et PDF/A-3 est le profil qui a ouvert l'embarquement aux types de fichiers arbitraires, ce qui est précisément pourquoi les formats de factures hybrides se construisent dessus

PDFlibPas refuse la pièce jointe quand le mode de conformité actif ne l'autorise pas, à l'appel, et non des centaines d'opérations plus tard pendant la sortie. C'est un choix délibéré sur l'endroit où une erreur est la moins chère à traiter : un refus sur le site d'appel nomme le fichier que vous étiez en train d'ajouter, tandis qu'un refus à la sauvegarde nomme un document et vous laisse deviner laquelle de quarante pièces jointes en est cause

C'est aussi pourquoi les fichiers associés apparaissent si souvent en facturation électronique. Une facture hybride est un PDF qu'un humain lit avec une charge utile XML lisible par machine attachée et marquée de la bonne relation, et le profil du conteneur comme la clé de relation font partie de la spécification plutôt que des conventions. Cette construction est couverte dans la construction de factures hybrides Factur-X et ZUGFeRD, avec le volet métadonnées dans le schéma d'extension XMP de PDF/A-3

Quand l'association doit-elle être par page plutôt que par document ?

Quand un consommateur a besoin de savoir à quelle page les données appartiennent, et seulement dans ce cas. Les pièces jointes de niveau document sont plus simples, mieux prises en charge par les visionneuses, et suffisantes dès que la charge utile décrit tout le document, un XML de facture, un manifeste de signature, une archive de sources. Tournez-vous vers l'association de niveau page quand la charge utile est réellement circonscrite à la page et que l'identité de la page fait partie de son sens

Le support est la contrainte pratique. Les fichiers associés au niveau page sont une construction PDF 2.0, et le support par les visionneuses est plus mince que pour les pièces jointes de niveau document. Comme la charge utile se trouve de toute façon dans l'arbre de noms, une visionneuse qui ignore /AF sur les pages affiche quand même le fichier dans sa liste de pièces jointes, donc la dégradation est gracieuse. Mais si la liaison de page est essentielle à votre consommateur plutôt qu'une métadonnée utile, vérifiez le lecteur que vous ciblez réellement au lieu de le supposer

Les fichiers associés au niveau page, les pièces jointes de niveau document et les barrières de profils d'archivage qui gouvernent les deux sont livrés avec la bibliothèque PDF Delphi PDFlibPas. Si vous réparez aussi d'anciens fichiers à l'entrée, le travail sur métadonnées et conformité décrit dans la conversion vers PDF/A avec réparation des métadonnées est ce qui décide laquelle de ces routes de pièces jointes vous est disponible en premier lieu