Article technique

Sauvegardes PDF à version exacte en Delphi : conformité PDFiumPas

PDFiumPas, l'enveloppe Delphi et C++Builder autour du moteur PDFium de Google, enregistre un document à une version PDF exacte de 1.3 à 1.7 via le paramètre PdfVersion de la méthode TPdf.SaveAs. Le propre appel FPDF_SaveWithVersion de PDFium ne réécrit que l'en-tête %PDF-M.m, sans vérifier si le contenu réel du document est légal à cette version. PDFiumPas comble cet écart avec une passe de conformité post-sauvegarde qui parcourt la chaîne de révisions de référence croisée active et vérifie les déclarations de niveau d'extension Adobe avant que le fichier ne quitte la méthode

Cette distinction compte le plus en production d'impression, où un profil PDF/X nomme une version PDF exacte et un outil de préflight ou un RIP rejette tout ce qui est silencieusement en désaccord avec son propre en-tête, un scénario couvert du côté sortie dans la validation de documents PDF/X prêts pour l'impression avec PDFiumPas. SaveAs expose la cible sous forme de l'énumération TPdfVersion, pv13 à pv17 aux côtés des valeurs pv10 à pv12 plus anciennes, plus un TSaveOption indépendant pour les réécritures incrémentales ou complètes. Transmettez PdfVersion et PDFiumPas effectue deux tâches en un seul appel : il demande à PDFium d'estampiller l'en-tête demandé, puis relit les octets fraîchement écrits et refuse de rendre un fichier dont le contenu actif ne peut légalement exister à cette version

var
  Pdf: TPdf;
begin
  Pdf:= TPdf.Create(nil);
  try
    Pdf.FileName:= 'source.pdf';
    Pdf.Active:= True;
    try
      Pdf.SaveAs('press-ready.pdf', saNoIncremental, pv17);
    except
      on E: Exception do
        // E.Message names the offending feature and the version or
        // extension level it actually needs, for example:
        // "RichMedia annotations and RichMediaExecute actions require
        // /Extensions /ADBE with /BaseVersion /1.7 and /ExtensionLevel 3
        // or newer."
        raise;
    end;
  finally
    Pdf.Free;
  end;
end;

Pourquoi la dernière définition d'objet dans le fichier est-elle la mauvaise chose à laquelle faire confiance ?

Le dernier objet physique portant un numéro donné dans un fichier PDF n'est pas nécessairement l'objet qu'un lecteur conforme résoudrait aujourd'hui pour ce numéro. Un PDF ayant subi plusieurs mises à jour incrémentales n'a pas un seul graphe d'objets, il a un historique de graphes superposés à l'intérieur d'un seul fichier, et chaque cycle d'ajout peut libérer un objet, le redéfinir sous un nouveau numéro de génération, ou laisser son ancien corps physique assis entre deux marqueurs endobj sans plus aucune entrée de référence croisée pointant vers lui

PDFiumPas a rencontré exactement ce mode d'échec avant de suivre explicitement les révisions xref : une annotation Redact orpheline suite à une réécriture ultérieure d'objet de page, ou un dictionnaire /MarkInfo laissé physiquement présent sans entrée xref pointant vers lui, pouvait tout de même apparaître dans un scan d'octets et tout de même déclencher une vérification de fonctionnalité de version qui ne s'appliquait plus au document qu'un lecteur ouvrirait réellement. La direction de l'échec était le faux rejet, pas la fausse acceptation : un fichier ayant réellement dépassé une fonctionnalité dans sa révision actuelle pouvait tout de même se voir bloquer l'enregistrement à une version inférieure à cause de contenu que plus personne ne pouvait atteindre

Comment PDFiumPas détermine-t-il quelles définitions d'objet sont réellement actives ?

