Article technique

Fusionner et scinder des PDF de plusieurs gigaoctets en Delphi avec l'accès direct PDF Library for Delphi

Fusionner ou scinder un PDF de deux gigaoctets par la voie évidente coûte deux choses à la fois : du temps de traitement et de l'espace d'adressage. La voie évidente, c'est de charger chaque entrée, de faire le travail, d'écrire la sortie. C'est le chargement qui casse tout. Une archive numérisée qui passe de 300 à 600 DPI double sa résolution linéaire et quadruple à peu près sur le disque ; le même job d'assemblage qui absorbait des fichiers de 400 Mo toute l'année se met à ramer dès qu'une entrée dépasse le gigaoctet, souvent alors qu'il ne fait rien de plus que compter des pages. La tâche n'est pourtant pas devenue plus difficile : ouvrir, compter, choisir des plages, concaténer, c'est tout. C'est le chargement de l'arbre complet qui a simplement cessé d'être un choix par défaut raisonnable à cette taille. PDF Library for Delphi, la bibliothèque PDF de losLab pour Delphi et C++Builder, y répond avec sa couche Direct Access : une famille de fonctions préfixées DA adossées à un lecteur en flux qui parcourt la table de références croisées sur place au lieu de construire tout le document en mémoire

Où part la mémoire lors d'un chargement complet

Charger un PDF « normalement » signifie analyser le xref, résoudre chaque objet indirect dans un arbre en mémoire, décoder les object streams, puis raccorder l'arbre des pages, les polices et les annotations à des objets manipulables. Pour des workflows d'édition, c'est le bon compromis. Pour des charges de fusion, de scission et d'inspection, c'est surtout du gaspillage : une archive numérisée de 30 000 pages peut contenir des millions d'objets indirects, alors qu'un job de scission n'a besoin d'en lire que quelques centaines — les nœuds de page de la plage demandée et ce qu'ils référencent

La couche Direct Access inverse ce modèle. DAOpenFile et DAOpenFileReadOnly analysent le trailer et le xref — quelques kilo-octets à la fin du fichier — puis renvoient un handle de fichier. Les objets sont récupérés paresseusement lorsqu'un appel en a besoin. En pratique, l'ouverture d'un fichier de plusieurs gigaoctets prend à peu près le même temps que celle d'un petit fichier, et la mémoire suit ce que vous touchez plutôt que ce que le fichier contient

Comparaison PDF Library for Delphi entre charger un PDF d'un gigaoctet dans un arbre d'objets entièrement en mémoire et l'ouvrir en accès direct, où l'analyse s'arrête au trailer et à la xref et un handle sert des lectures paresseuses objet par objet
Un chargement complet décode chaque objet indirect avant que la fusion puisse démarrer, si bien que la RAM et le temps d'ouverture croissent avec l'archive. La voie d'accès direct renvoie un handle fonctionnel après avoir lu quelques kilo-octets et laisse chaque appel ne tirer que les objets dont il a besoin

Sonder un énorme fichier sans le charger

Le schéma ci-dessous vient du benchmark grands fichiers de la bibliothèque elle-même : ouverture en lecture seule, questions ciblées, fermeture. Aucun arbre de document n'existe jamais

var
  Lib: TPDFlib;
  Handle, Pages: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Handle := Lib.DAOpenFileReadOnly('archive-2025.pdf', '');
    if Handle = 0 then
      raise Exception.Create('Direct access open failed');
    Pages := Lib.DAGetPageCount(Handle);
    Writeln('pages : ', Pages);
    Writeln('title : ', Lib.DAGetInformation(Handle, 'Title'));
    Lib.DACloseFile(Handle);
  finally
    Lib.Free;
  end;
end;

Le mode lecture seule mérite d'être préféré dès que possible : il permet à l'étape d'ingestion de tourner pendant que d'autres processus détiennent le fichier, et il documente l'intention — une étape de sondage qui appellerait accidentellement une fonction mutatrice échouera vite au lieu de corrompre l'archive

PageRef est un handle d'objet, pas un numéro de page

L'erreur la plus fréquente avec l'API DA consiste à passer un numéro de page là où une fonction attend un PageRef. Presque tous les appels DA par page — DAExtractPageText, DARenderPageToFile, DARotatePage, DACapturePage — prennent un handle de référence vers l'objet page, obtenu en traduisant le numéro visible par l'utilisateur via DAFindPage :

