PDFlibPas peut ouvrir un PDF local au moyen d’une vue mappée en mémoire, bornée et en lecture seule : LoadFromMappedFile et DAOpenMappedFile conservent exactement une fenêtre glissante sur le fichier, la remappent à la demande et fournissent chaque fragment d’objet par des lectures à offset absolu. La bibliothèque PDF pour Delphi ne conserve jamais la totalité de la source en mémoire, de sorte que l’espace d’adressage reste stable lorsque le fichier grandit. Cette conception vise une charge précise : les PDF de plusieurs gigaoctets dont l’analyse est terminée mais qui continuent d’être relus sur disque, objet par objet et fragment de flux par fragment de flux
Pourquoi les lectures dispersées restent-elles coûteuses une fois le PDF chargé ?
Charger un PDF ne signifie pas avoir fini de le lire, et sur un fichier de plusieurs gigaoctets c’est dans cet écart que se trouve le temps passé. Une table de références croisées ou un flux de références croisées (ISO 32000-1 §7.5.4 et §7.5.8) indique seulement où commence chaque objet indirect. Les octets arrivent plus tard, lorsqu’une page est rendue, qu’un programme de police est décodé ou qu’un flux de fichier incorporé (ISO 32000-1 §7.11.4) est extrait. Une archive de 2 Go contenant des dizaines de milliers d’objets devient des dizaines de milliers de petites lectures désordonnées, dont aucune n’est connue au moment du chargement
Ces lectures passaient auparavant par un Seek partagé suivi d’un Read sur un flux positionnel, et cette approche échoue dans deux directions à la fois. Chaque fragment paie une lecture de fichier même lorsque la page réside déjà dans le cache du système d’exploitation, et le curseur est un état mutable partagé ; un fichier local et la source par plages d’octets derrière le chargement progressif de PDF par plages avec prélecture ne pouvaient donc pas exécuter le même code d’analyse sans se disputer la position. PDFlibPas corrige les deux problèmes en transformant la lecture à offset absolu, d’optimisation en contrat
Que garantit TPDFReadAtStream ?
TPDFReadAtStream garantit une lecture à un offset absolu qui ne dépend pas du curseur logique du flux et ne le modifie pas. C’est un descendant abstrait de TStream qui ne comporte exactement qu’une méthode virtuelle, et les deux sources indépendantes du curseur dans la bibliothèque en dérivent : TReadOnlyMappedFileStream pour les fichiers locaux et TByteRangeStream pour les sources distantes servies par plages. Le lecteur de fragments d’objet vérifie une fois si sa source est un TPDFReadAtStream et revient à l’ancienne séquence seek-then-read dans le cas contraire ; un flux de fichier ordinaire ou un flux mémoire continue donc de fonctionner sans changement
type
// Flux en lecture seule dont les lectures absolues évitent un Seek puis Read partagés
TPDFReadAtStream = class(TStream)
public
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; virtual; abstract;
end;
// Accès fenêtré en lecture seule à un fichier local
TReadOnlyMappedFileStream = class(TPDFReadAtStream)
private
FMemoryMapped: Boolean;
public
constructor Create(const FileName: WideString; WindowSize: Int64 = 0);
function GetStats: TPDFMappedFileStats;
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; override;
property MemoryMapped: Boolean read FMemoryMapped;
end;
La distinction compte davantage que ne le laisse penser la signature. ReadAt utilise l’offset qui lui est transmis et laisse Position exactement là où il était, ce qui permet aux niveaux imbriqués de l’analyse d’effectuer des lectures sans sauvegarder puis restaurer la position autour de chaque appel. TReadOnlyMappedFileStream implémente toujours Read, Seek et Size comme n’importe quel autre TStream, Seek limite la position logique au fichier et Write renvoie toujours 0 parce que la source est ouverte en lecture seule
Ouvrir un PDF par une vue mappée dans Delphi
Deux points d’entrée explicites ouvrent une source mappée, sans modifier le comportement des points d’entrée que vous utilisez déjà. LoadFromMappedFile charge et sélectionne un document ; DAOpenMappedFile renvoie un handle Direct Access sur le même fichier, mode à privilégier pour fusionner et scinder des PDF de plusieurs gigaoctets par Direct Access. LoadFromFile et DAOpenFile conservent leurs sémantiques de partage de fichier, d’erreur et de compatibilité, de sorte que rien ne change pour les appelants qui n’activent pas la fonctionnalité. Les deux points d’entrée mappés prennent un WindowSize demandé en octets et un masque de bits Options, et tous deux acceptent 0 pour l’un ou l’autre
var
Pdf: TPDFlib;
Payload: AnsiString;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
// WindowSize 0 sélectionne la valeur par défaut de 64 MiB ; le mappage est obligatoire ici
if Pdf.LoadFromMappedFile('archive-2026.pdf', '', 0,
PDF_MAPPED_FILE_REQUIRE_MAPPING) <> 1 then
raise Exception.CreateFmt('mapped open refused, LastErrorCode=%d',
[Pdf.LastErrorCode]);
// L’extraction différée parcourt désormais des fenêtres mappées au lieu d’effectuer des Seek
Payload := Pdf.GetEmbeddedFileContentToString(1);
if Pdf.GetMappedFileInfo(Info) = 1 then
Writeln(Info);
finally
Pdf.Free;
end;
end;
Qu’impose réellement PDF_MAPPED_FILE_REQUIRE_MAPPING ?
PDF_MAPPED_FILE_REQUIRE_MAPPING transforme un repli silencieux en échec immédiat et diagnostiquable au moment de l’ouverture. Lorsque Options vaut 0, les deux points d’entrée acceptent un repli vers un flux de fichier en lecture seule : si la plateforme ne possède pas de code de mappage ou si l’appel de mappage échoue, le document s’ouvre tout de même et chaque lecture passe par un flux de fichier normal. Avec le flag activé, PDFlibPas n’accepte l’entrée que lorsque la première vue a été établie et signale le refus par LastErrorCode 401, plutôt que de charger un document qui se comporterait discrètement exactement comme l’ancien chemin
Sous Windows, le flux mappé ouvre un second handle en lecture seule avec FILE_SHARE_READ, FILE_SHARE_WRITE et FILE_SHARE_DELETE, ainsi que FILE_FLAG_RANDOM_ACCESS, crée sur celui-ci un mappage PAGE_READONLY et mappe la première fenêtre dans le constructeur. Le mappage immédiat est tout l’intérêt : un échec « mapping required » apparaît à LoadFromMappedFile, pas lors de la première lecture différée d’un objet au milieu d’un rendu. Il faut toutefois bien cerner la limite de la garantie. Le code de mappage n’est compilé que pour les cibles Windows, et un fichier de taille nulle ne tente jamais de mappage ; PDF_MAPPED_FILE_REQUIRE_MAPPING est donc une demande qui peut légitimement échouer, pas une promesse portable. Un WindowSize négatif ou tout bit de Options autre que la valeur documentée est rejeté immédiatement avec la même erreur 401
Une fenêtre, remappée selon la granularité d’allocation
Une seule vue est conservée à la fois, ce qui rend l’utilisation de l’espace d’adressage indépendante de la taille du fichier. Un WindowSize de 0 sélectionne 64 MiB ; une valeur inférieure à la granularité d’allocation du système est relevée jusqu’à cette granularité ; une valeur supérieure à 1 Gio est plafonnée ; et le résultat est arrondi à un nombre entier d’unités de granularité, soit 65536 octets sous Windows sauf si GetSystemInfo signale un autre dwAllocationGranularity. Lorsqu’une lecture tombe hors de la vue courante, PDFlibPas la démappe, aligne l’offset demandé vers le bas sur une limite de granularité et mappe une nouvelle fenêtre à cet endroit. La dernière fenêtre est limitée à la taille physique du fichier, afin que la vue ne dépasse jamais la fin du fichier
Une seule lecture peut franchir autant de fenêtres que nécessaire : la boucle copie ce que la vue courante peut fournir, remappe puis continue, et une demande qui dépasse la fin renvoie un nombre d’octets court au lieu d’échouer. PDFlibPas ne fait délibérément pas ce qui consisterait à vous remettre un pointeur dans la vue, car la prochaine lecture traversant une fenêtre l’invaliderait et aucun appelant ne pourrait raisonnablement s’en protéger. Les octets mappés sont copiés directement dans les tampons de destination appartenant à l’analyseur, ce qui supprime le tampon d’entrée de fichier supplémentaire et les changements de position, mais la bibliothèque ne promet pas un stockage final sans copie. Le fenêtrage en lecture se compose aussi avec l’écriture, puisque le décalage des références au niveau des octets lors d’une fusion PDF rapide extrait les octets d’objet pendant que la source mappée les fournit. Le compromis sur la taille de fenêtre est évident : une fenêtre plus petite utilise moins d’espace d’adressage et remappe plus souvent, ce qui est généralement le bon choix dans un processus 32 bits
Ce que protège le verrou et ce que signale GetMappedFileInfo
Une seule section critique couvre la vue mappée, le curseur du fichier de repli, la position logique et les statistiques, et la séparation entre les deux méthodes de lecture en découle directement. ReadAt prend le verrou et appelle le lecteur interne sans verrou ; Read prend le même verrou, appelle ce lecteur interne à la position logique courante puis l’avance. Réutiliser la fonction interne plutôt que la méthode publique ReadAt évite le verrouillage récursif, et conserver le verrou pendant toute la boucle de copie garantit qu’un remappage d’une seule fenêtre reste correct lors d’appels concurrents. Un détail Free Pascal mérite d’être connu avant un portage : l’unité FPC Windows déclare son propre enregistrement nommé TCriticalSection, de sorte que le champ et sa construction doivent s’écrire SyncObjs.TCriticalSection. Delphi compile volontiers la forme non qualifiée ; FPC la résout vers un enregistrement dépourvu de Create, Enter et Leave
var
Pdf: TPDFlib;
Handle, PageRef: Integer;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
Handle := Pdf.DAOpenMappedFile('archive-2026.pdf', '',
16 * 1024 * 1024, PDF_MAPPED_FILE_REQUIRE_MAPPING);
if Handle = 0 then
Exit;
try
PageRef := Pdf.DAFindPage(Handle, 1);
Writeln(Pdf.DAExtractPageText(Handle, PageRef, 0));
// {"memoryMapped":true,"fileSize":...,"remapCount":...}
if Pdf.DAGetMappedFileInfo(Handle, Info) = 1 then
Writeln(Info);
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
memoryMappedvaut false lorsque le repli portable vers un flux de fichier est actif ; c’est le seul champ qui prouve qu’aucun mappage n’a été établiwindowSizeest la fenêtre alignée effective plutôt que la valeur demandée, etmappedByteslui est inférieur dans la fenêtre de finmappedOffsetest le début aligné sur l’allocation de la vue conservée, ou -1 lorsqu’aucune vue n’est activereadCallscompte les demandes de lecture réussies dans les limites,bytesReadcompte les octets copiés vers les appelants etremapCountinclut la vue initiale
Les régressions ciblées couvrent les lectures absolues franchissant une fenêtre, la conservation du curseur logique, les lectures courtes en fin de fichier, les offsets invalides, les écritures rejetées, le remappage entre des fenêtres séparées, l’extraction différée d’une pièce jointe incompressible de 220 Ko et l’invalidation des statistiques après DACloseFile ; les suites headless Win32 et Win64 ont chacune découvert 1467 tests et les ont tous réussis, sans test ignoré, échoué, en erreur ou fuite. Si vous travaillez avec des PDF de plusieurs gigaoctets dans Delphi ou C++Builder et que votre profileur pointe sans cesse vers les lectures de fichiers plutôt que vers l’analyse, les points d’entrée de fichier mappé méritent un après-midi de mesures, et GetMappedFileInfo vous dira si vous avez réellement obtenu un mappage. La référence API complète et une version d’essai sont disponibles sur la page de la bibliothèque PDF PDFlibPas pour Delphi