PDFiumPas résout l'ensemble d'objets actif de la même manière qu'un lecteur conforme, en parcourant la chaîne de référence croisée plutôt qu'en scannant les octets à la recherche d'en-têtes d'objet. Le résolveur démarre au dernier décalage startxref du fichier et suit chaque lien /Prev vers l'arrière à travers les révisions plus anciennes, analysant au passage les tables de référence croisée classiques, les flux hybrides liés par /XRefStm, et les flux de référence croisée purs. Le parcours s'exécute du plus récent au plus ancien et fixe chaque numéro d'objet la première fois qu'il est vu, si bien qu'une entrée libre dans une révision ultérieure éclipse correctement un corps d'objet écrit dans une révision antérieure, et une redéfinition sous un nouveau décalage ou une nouvelle génération l'emporte toujours sur ce qu'elle remplace

Les membres de flux d'objets bénéficient d'une vérification supplémentaire qu'une simple recherche de décalage ne peut fournir à elle seule, un mécanisme couvert plus en détail dans la validation des flux d'objets et de référence croisée avec PDFiumPas. Un objet compressé récupéré depuis un /ObjStm doit voir son flux parent confirmé actif dans le même parcours, et son index doit concorder avec la propre position du membre à l'intérieur de l'en-tête de ce flux avant que PDFiumPas ne le traite comme du contenu vivant. ISO 32000-1 section 7.5.8.4 décrit même un cas de référence hybride où une table de compatibilité classique marque un objet libre tandis que l'entrée /XRefStm du trailer définit simultanément ce même objet comme un membre compressé ailleurs ; PDFiumPas fusionne le flux xref supplémentaire dans la même révision avant que les entrées classiques ne soient appliquées, si bien que la définition compressée l'emporte comme la spécification l'entend

Les niveaux d'extension Adobe : le portail au-dessus du numéro de version

Un en-tête %PDF-1.7 ne promet que l'ensemble de fonctionnalités que ISO 32000-1 a standardisé en 2008, tandis que plusieurs capacités sur lesquelles les producteurs de PDF s'appuient aujourd'hui ont été livrées ultérieurement comme des suppléments propres à Adobe superposés sur ce même numéro de version. Adobe a enregistré chaque supplément comme une paire BaseVersion et ExtensionLevel consignée dans le dictionnaire /Extensions du catalogue de document sous un préfixe de développeur, ADBE pour les propres extensions d'Adobe, afin qu'un lecteur puisse distinguer un simple fichier PDF 1.7 d'un fichier qui implémente aussi un niveau d'extension numéroté. Enregistrer à pv17 sans cette déclaration n'est pas une erreur en soi ; cela n'en devient une qu'à l'instant où le contenu actif dépend réellement d'une fonctionnalité que la déclaration est censée couvrir

Quelles fonctionnalités de version élevée déclenchent le portail de version explicite ?

PDFiumPas vérifie une liste spécifique, pilotée par la spécification, plutôt que de deviner à partir du seul numéro de version. Les dictionnaires d'image portant une entrée explicite /SMaskInData ou une valeur /BitsPerComponent de 16 nécessitent toutes deux PDF 1.5, le cas 16 bits suivant directement les règles de composante d'image de la section 4.8 de PDF Reference 1.5. Les annotations RichMedia et les actions RichMediaExecute nécessitent /BaseVersion /1.7 avec /ExtensionLevel 3 ou supérieur. Les flux 3D PRC, identifiés par un dictionnaire portant à la fois /Type /3D et /Subtype /PRC, nécessitent la même version de base mais seulement /ExtensionLevel 1. Les dictionnaires de mesure géospatiale et les annotations de projection nécessitent /BaseVersion /1.7 avec /ExtensionLevel 3, le même supplément Adobe dont dépend RichMedia

La vérification géospatiale porte un détail de lecture de spécification qui mérite d'être connu si vous construisez un jour votre propre logique conditionnée par version au-dessus de PDFiumPas. Le tableau 254 d'ISO 32000-1 marque l'entrée /Type du dictionnaire Measure comme facultative, notant seulement que « si présente, doit être Measure », tandis que le tableau 311 rend /Type obligatoire pour le dictionnaire de flux 3D dans lequel vit le contenu PRC. La sortie GeoPDF du monde réel provenant d'outils de cartographie omet couramment /Type sur le dictionnaire Measure et n'écrit que /Subtype /GEO, si bien que le détecteur géospatial de PDFiumPas fait correspondre sur /Subtype seul plutôt que d'exiger les deux clés comme son détecteur 3D PRC peut le faire en toute sécurité. Exiger /Type sur les deux dictionnaires aurait laissé du contenu GeoPDF conforme échapper au portail sans être détecté, atterrissant dans un simple fichier PDF 1.7 sans déclaration de niveau d'extension pour l'appuyer