PDF Library for Delphi : flux de traduction numéro de page vers PageRef montrant DAFindPage alimentant les appels d'accès direct par page, contre une ref entière brute atterrissant sur un objet arbitraire et produisant silencieusement le texte d'une mauvaise page
Chaque appel d'accès direct par page consomme un PageRef produit par DAFindPage, jamais le numéro vu par l'humain. Omettre cette traduction fait se déguiser l'entier en identifiant d'objet, et du texte de mauvaise page peut partir invisible
PageRef := Lib.DAFindPage(Handle, 250);          // page number -> object handle
if PageRef <> 0 then
begin
  Text := Lib.DAExtractPageText(Handle, PageRef, 0);
  Lib.DARenderPageToFile(Handle, PageRef, 5, 150, 'page250.png');
end;

Passer directement le nombre 250 ne déclenche pas d'erreur : cela adresse l'objet qui se trouve derrière cette valeur de handle, ce qui échoue visiblement les bons jours et extrait le texte de la mauvaise page dans un document client les mauvais jours. Si vous enveloppez la couche DA dans votre propre code de service, rendez la traduction impossible à oublier : acceptez les numéros de page à la frontière, appelez DAFindPage immédiatement, puis ne transmettez que des refs en interne

Fusionner des centaines de fichiers avec une liste nommée

Pour deux fichiers, MergeFiles(First, Second, Output) suffit. L'assemblage par lot se dimensionne mieux avec des listes de fichiers : enregistrez les entrées sous un nom de liste, puis fusionnez cette liste en un seul passage

PDF Library for Delphi : workflow de liste de fichiers nommée où les relevés de janvier, février et mars s'enregistrent sous un nom de liste et fusionnent en une seule passe, les variantes Fast, défaut et stricte échangeant la préservation de l'arbre de structure contre la vitesse
Des centaines d'entrées enregistrées se réduisent en une seule passe MergeFileList dont le résultat se vérifie en millisecondes par une autre sonde en lecture seule. La variante est une décision propre à chaque pipeline car Fast abandonne l'arborescence de structure du PDF balisé
Lib.AddToFileList('Statements', 'jan.pdf');
Lib.AddToFileList('Statements', 'feb.pdf');
Lib.AddToFileList('Statements', 'mar.pdf');
Lib.MergeFileList('Statements', 'q1-statements.pdf');

// Vérifie le résultat à moindres frais : de nouveau en accès direct
Handle := Lib.DAOpenFileReadOnly('q1-statements.pdf', '');
Writeln('merged pages: ', Lib.DAGetPageCount(Handle));
Lib.DACloseFile(Handle);

La famille de fusion comporte trois variantes, et la différence ne se limite pas à la vitesse. MergeFileListFast ignore la préservation de l'arbre de structure ; MergeFileListStrict impose le mode strict ; la version sans suffixe est le choix équilibré par défaut. La règle opérationnelle qui en découle est simple : si une entrée est un Tagged PDF dont la structure d'accessibilité doit survivre — tout ce qui est produit pour PDF/UA, par exemple — utilisez la variante par défaut ou Strict, car Fast supprimera silencieusement l'arbre de structure. Pour de simples archives de scans sans balisage, Fast offre une performance gratuite. Décidez par pipeline, pas selon l'humeur du développeur, et consignez la variante utilisée dans le journal du job

Scinder sans chargement : extraction de plages

La scission suit la même philosophie sans chargement. ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) extrait une plage de pages directement de fichier à fichier — '1-500', '501-1000' ou des sélections séparées par des virgules — sans que la source devienne un arbre de document. Lorsqu'un document est déjà chargé pour d'autres raisons, ExtractPageRanges produit un nouveau document en mémoire depuis l'actuel, et CopyPageRanges récupère des plages depuis un autre document chargé par ID. Pour scinder par relevé des flux d'impression consolidés, la forme fichier vers fichier est celle qui empêche une entrée de 4 Go de gonfler en RAM

Les fichiers qui mentent sur leur géométrie

