Article technique

Limites d'implémentation PDF/A et contrôles d'encodage

Une archive de factures peut se rendre proprement, imprimer, incorporer chaque police et porter une intention de sortie valide, et un validateur PDF/A la rejettera quand même parce qu'un dictionnaire de ressources tient 4 096 entrées. Le plafond est 4 095. PDF/A hérite une petite table de limites d'implémentation de la PDF Reference, et un fichier à une unité au-dessus de l'une quelconque n'est plus conforme alors qu'aucun lecteur ne se plaint

L'autre échec qui atterrit dans le même panier « mais il a l'air bien » est une police TrueType symbolique qui porte une entrée /Encoding. Cases à cocher, codes-barres et dingbats sortent juste à l'écran, et le validateur signale quand même le dictionnaire de police. Les deux problèmes vivent dans le graphe d'objets plutôt que dans quoi que ce soit de visible sur la page, donc ils méritent un contrôle mécanique avant qu'un fichier n'entre dans une archive

Les nombres que PDF/A hérite, et où passe la frontière

PDFium Component vérifie quatre limites de conteneur à chaque validation PDF/A : un jeton de nom peut mesurer au plus 127 octets, un tableau peut tenir au plus 8 191 éléments, un dictionnaire peut tenir au plus 4 095 entrées, et tableaux et dictionnaires peuvent s'imbriquer au plus sur 28 niveaux. Les quatre frontières sont inclusives. Un nom de 127 octets passe et un nom de 128 échoue ; un tableau de 8 191 éléments pile passe et un de plus échoue

Les limites de nom, tableau et dictionnaire viennent de l'époque où un lecteur conforme n'était tenu de gérer des structures que jusqu'à ces tailles. Une visionneuse de bureau actuelle peut ouvrir un tableau bien plus grand sans se plaindre, mais un fichier d'archivage est censé s'ouvrir dans des lecteurs que personne n'a encore testés, y compris de vieux qu'un système de records fait peut-être encore tourner. Voilà pourquoi PDF/A garde les plafonds historiques au lieu de les desserrer

Inclusif contre exclusif est là où les contrôles improvisés se gâtent d'ordinaire. Un test écrit Count >= 8191 au lieu de Count > 8191 rejette des fichiers conformes, et un validateur qui rejette des fichiers conformes a tendance à être cru quand même, donc le générateur se fait « réparer » pour un problème qu'il n'a jamais eu. La fonction ValidatePdfACompliance de l'unité FPdfPdfa compte strictement au-delà de la limite, et elle lit les octets du fichier directement, donc le contrôle tourne sans charger la DLL PDFium

Diagramme des quatre limites d'implémentation inclusives de l'annexe C de l'ISO 19005-1 validées par un pur scan d'octets depuis Delphi : tokens de nom de 127 octets, 8191 éléments de tableau, 4095 entrées de dictionnaire et 28 niveaux d'imbrication de conteneurs
Les quatre limites sont inclusives — la valeur de frontière se valide elle-même et seule l'unité suivante échoue
uses
  System.Classes, FPdfPdfa;

procedure CheckContainerLimits(const FileName: string; Log: TStrings);
var
  Src: TFileStream;
  Res: TPdfAValidationResult;
begin
  Src := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Res := ValidatePdfACompliance(Src);   // scan d'octets, pas de DLL PDFium nécessaire
    if pvaiNameOverLimit in Res.Issues then
      Log.Add('A name token is longer than 127 bytes');
    if pvaiArrayOverLimit in Res.Issues then
      Log.Add('An array holds more than 8191 elements');
    if pvaiDictOverLimit in Res.Issues then
      Log.Add('A dictionary holds more than 4095 entries');
    if pvaiNestingOverLimit in Res.Issues then
      Log.Add('Arrays and dictionaries nest deeper than 28 levels');
    if Res.IsCompliant then
      Log.Add('No PDF/A findings');
  finally
    Src.Free;
  end;
end;

Les quatre drapeaux sont membres de l'ensemble Issues sur TPdfAValidationResult, à côté de tout autre constat PDF/A, et IsCompliant renvoie True seulement quand une identification PDF/A a été trouvée et que l'ensemble est vide. Un fichier qui ne porte aucune identification PDF/A du tout est contrôlé contre le jeu de règles strict PDF/A-1, donc les limites de conteneur s'y appliquent quand même

Quels générateurs butent sur ces limites ?

Presque toujours ceux qui bâtissent la structure dans une boucle. Un formulaire de plusieurs milliers de champs produit un tableau AcroForm /Fields, ou un tableau /Annots de page, qui dépasse 8 191 entrées. Un moteur de rapports qui enregistre une ressource de police ou d'image par objet généré, sans jamais réutiliser une entrée existante, fait grossir un sous-dictionnaire /Resources au-delà de 4 095 clés. Un document balisé dont l'arbre de structure est produit en récursant sur un modèle de données imbriqué peut passer 28 niveaux sans que personne ne regarde