PDFiumPas rétrograde-t-il automatiquement les fonctionnalités non prises en charge ?

Pas comme capacité générale, et supposer le contraire est l'erreur à éviter ici. SaveAs achemine la version cible à travers une routine interne, ValidatePdfVersionCompliance, et lorsque cette routine trouve une fonctionnalité que la version cible ou sa déclaration de niveau d'extension ne peut pas prendre en charge, SaveAs lève une exception portant le texte d'erreur de la routine au lieu d'écrire le fichier ; l'appelant reçoit en retour une raison précise, nommée par fonctionnalité, jamais un document silencieusement réécrit. Le seul endroit où PDFiumPas réécrit effectivement du contenu automatiquement est une cible PDF 1.3, où il retire les valeurs par défaut de transparence sémantiquement neutres /BM /Normal, /CA 1, et /ca 1 que PDFium écrit toujours dans les dictionnaires ExtGState quelle que soit la version cible, car ces valeurs spécifiques ne portent aucune signification visuelle et PDF 1.3 précède entièrement ces clés

// PDF 1.3 targets rewrite the saved bytes to strip transparency
// defaults PDFium always emits, so incremental mode cannot apply
Pdf.SaveAs('legacy-archive.pdf', saIncremental, pv13);
// raises: PDF 1.3 normalization is incompatible with incremental
// save mode

Une véritable transparence non par défaut et de véritables masques doux d'image échouent tout de même purement et simplement à une cible PDF 1.3, car les retirer changerait l'apparence réelle de la page, et PDFiumPas ne prendra pas cette décision à votre place. Deux limites apparentées méritent d'être anticipées avant qu'une version exacte n'entre dans un pipeline par lot. La sortie à version explicite ne porte jamais de dictionnaire /Encrypt ; l'enregistrement échoue immédiatement si la source est protégée, ce qui se trouve s'aligner avec les profils PDF/X et PDF/A qui interdisent de toute façon le chiffrement, mais cela signifie que le déchiffrement est une étape séparée dans votre flux de travail plutôt que quelque chose que SaveAs fait pour vous. PDFiumPas n'a pas non plus de méthode publique pour écrire une déclaration /Extensions /ADBE sur un catalogue, si bien qu'un fichier source contenant du RichMedia, du 3D PRC, ou du contenu géospatial mais dépourvu de cette déclaration ne passera pas le portail quelle que soit la PdfVersion demandée ; la déclaration doit déjà exister dans la source, généralement parce que l'outil de création l'a écrite, ou la fonctionnalité doit être retirée avant l'enregistrement. La propriété en lecture seule TPdf.PdfVersion mérite d'être vérifiée avant même de tenter un enregistrement à version exacte, car elle résout la même version effective consciente du catalogue, en-tête ou remplacement /Version, quel que soit celui qui est courant, dont dépend le validateur au moment de l'enregistrement lui-même

Pdf.FileName:= 'incoming.pdf';
Pdf.Active:= True;
// PdfVersion resolves the same catalog-aware effective version the
// save-time validator uses, so a mismatch here is worth investigating
// before spending a full SaveAs attempt on it
LogSourceVersion('incoming.pdf', Pdf.PdfVersion);

Traitez une exception SaveAs sur une cible de version exacte comme un rapport de préflight plutôt que comme un bogue : le message nomme la clause exacte que le document source viole, ce qui est précisément l'information dont un atelier d'impression ou un pipeline d'archivage a besoin avant qu'un fichier n'aille plus loin. Le chemin d'enregistrement à version explicite, le résolveur de révision xref active, et les vérifications de niveau d'extension Adobe décrits ici font partie du composant PDFiumPas standard pour Delphi et C++Builder ; la page produit porte la référence complète de TPdf.SaveAs aux côtés du reste de l'API de conformité et de formulaires