Article technique

Traiter de grands PDF dans Delphi : Direct File API de HotPDF

Le job n'avait rien d'extraordinaire : un service Delphi nocturne qui reçoit des archives hypothécaires numérisées, compte les pages et route chaque fichier vers la bonne file de traitement. Il a fonctionné sans bruit pendant des mois, jusqu'à l'arrivée d'une archive de 1,4 Go. LoadFromFile parse les données de références croisées et matérialise un objet pour chacun des centaines de milliers d'objets indirects du fichier ; dans un service 32 bits, cet arbre a poussé le working set au-delà du plafond d'espace d'adressage de 2 Go au milieu du parse. La correction n'était pas un serveur plus gros. Le job ne posait jamais qu'une question, combien de pages, et y répondre n'exigeait jamais de charger le document.

La Direct File API de HotPDF existe exactement pour cette classe de travaux : des opérations PDF au niveau fichier depuis Delphi et C++Builder, qui lisent depuis le disque ce dont elles ont besoin au lieu de matérialiser tout le modèle documentaire. Savoir à quel niveau de l'API appartient une opération fait la différence entre un service à mémoire stable et un service qui tombe sur la première entrée surdimensionnée.

Ce que le chargement complet apporte, et ce qu'il coûte

Charger un document avec LoadFromFile donne un accès aléatoire à tout : n'importe quelle page, n'importe quel objet, prêts pour restructuration, modifications de contenu ou resérialisation par SaveLoadedDocument. Cette puissance est le bon outil pour la manipulation de pages ; InsertPagesFromDocument et MovePage ont besoin de l'arbre. Le coût est proportionnel au document, pas à votre opération : le temps de parse augmente avec le nombre d'objets, et la mémoire résidente atteint un multiple de la taille du fichier une fois les structures d'objets et les flux décodés comptés.

Le décalage apparaît quand les tailles d'entrée sont non bornées. Téléversements clients, sorties de scanners et archives vieilles de dix ans ne respectent pas les hypothèses d'un corpus de test. Un pipeline qui charge chaque entrée a des besoins mémoire fixés par le plus gros fichier que quelqu'un soumettra un jour ; un pipeline qui utilise des lectures par handle pour les questions qui le permettent a des besoins mémoire à peu près constants. Pour un service de longue durée, cette distinction compte davantage que la vitesse brute.

Passer le service en 64 bits relève le plafond mais ne change pas l'économie : un worker qui parse un fichier de taille gigaoctet dépense toujours des secondes de CPU et un multiple du fichier en RAM pour répondre à des questions auxquelles la structure du fichier peut répondre directement. La concurrence aggrave encore la situation : quatre grands chargements simultanés se disputent le même budget mémoire, et le débit s'effondre précisément quand la file est la plus chargée.

Inspection par handle

Le niveau lecture seule ouvre le fichier comme un handle, répond aux questions structurelles et le ferme : pas d'arbre d'objets, pas de rendu de page, pas de mémoire proportionnelle.

var
  Pdf: THotPDF;
  Handle, PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Handle := Pdf.DAOpenFileReadOnly('archive-2026-06.pdf', '');
    if Handle > 0 then
    try
      PageCount := Pdf.DAGetPageCount(Handle);
      RouteByPageCount('archive-2026-06.pdf', PageCount);
    finally
      Pdf.DACloseFile(Handle);
    end;
  finally
    Pdf.Free;
  end;
end;

Trois règles gardent ce niveau fiable. Vérifiez le handle : un retour non positif signifie que l'ouverture a échoué, et appeler DAGetPageCount sur un handle mort est le type d'erreur qui n'apparaît que sur le fichier malformé envoyé par un client. Associez chaque ouverture réussie à DACloseFile dans un bloc finally, car un service qui fuit des handles se dégrade lentement au lieu d'échouer visiblement. Et connaissez le coût du paramètre mot de passe : DAOpenFileReadOnly en accepte un, mais pour les entrées chiffrées il retombe en interne sur un parse complet pour répondre au comptage de pages ; la propriété de mémoire plate ne vaut que pour les fichiers non chiffrés, les entrées protégées devant passer d'abord par DecryptFile.

Le niveau handle fournit aussi une porte de triage économique. Les fichiers arrivent de clients mal étiquetés, tronqués par des uploads échoués ou renommés depuis d'autres formats ; une sonde DAOpenFileReadOnly les rejette en millisecondes à l'entrée, avec une erreur claire attachée au bon fichier, au lieu de les laisser échouer au fond d'un worker de file où le diagnostic coûte un après-midi.

Opérations fichier complet : copier, déchiffrer, chiffrer

Le deuxième niveau transforme des fichiers complets sans exposer leurs internes : ce sont les outils de base des pipelines d'ingestion.

// Structural copy: validate-and-move without parsing the object tree
Status := Pdf.DACopyFile('incoming\statement.pdf', 'verified\statement.pdf');
LogDirectFileStatus('copy', Status);

// Decrypt while copying: the Direct File route into protected inputs
Status := Pdf.DecryptFile('incoming\protected.pdf',
  'verified\plain.pdf', 'batch-password');
LogDirectFileStatus('decrypt-copy', Status);

// Encrypt while copying: protect an output without a full load
Status := Pdf.EncryptFile('verified\statement.pdf',
  'outbound\statement.pdf', 'owner-secret', '', aes256, [prPrint]);
LogDirectFileStatus('encrypt-copy', Status);