Les noms longs ont une autre origine : des données entassées dans des jetons de nom. Une colorant de séparation nommé d'après une référence client, un groupe de contenu optionnel nommé d'après un chemin de fichier, une étiquette de champ assemblée depuis six niveaux de hiérarchie. Les noms ne coûtent rien à générer, et 127 octets partent vite dès qu'une étiquette UTF-8 avec caractères accentués entre en jeu, car chacun de ces caractères prend deux ou trois octets. Les 127 octets se comptent après l'introduceur /, jusqu'au prochain délimiteur ou blanc

Le remède est structurel chaque fois. Scindez le tableau, scindez le dictionnaire, aplatissez la récursion, raccourcissez le nom. Injecter des marqueurs PDF/A dans le fichier ne peut rien pour aucun de ces constats, puisque le problème est la forme du graphe d'objets et que les métadonnées XMP n'en disent rien

Comment le scan compte éléments et entrées

La règle de compte compte quand vous essayez de reproduire un constat dans un éditeur hexadécimal. Un tableau ou dictionnaire imbriqué compte pour un élément de son parent, ainsi que la limite est énoncée : un tableau de 10 éléments de tableaux de 1 000 éléments va bien, car aucun tableau unique ne dépasse 8 191. Dans un dictionnaire, seules les clés sont comptées, donc une valeur qui se trouve être un nom ne gonfle pas le total d'entrées. Les chaînes hexadécimales comme <FEFF0041> sont sautées comme jetons uniques, donc leurs crochets en angle ne sont jamais pris pour de l'imbrication de dictionnaire

Le scan couvre la structure d'objets de tout le fichier, y compris les objets stockés dans des flux d'objets compressés, qui sont déployés avant le compte. Les corps de flux eux-mêmes, comme le contenu de pages et les données d'images, sont mis à blanc d'abord, donc une suite d'octets binaires qui contiendrait par hasard un / ou un [ ne peut pas déclencher un faux constat de nom ou de tableau

Il y a une limite numérique voisine que la même validation rapporte sous son propre drapeau. Quand le fichier déclare PDF/A-2 ou une partie plus récente, pvaiIntegerImplementationLimit est levé pour tout jeton d'entier hors de la plage signée 32 bits, c'est-à-dire hors de -2 147 483 648 à 2 147 483 647. Les générateurs qui écrivent des décalages d'octets ou de grands compteurs en entiers simples sont la source habituelle

Pourquoi une police TrueType symbolique doit-elle lâcher /Encoding ?

ISO 19005-1 §6.3.7 n'admet que le cmap intégré au programme de police comme mappage de caractères pour une police TrueType symbolique. Une police symbolique mappe les codes vers les glyphes à sa manière ; c'est ce que déclare le drapeau Symbolic. Ajoutez une entrée /Encoding et le fichier donne désormais deux réponses à la question « quel glyphe l'octet 0x41 sélectionne-t-il », sans rien dans le fichier qui dise laquelle l'emporte. Les lecteurs résolvent le conflit autrement, et la même case à cocher peut sortir cochée dans un lecteur et comme lettre A dans un autre

ValidatePdfACompliance trouve chaque dictionnaire de police avec /Subtype /TrueType, vérifie s'il a une clé /Encoding, puis lit le bit 3 (valeur 4) de l'entrée /Flags dans le descripteur de police. Le descripteur est suivi qu'il soit écrit en ligne ou référencé comme objet indirect. Une police TrueType non symbolique qui garde /WinAnsiEncoding ou /MacRomanEncoding n'est pas signalée par ce contrôle, car pour les polices non symboliques PDF/A attend exactement cet encodage

Diagramme contrastant une police TrueType symbolique dont l'entrée /Encoding crée deux mappages de glyphes concurrents et une police non symbolique qui garde légitimement WinAnsiEncoding dans une vérification PDFium Component Delphi
Le contrôle lit l'indicateur symbolique dans le descripteur de police et se déclenche sur la contradiction, non sur la présence d'un encodage
// Res vient de ValidatePdfACompliance comme dans le premier exemple
if pvaiSymbolicTrueTypeEncoding in Res.Issues then
  Log.Add('A symbolic TrueType font carries /Encoding; PDF/A admits only ' +
    'the font''s built-in cmap (ISO 19005-1 6.3.7)');

// Une règle de police voisine au déclencheur différent : une police
// TrueType NON symbolique dont le dictionnaire Encoding porte /Differences
if pvaiTrueTypeDifferencesForbidden in Res.Issues then
  Log.Add('A non-symbolic TrueType Encoding dictionary carries /Differences');

