Vous voulez un dictionnaire hors d’un PDF de 2 Go et l’outil développe d’abord toute la table des références croisées en un tableau dimensionné par le /Size de la remorque. PDFiumPas remplace cette étape par un index d’objets épars et paresseux : il ne garde que les descripteurs de sections xref, résout un seul numéro d’objet à la demande à travers des fenêtres bornées, et ne met en cache que les entrées que vous avez réellement touchées
La vieille forme de ce code dans FPdfCompress était honnête mais coûteuse. ApplyDefaultOpenAction lisait le fichier complet dans un seul TBytes, puis allouait un tableau dense TPdfActiveXrefEntries avec un emplacement par numéro d’objet jusqu’à /Size. Deux choses tournaient mal à grande échelle. Le coût de lecture croissait linéairement avec la taille du document même quand l’appelant voulait quatre dictionnaires, et le tableau dense entrait en collision avec le budget de l’analyseur : TPdfParserResourceBudget.Default règle MaxObjects à 4 000 000, si bien qu’un fichier parfaitement valide dont le plus haut numéro d’objet se tient au-dessus de ce plafond était rejeté sur un argument de mémoire plutôt que de correction
Pourquoi l’API publique de PDFium ne répond-elle pas à cette question ?
Parce que l’information existe à l’intérieur de PDFium mais ne franchit jamais la frontière C. CPDF_Parser maintient en interne la table des références croisées, l’appartenance aux object streams et la précédence des révisions, pourtant les en-têtes publiés n’exposent aucun point d’entrée qui prenne un numéro d’objet et renvoie son décalage brut, sa génération, quelle révision a gagné, ou dans quel ObjStm il vit. Le côté sauvegarde est tout aussi fermé : FPDF_SaveAsCopy et FPDF_SaveWithVersion ne vous remettent qu’un rappel d’écriture séquentielle. Tout correctif au niveau des octets d’un catalogue après une sauvegarde native doit donc être construit dans la couche Pascal, voilà pourquoi PDFiumPas analyse lui-même ces structures au lieu de réutiliser la DLL
Que garde réellement l’index épars en mémoire ?
Des descripteurs, pas des entrées. Pour une table classique (ISO 32000-1 §7.5.4) une TPdfSparseXrefSubsection stocke le premier numéro d’objet, le compte d’objets, le décalage d’octet où commencent les rangées d’entrées et la largeur d’entrée mesurée. Les entrées elles-mêmes restent dans le fichier. La largeur est mesurée depuis la première rangée plutôt que supposée être 20 octets, parce que les producteurs divergent sur les fins de ligne ; PDFiumPas accepte 18 à 64 et rejette tout ce qui est hors de cette bande, de même que toute sous-section dont le compte déclaré courrait au-delà de la fin du flux. Pour un flux de références croisées (§7.5.8) la section porte les trois largeurs de champs /W, chacune contrainte entre 0 et 8, les paires /Index aplaties, et les octets d’entrée décodés, dont la longueur attendue est calculée depuis /W et /Index avant qu’un seul octet soit gonflé
L’index entier est construit par Initialize depuis une fenêtre de queue d’au plus 1 MiB, qui est où startxref est trouvé, et chaque lecture d’objet subséquente utilise une fenêtre d’objet de 1 MiB. Le plafond du flux brut est 64 MiB et une seule ligne xref ne peut pas dépasser 1024 octets. Si vous avez lu notre note sur valider les object streams et flux de références croisées avec PDFiumPas, la même discipline de largeur de champ s’applique ici, seulement elle sert maintenant à adresser une entrée au lieu d’auditer toute une table
uses
FPdfCompress;
var
Source: TFileStream;
Revision: TPdfSparseRevisionInfo;
begin
Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
{ walks startxref, the /Prev chain and the catalog only }
if ReadPdfSparseRevisionInfo(Source, Revision) then
begin
Writeln('root ', Revision.RootObjectNumber, ' ',
Revision.RootGeneration);
Writeln('max obj ', Revision.MaximumObjectNumber);
Writeln('xref str ', Revision.UsesXrefStream);
Writeln('encrypted ', Revision.HasEncrypt);
Writeln(string(Revision.CatalogDictionary));
end;
finally
Source.Free;
end;
end;
Comment une consultation atteint-elle un objet ?
Par arithmétique, dans les deux dispositions. Une sous-section classique a des rangées à largeur fixe, si bien que l’adresse d’une entrée est le début de la sous-section plus le décalage d’objet fois la largeur mesurée ; PDFiumPas lit ensuite cette seule ligne, analyse le décalage à dix chiffres et la génération à cinq chiffres, vérifie la génération contre le plafond de 65535 du §7.5.4, et classe le mot-clé final comme axkDirect ou axkFree. Un flux de références croisées exige une étape de plus parce que les sous-sections /Index sont concaténées dans la suite d’octets décodée, si bien que l’index accumule les comptes des sous-sections précédentes avant de multiplier par la largeur /W sommée. Le type 1 produit un décalage, le type 2 produit un numéro d’object stream et un index de membre, et tout le reste devient axkUnknown plutôt qu’une supposition
{ classic table, ISO 32000-1 section 7.5.4 }
EntryOffset := Subsection.EntryOffset +
Int64(ObjectNumber - Subsection.FirstObject) * Subsection.EntryWidth;
{ cross-reference stream, ISO 32000-1 section 7.5.8 }
EntryWidth := Section.Widths[0] + Section.Widths[1] + Section.Widths[2];
EntryPosition := Integer((PriorCount + ObjectNumber -
Section.IndexValues[I]) * EntryWidth);
Rien dans l’un ou l’autre chemin n’est proportionnel à /Size. C’est tout l’intérêt de la réécriture : la valeur de taille de la remorque est portée vers l’avant comme métadonnée et utilisée à l’écriture de la révision incrémentielle, mais elle ne pilote jamais une allocation. La suite de régression l’épingle avec une fixture dont l’arbre de pages vit aux objets 1 000 000 000 et 1 000 000 001 sous une remorque déclarant /Size 1000000002. La vieille implémentation dense refusait ce fichier ; l’index épars résout les deux références et préserve la taille déclarée dans la remorque de sortie
Révisions hybrides, chaînes /Prev et gardes autour
La précédence des révisions est là où un index paresseux naïf se trompe. PDFiumPas parcourt la chaîne depuis startxref en ordre du plus récent au plus ancien et arrête une consultation à la première section qui répond, ce qui reproduit la règle de précédence sans matérialiser une table fusionnée. Les fichiers à référence hybride (§7.5.8.4) sont traités à l’intérieur de la branche classique : quand la remorque porte un /XRefStm, la section de flux supplémentaire est enregistrée avant la section classique qui l’a nommée, si bien que les objets compressés invisibles à la table plate sont quand même trouvés pendant que les entrées classiques gardent leur rang. Les révisions plus anciennes sont ensuite suivies à travers /Prev
Deux gardes bornent ce parcours, et toutes deux comptent sur des fichiers endommagés. Chaque décalage visité est enregistré, si bien qu’un /Prev pointant en retour dans la chaîne se termine au lieu de tourner en rond, et la profondeur de parcours est plafonnée par MaxRecursionDepth, qui vaut 1024 par défaut. Le drapeau de chiffrement est accumulé à travers toute la chaîne plutôt que lu de la seule remorque la plus récente, parce qu’un document dont la dernière remorque omet /Encrypt peut quand même être chiffré plus en arrière ; les appelants qui ajoutent des révisions comptent sur ce drapeau pour refuser d’écrire des objets en clair dans un fichier chiffré
Entrées de type 2 : pourquoi l’object stream attend
Une entrée de type 2 nomme un object stream, et PDFiumPas ne touche pas ce flux avant qu’un appelant demande un membre de lui. Quand il le fait enfin, /Type /ObjStm est vérifié, /N est vérifié contre le budget d’objets et /First contre le plafond d’octets décodés, et /N est soumis à une vérification de bon sens contre /First puisque chaque paire d’en-tête a besoin d’au moins quatre octets. Seulement alors le flux est gonflé, et le balayage d’en-tête s’arrête au membre demandé et à son successeur plutôt que de construire une table de membres complète. Un seul object stream décodé est retenu à la fois, ce qui est le bon compromis quand une branche d’arbre de pages se regroupe dans un seul ObjStm ; notre exposé sur le décodage d’object streams et de prédicteurs en Delphi couvre ce qui se passe à l’intérieur de cette étape de gonflement (§7.5.7)
var
Reader: TPdfSparseDictionaryReader;
Generation: Integer;
Dict: AnsiString;
begin
{ one retained index, many generation-aware reads }
Reader := TPdfSparseDictionaryReader.Create(Source);
try
if Reader.Valid and
Reader.ReadLatestDictionary(PageObjectNumber, Generation, Dict) then
HandlePage(PageObjectNumber, Generation, Dict);
finally
Reader.Free; { Source stays yours }
end;
end;
Là où le cache cesse de tenir ses promesses
L’index est un instantané, et il vaut la peine d’être franc là-dessus. Les sections sont analysées une fois dans Initialize ; si le flux sous-jacent est modifié ensuite, chaque entrée en cache est périmée et la classe ne le remarquera pas. TPdfSparseDictionaryReader détient l’index pour la durée de vie de la source possédée par l’appelant, ce qui est exactement ce que veut un parcours récursif d’un arbre de pages et exactement ce que vous ne devez pas faire à travers une réécriture. Le cache d’entrées est un tableau plat cherché linéairement et il stocke aussi les résultats négatifs, si bien que quelques centaines de consultations sont bon marché et quelques centaines de milliers ne le sont pas. ReadDictionary exige une correspondance de génération exacte tandis que ReadLatestDictionary résout la génération active, et la différence est délibérée : la résolution de références a besoin de la première, l’inspection de catalogue de la seconde. Là où ces bornes ne peuvent pas être honorées, les unités environnantes retombent sur l’analyseur hérité du fichier entier au lieu de rétrécir l’ensemble des fichiers qui marchent encore, un motif que nous utilisons aussi pour le streaming à la demande de gros PDF
Les régressions multi-compilateurs couvrent le même comportement sur les trois chaînes d’outils, y compris une assertion qu’une source de 2 MiB ne voit jamais une seule lecture plus grande que 1 MiB. Si vous maintenez du code Delphi, C++Builder ou Lazarus qui touche la structure PDF directement et que vous en avez assez de payer des coûts d’analyse du fichier entier pour quatre dictionnaires, l’index épars et la couture publique autour sont livrés dans le PDFiumPas Delphi PDFium component