Exportez un document depuis Microsoft Word ou Excel avec Enregistrer sous forme de PDF (Save as PDF) et le fichier sur le disque est, le plus souvent, un fichier à référence hybride (hybrid-reference file). Il porte ses informations de références croisées (cross-reference information) deux fois : une fois en tant que table classique à largeur fixe (fixed-width table) qui terminait chaque PDF jusqu'à la version 1.4, et une fois en tant que flux de références croisées (cross-reference stream) compressé dont dépend réellement la majeure partie du document. Une seule clé de bande-annonce (trailer key), /XRefStm, relie (stitches) les deux vues ensemble, et le fait qu'un outil voie l'ensemble du document dépend de la question de savoir s'il suit cette clé
Cet article examine les fichiers hybrides du côté consommateur (consuming side) : à quoi ressemblent les octets à la fin du fichier, comment les deux vues s'éloignent (drift apart) lors de l'édition, et comment un pipeline Delphi peut détecter et acheminer (route) des entrées hybrides. Comment un chargeur fusionne les vues, et pourquoi l'ordre n'est pas négociable, est le sujet de notre article HotPDF sur le chargement des fichiers à référence hybride ; celui-ci a pour but de reconnaître la mise en page (layout) en premier lieu
Pourquoi les exportations Office écrivent l'index deux fois
Le PDF 1.5 a introduit deux fonctionnalités qui ont modifié la forme du fichier : les flux de références croisées (cross-reference streams), qui stockent l'index des objets (object index) sous forme de données binaires compressées au lieu d'une table en texte brut (plaintext table), et les flux d'objets (object streams), qui regroupent (pack) de nombreux petits objets dans un seul conteneur compressé Flate (Flate-compressed container). Un rédacteur (writer) qui les utilise produit fichiers plus petits, mais un lecteur (reader) PDF 1.4 ne peut pas ouvrir le résultat, car les structures sur lesquelles il s'appuie, le mot-clé xref et le dictionnaire trailer, ont disparu
ISO 32000-1 §7.5.8.4 définit le compromis. Un fichier à référence hybride écrit les deux : une table de références croisées classique (classic cross-reference table) adressant les objets qu'un ancien lecteur doit atteindre, le catalogue et l'arborescence des pages (page tree) parmi eux, et un flux de références croisées (cross-reference stream) qui indexe tout le reste. Les objets pliés (folded) dans les flux d'objets (object streams) sont marqués libres (free) dans la table classique, de sorte qu'un lecteur 1.4 les ignore sans se plaindre ; leurs emplacements réels n'existent que dans le flux. La bande-annonce classique (classic trailer) porte alors une clé /XRefStm contenant le décalage d'octet (byte offset) de ce flux. Une ancienne visionneuse ne lit jamais la clé et effectue le rendu du fichier à partir de la vue tableau. Une visionneuse moderne la suit et voit le document complet. Word et Excel émettent exactement cette disposition depuis des années, c'est pourquoi les fichiers hybrides ne sont pas un cas limite (corner case) exotique mais une grande part de ce que reçoivent les pipelines d'entreprise
À quoi ressemble la queue d'un fichier hybride
La mise en page est plus facile à comprendre à partir des octets. Voici la queue d'un petit fichier hybride, les décalages (offsets) ont été raccourcis ; dans une véritable exportation Office, la valeur /XRefStm est généralement un grand décalage vers la fin du fichier. L'ordre de lecture (reading order) est la marche de la queue d'abord (tail-first walk) décrite dans notre présentation de la structure du fichier PDF : trouvez %%EOF, lisez startxref, passez à la table
% ... body objects, including object streams and, at byte 116,
% the cross-reference stream (a stream object with /Type /XRef) ...
xref % classic section: what startxref points at
0 4
0000000000 65535 f % slot 0: head of the free list, always present
0000000017 00000 n % object 1: the catalog, visible to any reader
0000000000 65535 f % object 2: marked free -- lives in an object stream
0000000000 65535 f % object 3: same; only the stream view locates it
trailer
<<
/Size 4
/Root 1 0 R
/XRefStm 116 % byte offset of the cross-reference stream
>>
startxref
7164 % byte offset of the 'xref' keyword above
%%EOF
Deux détails dans ce vidage (dump) portent tout le mécanisme. Tout d'abord, startxref pointe vers la section classique, exprès : c'est l'adresse sur laquelle un ancien lecteur doit atterrir. Le flux de références croisées (cross-reference stream) n'est accessible que via la clé /XRefStm à l'intérieur du dictionnaire de bande-annonce (trailer dictionary), de sorte qu'un analyseur (parser) qui ne recherche jamais cette clé n'apprend jamais l'existence du flux. Deuxièmement, les objets 2 et 3 sont des mensonges d'un type bénin. Le tableau classique les déclare libres, mais ce sont de vrais objets assis à l'intérieur d'un conteneur compressé ; le marquage libre est ce qui empêche un lecteur 1.4 de trébucher sur (tripping over) des entrées qu'il ne peut pas utiliser. Un consommateur (consumer) qui fait confiance à la vue classique uniquement conclut que la majeure partie de ce document n'existe pas
Comment les deux vues s'éloignent (drift apart)
Un fichier hybride tout droit sorti de Word est cohérent en interne : les deux vues décrivent le même document, chacune dans sa portée (scope) déclarée. Le problème commence lorsque le fichier est édité par un outil qui ne comprend qu'une seule des vues. Considérons un utilitaire d'estampillage (stamping utility) qui ajoute une mise à jour incrémentielle (incremental update) de style classique : de nouveaux objets, une nouvelle section xref, une chaîne /Prev vers la section précédente et une nouvelle bande-annonce. Si cette bande-annonce supprime la clé /XRefStm, la vue en flux (stream view) est orpheline ; s'il copie l'ancienne valeur vers l'avant, la vue en flux décrit toujours le document tel qu'il était avant l'édition. Quoi qu'il en soit, les deux index sont désormais en désaccord sur ce que contient le fichier
Le fichier résultant a une signature de défaillance (failure signature) distinctive : les objets visibles dans une vue sont manquants (missing) ou périmés (stale) dans l'autre. Un lecteur qui se résout via la vue en flux trouve la version pré-édition (pre-edit) d'un objet mis à jour, ou aucune entrée du tout pour un objet ajouté. Un lecteur de la vue sous forme de tableau (table view) voit l'édition mais perd la trace des objets compressés que le flux seul localise. Dans la pratique, cela se traduit par des champs de formulaire (form fields) qui survivent dans une visionneuse et disparaissent dans une autre, des annotations qu'une passe d'estampillage (stamping pass) semble avoir supprimées ou des recherches qui atterrissent entièrement sur le mauvais objet
Ce qui rend ces fichiers coûteux à déboguer (debug), c'est qu'Adobe Acrobat les ouvre généralement sans se plaindre : lorsque l'index est en désaccord avec les octets, il reconstruit discrètement (quietly) les données de références croisées (cross-reference data) en recherchant (scanning for) les en-têtes d'objet (object headers), de sorte que celui qui a produit le fichier cassé ne voit rien d'anormal. La défaillance (failure) apparaît plus tard, lorsque le fichier atteint un consommateur (consumer) strict, un validateur de contrôle en amont (preflight validator), un service de signature, une tâche d'ingestion d'archivage (archival ingest job), qui fait confiance à la structure déclarée et signale des objets manquants ou une discordance (mismatch) de références croisées. "Il s'ouvre très bien dans Acrobat" est la façon dont commence presque chaque ticket de désynchronisation hybride (hybrid desynchronization)
Détection d'un fichier hybride en Delphi pur (plain Delphi)
La classification des entrées ne nécessite pas de bibliothèque PDF. La clé /XRefStm ne peut se trouver qu'à l'intérieur d'un dictionnaire de bande-annonce classique (classic trailer dictionary), et la bande-annonce active (active trailer) se trouve dans les derniers kilo-octets du fichier, car la spécification exige que %%EOF apparaisse près de la fin physique. La lecture d'une fenêtre de queue limitée (bounded tail window) et sa recherche suffisent pour le triage :
uses
System.SysUtils, System.Classes, System.StrUtils, System.Math;
function IsHybridReferencePdf(const FileName: string): Boolean;
const
TailWindow = 2048;
var
Stream: TFileStream;
Buf: TBytes;
Tail: string;
Len, TrailerPos, NextPos, KeyPos, StartXrefPos: Integer;
begin
Result := False;
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
if Stream.Size < 48 then
Exit;
Len := Min(TailWindow, Integer(Stream.Size));
SetLength(Buf, Len);
Stream.Position := Stream.Size - Len;
Stream.ReadBuffer(Buf[0], Len);
finally
Stream.Free;
end;
// Every keyword involved is 7-bit ASCII, so a byte-wise decode is safe
Tail := TEncoding.ANSI.GetString(Buf);
// Find the LAST 'trailer' keyword: with incremental updates,
// the newest trailer is the one that governs the file
TrailerPos := 0;
NextPos := Pos('trailer', Tail);
while NextPos > 0 do
begin
TrailerPos := NextPos;
NextPos := PosEx('trailer', Tail, NextPos + 1);
end;
if TrailerPos = 0 then
Exit; // no classic trailer: a pure xref-stream file, not hybrid
// A hybrid trailer carries /XRefStm between 'trailer' and 'startxref'
KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
StartXrefPos := PosEx('startxref', Tail, TrailerPos);
Result := (KeyPos > 0) and
((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;
Les trois résultats s'alignent (line up) sur les trois dispositions. Un fichier purement classique a une bande-annonce mais pas de /XRefStm : Faux (False). Un fichier qui s'engage pleinement (commits fully) dans les flux de références croisées n'a pas du tout de mot-clé trailer, ses clés de bande-annonce vivent dans le dictionnaire de flux : également Faux, à juste titre, car un tel fichier est compressé, pas hybride. Seule la disposition à double index (double-indexed layout) renvoie Vrai (True)
Pour une utilisation en production, deux renforcements (hardenings) valent les lignes supplémentaires. Analysez (Parse) l'entier après /XRefStm, recherchez (seek) ce décalage (offset), et confirmez qu'un objet de flux (stream object) avec /Type /XRef s'y trouve réellement ; un fichier tronqué peut porter la clé alors que le flux a disparu, ce qui appartient à un seau (bucket) différent de celui d'un hybride sain. Et traitez la taille de la fenêtre comme un paramètre : 2 Ko couvrent la sortie Office ordinaire, mais un dictionnaire de bande-annonce (trailer dictionary) exceptionnellement grand peut pousser le mot-clé hors de portée (out of range), et l'élargissement de la fenêtre vaut mieux (beats) que de déclarer le fichier classique par accident
Routage (Routing) de fichiers hybrides via un pipeline Delphi
La détection vous permet d'acheter (buys you) une décision de routage (routing decision). Pour les fichiers qui sont uniquement lus, rendus ou validés, utilisez un chargeur (loader) qui résout les deux vues, puis vérifiez le comportement (behavior) plutôt que les octets. Le Composant PDFium analyse la chaîne /XRefStm lors du chargement, de sorte que la table d'objets (object table) que votre code voit est celle fusionnée, et les vérifications décrites dans notre article sur la validation des flux d'objets et de références croisées s'appliquent sans modification. Si un hybride désynchronisé est suffisamment endommagé pour refuser le chargement, le moteur (engine) le signale via son ensemble d'erreurs (error set), FPDF_ERR_SUCCESS, FPDF_ERR_UNKNOWN, FPDF_ERR_FILE, FPDF_ERR_FORMAT, FPDF_ERR_PASSWORD, FPDF_ERR_SECURITY et FPDF_ERR_PAGE, avec FPDF_ERR_FORMAT celui qui produit des dommages structurels. Ne vous appuyez cependant pas (Do not lean on) sur ce signal : PDFium est indulgent (lenient) par conception et reconstruit silencieusement la plupart des fichiers incohérents, de sorte qu'un chargement réussi prouve que le fichier était récupérable, et non que ses deux vues concordent. La vérification de cohérence (consistency check) significative consiste à comparer ce qu'une analyse complète de l'objet (full object walk) trouve par rapport à ce que déclare la /Size de la bande-annonce
Pour les fichiers modifiés par votre pipeline, la politique la plus sûre est de les empêcher complètement d'être hybrides. Un chargement suivi d'une sauvegarde complète via HotPDF réécrit le document avec une seule référence croisée cohérente (self-consistent cross-reference) sous une seule forme : pas de /XRefStm, pas de deuxième vue à désynchroniser, chaque objet détenu (owned) par exactement une entrée d'index. Cette normalisation (normalization) est ce que vous voulez avant l'ingestion d'archivage (archival ingest), avant un service strict en aval de RIP (strict downstream RIP) ou de signature, et après toute édition appliquée à une entrée hybride. Cela fonctionne car le chargeur (loader) a correctement fusionné les vues lors de l'entrée, le mécanisme décrit en détail (walks through in detail) dans l'article sur la référence hybride HotPDF
La seule classe de fichiers à laisser de côté est celle des documents signés numériquement (digitally signed documents). Une réécriture complète déplace chaque octet, ce qui invalide (invalidates) toute signature calculée sur les plages (ranges) d'origine. Une modification d'un hybride signé doit se faire (go in) sous la forme d'une mise à jour incrémentielle appropriée (proper incremental update) qui maintient les deux vues ; un fichier qui ne nécessite qu'une lecture doit passer sans être touché. La normalisation (Normalization) concerne les fichiers que vous possédez (own) ; les fichiers signés, vous ne faites que les ajouter (append to)
Les PDF à référence hybride ne sont pas malformés (malformed) ; ce sont le propre pont de compatibilité (compatibility bridge) du format, et les applications Office continueront de les produire tant que les lecteurs PDF 1.4 survivront dans la base d'installation (install base). Un pipeline capable de repérer la clé /XRefStm, de valider le document fusionné avec le Composant PDFium, et de régénérer (regenerate) une sortie propre à index unique (clean single-index output) avec le Composant HotPDF les traite comme ce qu'ils sont : des entrées ordinaires avec un panneau de signalisation supplémentaire (extra signpost) dans la bande-annonce (trailer)