Chaque appel a un rôle distinct. DACopyFile est la copie validée depuis un répertoire de quarantaine vers le stockage géré ; elle ouvre et indexe la structure PDF au passage, de sorte qu'une entrée tronquée ou non PDF échoue ici au lieu de trois étapes plus tard. DecryptFile produit une copie déchiffrée, en empruntant un chemin de réécriture AES-256 direct qui évite de construire l'arbre d'objets lorsque l'entrée le permet, équivalent grands fichiers du flux charger et réenregistrer décrit dans l'article sur le chiffrement AES-256. EncryptFile est l'image miroir, appliquant la protection par mot de passe pendant une copie au niveau fichier avec les mêmes paramètres de type de clé et de permissions que le chemin en mémoire.

Ajouter des changements au lieu de réécrire

La mise à jour incrémentale, définie dans ISO 32000-1 §7.5.6, est le troisième niveau : les octets originaux restent intacts sur disque, et les objets modifiés ou nouveaux sont ajoutés après eux avec une section de références croisées qui chaîne vers l'original. Pour une archive de 900 Mo qui doit recevoir une page ajoutée, le coût d'écriture est le delta, pas le fichier.

// Append an audit page to a large archive without rewriting it
Pdf.BeginIncrementalUpdate('archive-2026-06.pdf');
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Processed by intake service 2026-06-11');
Pdf.SaveIncrementalUpdate('archive-2026-06-stamped.pdf');  // original bytes + delta

La discipline ici : BeginIncrementalUpdate doit pointer vers le fichier original, car les références croisées ajoutées chaînent vers des offsets situés dans ce fichier. Et le modèle est append-only par conception : chaque enregistrement incrémental agrandit le fichier, jamais ne le réduit ; un document tamponné chaque nuit grossit donc sans limite jusqu'à ce qu'une resérialisation périodique, chargement du document et réécriture par SaveLoadedDocument, le compacte. Cette propriété append-only est aussi ce qui fait de la mise à jour incrémentale la seule manière sûre de modifier des documents signés numériquement, contrainte examinée dans l'article sur les signatures numériques et PAdES ; les mécanismes sous-jacents de références croisées sont couverts dans l'article sur les flux d'objets et les mises à jour incrémentales.

Une propriété des enregistrements append-only se manque facilement en revue : les octets originaux restent dans le fichier, lisibles par quiconque regarde. Une mise à jour incrémentale qui « remplace » une page n'efface pas l'ancienne ; elle la supplante dans la révision courante tandis que la révision précédente reste récupérable. N'utilisez jamais les mises à jour incrémentales pour retirer du contenu sensible ; une resérialisation complète, LoadFromFile suivi de SaveLoadedDocument, qui ne transporte que l'état courant, est la bonne manière d'abandonner l'historique qu'un destinataire ne doit pas voir.

Associer le niveau à l'opération

La logique de sélection tient en quatre lignes, et elle mérite d'être encodée comme décision de routage explicite au début d'un pipeline plutôt que de laisser chaque job choisir son propre chemin :

  • Compter, inspecter, classer : ouvrir un handle, DAOpenFileReadOnly, DAGetPageCount, DACloseFile.
  • Déplacer, déchiffrer ou chiffrer des fichiers complets : appels niveau fichier, DACopyFile, DecryptFile, EncryptFile.
  • Restructurer des pages ou fusionner des documents : chargement complet, LoadFromFile, puis InsertPagesFromDocument ou MovePage, puis SaveLoadedDocument.
  • Ajouter un petit delta à un fichier énorme ou signé : BeginIncrementalUpdate et enregistrer.

Les pipelines mixtes gagnent à placer un seuil de taille avant le chemin de chargement complet : routez tout ce qui dépasse quelques centaines de mégaoctets vers les niveaux Direct File et envoyez les vraies opérations de restructuration vers un worker 64 bits doté d'un budget mémoire. Le seuil transforme les crashs out-of-memory en décision de routage explicite et observable.

Quel que soit le niveau qui traite le job, envoyez sa sortie vers un nom temporaire et renommez-la en place seulement après validation du résultat ; un fichier à moitié écrit sous son nom final est indiscernable d'un bon fichier pour l'étape suivante du pipeline. Les appels Direct File rendent cette validation économique, car confirmer la sortie est lui-même une sonde de handle en une ligne.

FAQ : grands PDF dans les services Delphi

Comment obtenir le nombre de pages d'un PDF sans charger tout le fichier ?

DAOpenFileReadOnly plus DAGetPageCount, comme dans l'exemple d'inspection ci-dessus ; l'usage mémoire reste plat quelle que soit la taille du fichier.

Pourquoi mon PDF grossit-il après chaque enregistrement ?

Les mises à jour incrémentales ajoutent par conception ; rien n'est jamais retiré. Compactez périodiquement par un chargement complet et un réenregistrement, LoadFromFile puis SaveLoadedDocument, lorsque les révisions accumulées ne sont plus nécessaires.

La Direct File API ouvre-t-elle les PDF chiffrés ?

Elle accepte un mot de passe, mais les entrées chiffrées passent en interne par un parse complet et perdent l'avantage de mémoire plate. Pour les entrées protégées, DecryptFile avec le mot de passe produit une copie claire que le reste du pipeline peut traiter au niveau fichier.

La Direct File API est livrée avec le HotPDF Component pour Delphi et C++Builder ; la page produit renvoie à la référence complète des fonctions, y compris les appels de mise à jour incrémentale montrés ici.