Les polices qui butent sur cette règle sont prévisibles : Symbol, Wingdings, polices de codes-barres, polices d'icônes. Elles surgissent dans les documents d'affaires comme cases à cocher, étoiles de notation et codes-barres, et une étape de sous-ensemblage qui traite toute police TrueType de la même manière écrit un encodage pour elles aussi. Ce sont aussi les polices que personne ne regarde quand un validateur dit « problème de police », parce que les polices du corps de texte sont contrôlées d'abord

L'erreur inverse surgit aussi dans la nature : un producteur pose le drapeau Symbolic sur une police de texte ordinaire qui est réellement encodée WinAnsi. Le contrôle PDF/A suit la clause telle qu'écrite et signale cette police, et ici la réparation appartient au descripteur : corrigez le drapeau dans le descripteur plutôt que supprimer l'encodage. Retirer /Encoding d'une police de texte mal étiquetée laisserait le texte dépendre du cmap que le sous-ensemble contient par hasard, ce qui est un pire fichier que celui du départ. Regardez le jeu de glyphes de la police avant de choisir quelle moitié de la contradiction retirer

Lire les constats dans un rapport de prévol

L'unité de rapport de prévol est livrée avec l'édition Enterprise, tandis que ValidatePdfACompliance est dans chaque édition. Le rapport de l'unité FPdfPreflightReport range les quatre limites de conteneur dans la catégorie Structure et le problème d'encodage TrueType symbolique dans la catégorie Content. Cette scission est délibérée. Les constats de structure retournent d'ordinaire à qui maintient le générateur, tandis que les constats de contenu vont d'ordinaire à qui a fourni les polices et les modèles, et un rapport qui mélange envoie les deux personnes courir après des problèmes qui ne sont pas les leurs

Chaque problème porte son nom d'enum comme Code et une recommandation qui nomme le remède en termes concrets, par exemple « Split dictionaries so none carries more than 4095 entries ». Notez que la catégorie Structure tient aussi deux autres constats, une structure balisée manquante sur une revendication de niveau A et des flux de références croisées ou d'objets PDF 1.5 dans un fichier PDF/A-1, donc filtrez par code quand vous ne voulez que les quatre limites

uses
  System.SysUtils, System.Classes, PDFium, FPdfPreflightReport;

procedure ReportStructureFindings(const FileName: string; Log: TStrings);
var
  Pdf: TPdf;
  Report: TPdfPreflightReport;
  Issues: TPdfPreflightIssues;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;          // ce chemin charge la DLL PDFium
    if not Pdf.Active then
      Exit;
    Report := BuildPdfPreflightReport(Pdf, [ppsPdfA]);
    Issues := Report.FilterByCategory('Structure').FlattenIssues;
    for I := 0 to High(Issues) do
      Log.Add(Issues[I].Code + ': ' + Issues[I].Recommendation);
    Report.SaveJsonToFile(ChangeFileExt(FileName, '.preflight.json'));
  finally
    Pdf.Free;
  end;
end;

Les deux chemins répondent à des questions légèrement différentes. BuildPdfPreflightReport passe par TPdf.ValidatePdfA, qui sauvegarde le document ouvert vers une copie temporaire, fait tourner le même scan d'octets sur cette copie puis ajoute des contrôles qui exigent le moteur PDFium, comme les polices employées sur une page mais non incorporées. ValidatePdfACompliance juge les octets exacts sur disque. Pour les limites d'implémentation, la voie d'octets sur le fichier d'origine est celle à qui se fier : elle rapporte sur le fichier que vous vous apprêtez à archiver, et elle tourne dans une étape de build ou un service verrouillé où une DLL native ne peut pas être déployée. Employez la voie du rapport quand vous voulez aussi les contrôles au niveau moteur et la sortie JSON, SARIF ou JUnit pour un pipeline

Où aller ensuite

Exécuter ces contrôles sur un dossier, décider quels constats bloquent une publication et tourner le résultat en un rapport que les gens liront sont couverts dans la visite guidée de la validation prévol PDF/A en Delphi et dans le CLI de rapport prévol par lots. Quelle partie PDF/A et quel niveau de conformité viser en premier lieu fait l'objet des notes sur la conformité d'archivage PDF/A ; ce choix décide, entre autres, de savoir si la limite d'entier s'applique

Les validateurs montrés ici sont livrés avec PDFium Component pour Delphi, C++Builder et Lazarus, à côté du rendu, du remplissage de formulaires et de l'édition par-dessus le moteur PDFium. La page produit de PDFium Component liste les normes de conformité prises en charge, les éditions et les versions de compilateurs