Les pipelines grands fichiers rencontrent des fichiers endommagés à un rythme que les petits fichiers ne connaissent pas, simplement parce que les entrées traversent plus de systèmes. Deux formes d'échec méritent un traitement explicite

D'abord, les en-têtes décalés. Les passerelles mail et les spoolers d'impression ajoutent parfois des octets au début d'un PDF ; le marqueur %PDF n'est alors plus à l'offset 0 et tous les offsets xref du fichier sont faux du même décalage. Le lecteur en flux le détecte et l'expose — DAShiftedHeader au niveau flat, ShiftedHeader sur TSmartPDFReader — puis compense pendant les lectures. Les calculs d'offset faits maison ne le font généralement pas, ce qui explique le symptôme classique « fonctionne sur tous les fichiers que nous générons, échoue sur ceux du client X »

Ensuite, les tables de références croisées cassées. DACopyFile(InputFileName, OutputFileName, PageCount) diffuse tout le fichier vers une nouvelle copie en reconstruisant le xref, et renvoie le nombre de pages comme sous-produit. L'exécuter comme étape de normalisation avant un consommateur aval exigeant transforme une classe d'échecs intermittents d'analyse en une étape de réparation prévisible. Et lorsque vos propres modifications doivent être enregistrées, DAAppendFile les écrit comme une mise à jour incrémentale — en ajoutant une nouvelle révision plutôt qu'en réécrivant des gigaoctets, ce qui rend le coût d'enregistrement proportionnel au changement, pas au fichier

Détails de livraison : linéarisation et composition

Deux capacités voisines complètent un pipeline grands fichiers. Lorsque la sortie assemblée est servie en HTTP pour affichage dans le navigateur, LinearizeFile la réorganise pour la diffusion par plages d'octets, afin que la première page s'affiche avant que le reste d'un paquet de 500 Mo soit téléchargé — à faire en dernière étape, après toutes les fusions, car toute modification ultérieure délinéarise le fichier. Et lorsque les paquets demandent de la composition plutôt qu'une simple concaténation — une page de garde tamponnée derrière chaque relevé, deux pages source imposées sur une feuille de sortie — DACapturePage transforme n'importe quelle page en gabarit réutilisable que DADrawCapturedPage place sur une page de destination dans un rectangle arbitraire, toujours sans chargement complet de la source de plusieurs gigaoctets

Limites et ce qui reste en lecture seule

Le format lui-même manque de place bien avant Direct Access. Les offsets sont en Int64 dans toute la couche DA ; les vraies limites sont donc le disque disponible et le champ d'offset xref à 10 chiffres des tables de références croisées classiques, celles qui ne sont pas stockées en flux. Les archives numérisées de plusieurs gigaoctets n'ont rien d'exceptionnel en pratique, et la mémoire reste bornée quelle que soit la taille du fichier parce que les objets ne sont lus que lorsqu'un appel les réclame

Deux questions reviennent assez souvent pour mériter une réponse directe. La fusion par le chemin par défaut transporte la structure du document : les signets et les liens survivent ; c'est la variante Fast qui échange l'arbre de structure contre de la vitesse, et c'est toute la raison de la réserver aux entrées non balisées. La bonne habitude consiste à ouvrir la sortie fusionnée, à parcourir son plan et à contrôler quelques liens internes avant de la livrer. Quant à l'édition, il existe un juste milieu utile entre le sondage en lecture seule et le chargement complet : les opérations au niveau page travaillent directement sur le handle — DARotatePage, DAMovePage et DAHidePage parmi elles — de même que les lectures de champs de formulaire, et DAAppendFile persiste ces modifications sous forme de révision incrémentale. L'édition au niveau du contenu, tout ce qui réécrit les opérateurs de marquage à l'intérieur d'une page, reste du ressort de la couche document complète

Articles liés

Si votre sortie fusionnée doit rester accessible, le contexte sur l'arbre de structure est traité dans l'article sur l'accessibilité Tagged PDF — il explique ce que la variante de fusion Fast supprimerait. Pour extraire du contenu des plages que vous scindez, consultez le guide d'extraction de texte, d'images et de polices

La liste complète des fonctions Direct Access est livrée avec la bibliothèque ; les éditions et téléchargements d'essai sont disponibles sur la page produit PDF Library for Delphi