HotPDF écrit des fichiers PDF linéarisés, la disposition qu Acrobat nomme Fast Web View, via la propriété LinearizeOutput sur THotPDF. La définir avant BeginDoc fait réordonner par HotPDF le graphe d objets terminé afin qu un lecteur conscient des plages d octets puisse afficher la page un après avoir récupéré uniquement la partie initiale du fichier, au lieu de télécharger d abord le document entier. Le mécanisme est l Annexe F de la norme ISO 32000-1
La raison pour laquelle cela compte n a rien de glamour. Un PDF normal place sa table de références croisées à la fin, donc un visualiseur doit atteindre le dernier octet avant de savoir où quoi que ce soit se trouve. Donnez à un navigateur un rapport scanné de 200 pages et l utilisateur fixe une roue de chargement pendant le transfert complet, alors que la seule chose qu il voulait était la page 1. La linéarisation corrige cela en payant un coût au moment de l écriture. Cet article porte spécifiquement sur ce chemin d écriture, le partitionnement, la boucle de mesure et les limites strictes ; pour le contexte conceptuel sur ce que Fast Web View apporte, l explication précédente de la linéarisation PDF et Fast Web View couvre ce terrain
Ce que la disposition linéarisée garantit réellement
Un fichier linéarisé est un PDF ordinaire avec un ordonnancement physique extrêmement spécifique, et chaque garantie qu il offre provient de cet ordonnancement plutôt que d un quelconque nouveau type d objet. HotPDF émet les parties dans la séquence prescrite par l Annexe F : le dictionnaire de paramètres de linéarisation dans les 1024 premiers octets, une table de références croisées précoce, les objets au niveau document, le flux d indices primaire, la première page et ses objets privés, puis les pages restantes, puis les objets partagés, puis tout le reste, et enfin la table de références croisées principale
Le partitionnement est dérivé, pas déclaré. HotPDF parcourt le graphe de références depuis chaque objet page et enregistre, pour chaque objet indirect, combien de pages l atteignent et laquelle l a atteint en premier. Un objet utilisé par exactement une page devient privé à cette page. Un objet atteint par plus d une devient partagé. Le catalogue, plus tout ce qu il référence sous /ViewerPreferences, /OpenAction, /Threads et /AcroForm, plus le dictionnaire de chiffrement quand la protection est active, forment le groupe au niveau document qui doit précéder tout le reste. Les nœuds de l arbre de pages sont volontairement retenus afin de ne pas polluer la section de première page
Le dictionnaire de paramètres porte les chiffres dont un lecteur a besoin avant d avoir lu quoi que ce soit d autre : /L pour la longueur totale du fichier, /H pour le décalage et la longueur du flux d indices, /O pour le numéro d objet de la première page, /E pour l octet où se termine la section de première page, /N pour le nombre de pages et /T pour le décalage de l entrée de la table de références croisées principale. Chacun de ces éléments est un décalage d octets dans un fichier qui n existe pas encore au moment où vous devez les écrire
Pourquoi les décalages des tables d indices doivent-ils converger ?
Parce que les chiffres du dictionnaire de paramètres décrivent le fichier qui les contient, et que modifier l un d eux modifie le fichier. C est la difficulté centrale d un écrivain linéarisé, et c est pourquoi HotPDF mesure de façon répétée au lieu d écrire une seule fois. Élargissez /T de 6 chiffres à 7 et le dictionnaire de paramètres grandit d un octet ; l en-tête grandit ; chaque objet se décale ; la table de références croisées principale bouge ; /T a maintenant besoin d une valeur différente. La disposition doit atteindre un point fixe avant qu un seul octet de sortie réelle ne soit engagé
HotPDF gère cela avec une itération bornée. Il sérialise d abord chaque objet dans un flux compteur qui enregistre la longueur sans conserver les octets, de sorte que chaque objet a une taille sérialisée connue. Il exécute ensuite une passe de disposition qui attribue des décalages au groupe de niveau document, au flux d indices, au groupe de première page, aux groupes de pages ultérieures, au groupe partagé et au reste, et rapporte où la table de références croisées principale atterrirait. Ce résultat est réinjecté comme entrée pour la passe suivante. La boucle est plafonnée à huit tentatives, et une non-convergence lève une exception plutôt que de produire un fichier avec des décalages erronés d apparence plausible
CandidateMainOffset := 0;
for Attempt := 0 to 7 do
begin
CalculateLayout(CandidateMainOffset, FirstXRefData,
HintOffset, EndFirstPage, NewMainOffset);
if NewMainOffset = CandidateMainOffset then
Break;
CandidateMainOffset := NewMainOffset;
end;
if NewMainOffset <> CandidateMainOffset then
raise Exception.Create('Linearization layout did not converge');
Deux détails empêchent la boucle de s emballer. Le dictionnaire de paramètres est écrit dans un emplacement fixe de 384 octets, complété par des espaces, de sorte que sa propre croissance ne peut jamais déstabiliser la disposition ; si le texte du dictionnaire dépassait jamais cette réservation, HotPDF lèverait une exception plutôt que de tout décaler silencieusement. Et après convergence, HotPDF exécute une passe de disposition de confirmation supplémentaire et revérifie la longueur du flux d indices, car le flux d indices lui-même encode des décalages qui n étaient connus qu une fois la disposition stabilisée. Le bénéfice de toute cette mesure est que HotPDF ne met jamais en mémoire tampon une seconde copie du document : une fois les décalages fixés, les objets sont sérialisés directement dans le flux de destination, avec une assertion à chaque limite de section vérifiant que les octets écrits correspondent au décalage promis
L activer depuis Delphi
La surface de l API est un unique booléen, et sa seule exigence est que vous le définissiez avant que la génération ne commence. LinearizeOutput vaut False par défaut, et la passe de disposition s exécute lors de l écriture du document, donc l assigner après EndDoc n accomplit rien
var
PDF: THotPDF;
begin
PDF := THotPDF.Create(nil);
try
PDF.FileName := 'fast-view.pdf';
PDF.Version := pdf17;
PDF.LinearizeOutput := True; // must precede BeginDoc
PDF.BeginDoc;
PDF.Canvas.TextOut(72, 72, 'First page');
PDF.EndDoc;
finally
PDF.Free;
end;
end;
Une mise en garde de déploiement l emporte sur tout le reste côté code. La linéarisation ne rapporte que lorsque le transport prend en charge les requêtes HTTP par plage. Servez le même fichier depuis un point de terminaison qui le diffuse en entier, ou depuis une configuration CDN qui ignore Range, et vous vous êtes acheté un chemin d écriture plus lent et un fichier plus volumineux sans aucun gain visible pour l utilisateur. Vérifiez le serveur avant de vérifier le code
Pourquoi la linéarisation prime-t-elle sur UseXRefStream et UseObjectStreams ?
Parce que l écrivain linéarisé a besoin que chaque objet ait son propre décalage d octet directement adressable, et les deux fonctionnalités mentionnées retirent cela. HotPDF émet donc des tables de références croisées textuelles traditionnelles et des objets indirects non compactés chaque fois que LinearizeOutput est activé, même si l appelant a aussi défini UseXRefStream ou UseObjectStreams. C est un remplacement délibéré, pas un conflit que vous devez résoudre vous-même
Le raisonnement découle des tables d indices. Une table d indices décrit où commence une section de page et quelle est sa longueur, de sorte qu un lecteur puisse demander exactement cette plage. Un objet compacté dans un conteneur /ObjStm n a aucun décalage indépendant du tout ; il n existe que comme une tranche à l intérieur d un autre flux compressé qui doit être récupéré et décompressé comme une unité. Si vous comptiez sur les flux d objets pour la taille du fichier, comprenez que la linéarisation et la compression tirent ici dans des directions opposées, et lisez le compromis dans l article compagnon sur les flux d objets et les mises à jour incrémentielles dans HotPDF. La même tension façonne les fichiers à référence hybride, qui existent précisément pour garder les anciens lecteurs fonctionnels aux côtés des tables basées sur les flux, comme couvert dans l article sur les flux de références croisées hybrides dans les PDF générés par Office
Il existe aussi un plancher de version. La linéarisation nécessite PDF 1.2 ou ultérieur. Si la version sélectionnée est plus ancienne, HotPDF l élève automatiquement, à moins que StrictVersionLock ne soit défini, auquel cas l écriture lève une exception plutôt que de promouvoir silencieusement un document que vous avez délibérément fixé
Le mur des 4 Gio, et pourquoi HotPDF refuse plutôt que de tronquer
Les tables d indices de linéarisation stockent les décalages sous forme de valeurs 32 bits, donc un fichier linéarisé ne peut adresser rien à 4 Gio ou au-delà, et HotPDF rejette une telle sortie avec une exception explicite plutôt que d écrire un fichier avec des décalages qui bouclent. La limite n est pas un choix d implémentation de HotPDF ; c est la largeur des champs que l Annexe F définit
La vérification est appliquée à trois endroits, et les trois comptent. HotPDF valide chaque objet une fois sa longueur sérialisée connue, valide chaque longueur de section de page en construisant les entrées d indices, et valide la longueur finale du fichier une fois la table de références croisées principale dimensionnée. Échouer tôt est tout l intérêt : une table d indices avec un décalage silencieusement tronqué produit un fichier qui s ouvre correctement dans un visualiseur le téléchargeant en entier et échoue seulement pour le client par plage d octets que la linéarisation était censée servir, ce qui est le pire mode de défaillance possible car votre visualiseur de test ne le reproduit jamais. Si vous produisez une sortie de plusieurs gigaoctets, la linéarisation n est pas l outil, et l approche de streaming décrite dans les notes sur l API de fichier direct pour les flux de travail PDF volumineux est la direction à regarder
Détecter la linéarisation sur un fichier chargé
THotPDF.IsLoadedLinearized indique si le document actuellement chargé a déjà été écrit sous forme linéarisée, et répond à partir d un instantané pris avant l analyse, pas depuis le flux en direct. HotPDF lit les 1024 premiers octets depuis la position zéro du flux source, les scanne à la recherche du premier mot-clé obj puis d une entrée /Linearized avec la valeur 1, et met en cache le résultat booléen
var
PDF: THotPDF;
PageCount: Integer;
begin
PDF := THotPDF.Create(nil);
try
PageCount := PDF.LoadFromFile('incoming.pdf');
if (PageCount > 0) and (not PDF.IsLoadedLinearized) then
Writeln('Source is not Fast Web View ready');
finally
PDF.Free;
end;
end;
Deux contraintes dans cette description sont structurantes. La détection ne peut pas s appuyer sur la position du flux, car au moment où le code applicatif pose la question, l analyseur l a déjà déplacée, et elle ne peut pas relire à la demande car LoadFromFile libère le flux source interne une fois le chargement terminé. D où la conception capturer-avant-analyse-et-mettre-en-cache. Le balayage est aussi délibérément littéral sur la valeur : seul /Linearized 1 ou une forme numériquement équivalente avec une fraction entièrement nulle est acceptée, car un fichier dont le dictionnaire de paramètres dit autre chose ne tient pas la promesse de l Annexe F
Un piège d enregistrement Delphi qui mérite d être retenu
Les enregistrements locaux contenant des tableaux dynamiques initialisent leurs champs gérés et rien d autre, et si vous gardez un champ Count ordinaire à côté du tableau vous devez le vider vous-même. Cela a mordu le partitionnement de linéarisation durant le développement, et c est le genre de bogue qui coûte une journée précisément parce qu une plateforme le cache
type
THPDFLinearIndexList = record
Values: THPDFIntegerArray; // managed field: cleared for you
Count: Integer; // plain field: whatever was on the stack
end;
// Required, not cosmetic:
Part4 := Default(THPDFLinearIndexList);
Part6 := Default(THPDFLinearIndexList);
Part8 := Default(THPDFLinearIndexList);
Part9 := Default(THPDFLinearIndexList);
Le champ de tableau dynamique est à comptage de références, donc le compilateur le remet à zéro. Le Count à côté est un entier ordinaire sans une telle garantie, et un Count non initialisé envoie le tout premier ajout vers un index arbitraire. Sous Win32, l emplacement de pile contenait par hasard zéro, l ajout atterrissait à l index 0, et tous les tests passaient. Sous Win64, le même code écrivait au-delà de la fin du tableau. La leçon se généralise bien au-delà de la linéarisation : quand un enregistrement mélange des champs gérés et non gérés, assignez Default(TRecord) et cessez de réfléchir à quels champs le compilateur couvre, et ne traitez jamais une exécution Win32 réussie comme une preuve que l initialisation est correcte
Les membres LinearizeOutput et IsLoadedLinearized décrits ici sont livrés avec le HotPDF Component standard pour Delphi et C++Builder ; la page produit porte la référence complète des propriétés, y compris les règles d interaction avec les flux de références croisées, les flux d objets et le verrouillage de version