Extraire des pages d'un PDF devrait prendre quelques secondes. Cet article retrace les véritables goulots d'étranglement dans le flux de travail Delphi de HotPDF et montre quel chemin d'API permet de retrouver la vitesse attendue
Qu'est-ce qui est réellement lent ?
Ce PDF est un document de référence de 40 pages doté d'un arbre de pages complexe : plusieurs nœuds intermédiaires /Pages plutôt qu'un unique tableau plat. Le code d'exemple initial appelait LoadFromFile, puis utilisait BeginDoc pour construire un nouveau document, bouclait sur les numéros de pages sélectionnés et rechargeait à chaque itération le document source depuis le disque pour extraire une page. Cela représente le coût complet de l'analyse multiplié par le nombre de pages souhaité. Un fichier de 12 Mo sollicitait le disque six fois pour extraire trois pages car personne n'avait vérifié si le fichier devait rester ouvert d'une itération à l'autre
Le second facteur est invisible dans le code : LoadFromFile de HotPDF analyse toute la table des références croisées lors du chargement et décompresse chaque flux d'objets. C'est le bon comportement pour un document que vous vous apprêtez à modifier, mais si vous n'avez besoin que du nombre de pages et d'une partie d'entre elles, c'est plus de travail que nécessaire. Pour un accès en lecture seule à la structure, DAOpenFileReadOnly évite de désérialiser l'arbre d'objets complet, ce qui est important pour les fichiers compressés contenant de nombreuses ressources d'images
Aucun de ces cas n'est un bogue de la bibliothèque. Dans les deux situations, l'appelant a choisi une API conçue pour une tâche spécifique et l'a appliquée à une autre
Extraction de pages avec InsertPagesFromDocument
Pour copier une partie de page d'un document HotPDF à un autre, le chemin correct est InsertPagesFromDocument, appelé après LoadFromFile sur la source. Vous chargez la source une fois, chargez ou créez la cible une fois, déplacez ces pages, puis enregistrez. La source reste en mémoire pendant toutes les opérations d'insertion de page :
procedure ExtractPages(const SourceFile, DestFile: string;
const PageRange: string);
var
Source, Dest: THotPDF;
begin
Source := THotPDF.Create(nil);
Dest := THotPDF.Create(nil);
try
// 把源加载一次:完整解析在这里、而且只在这里发生
Source.LoadFromFile(SourceFile);
// 构建一个最小的目标文档
Dest.FileName := DestFile;
Dest.BeginDoc;
// 复制请求的范围;'1-3' 从目标的位置 1 开始
// 插入第 1 到第 3 页
Dest.InsertPagesFromDocument(Source, PageRange, 1);
Dest.EndDoc;
finally
Source.Free;
Dest.Free;
end;
end;
Validateur PDF : Outils pour vérifier la conformité aux normes PDF
Le temps d'exécution sur le même fichier de 12 Mo qui prenait auparavant deux minutes : moins de 1,5 seconde avec ce modèle. La majeure partie de ce temps est consacrée à cet unique appel LoadFromFile. Une fois la table des objets analysée pour la première fois, la structure du document importe peu
Lorsque LoadFromFile est trop lourd : l'API de fichier direct
Si vous avez seulement besoin de compter les pages, de vérifier les informations du document ou de copier un fichier sans toucher à son contenu, l'API de fichier direct évite complètement une analyse complète. DAOpenFileReadOnly mappe la table des références croisées sans décompresser les flux d'objets, ainsi le comptage des pages est en O(taille de xref) plutôt qu'en O(taille du fichier) :
procedure InspectPDF(const FileName: string);
var
Pdf: THotPDF;
Handle, PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Handle := Pdf.DAOpenFileReadOnly(FileName, '');
if Handle <= 0 then
Exit;
try
PageCount := Pdf.DAGetPageCount(Handle);
Writeln('Pages: ', PageCount);
// DACopyFile 是一次保留字节的复制,不重新序列化
Pdf.DACopyFile(FileName, 'archive-copy.pdf');
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
Un point à noter : DAOpenFileReadOnly accepte un paramètre de mot de passe, mais revient à une analyse complète pour les entrées chiffrées, car le déchiffrement nécessite l'arbre des objets pour analyser le dictionnaire de chiffrement. Si votre fichier source est chiffré, déchiffrez-le d'abord en une copie non chiffrée avec , puis ouvrez cette copie avec l'API de fichier direct. La fonction DecryptFile au niveau du fichier utilise un chemin de réécriture direct AES-256 pour les fichiers chiffrés standard, ce qui est plus rapide pour les grands fichiers que l'enchaînement "DecryptFileLoadFromFile suivi de SaveLoadedDocument" car elle ne construit pas le modèle d'objet complet en mémoire
Mémoire lors du traitement de gros volumes
Dans les traitements par lots manipulant des dizaines de fichiers dans une boucle, il existe un modèle qui semble correct mais accumule la mémoire : créer THotPDF dans la boucle, appeler LoadFromFile, travailler, puis appeler Free. C'est correct sur le plan structurel. Le problème survient lorsque le travail interne alloue des objets temporaires, capture des exceptions et laisse ces objets temporaires actifs sur le chemin d'erreur. Le gestionnaire de mémoire de Delphi ne fait pas de compactage, donc une centaine de fuites sur le chemin d'erreur dans un traitement par lots suffit à augmenter la mémoire au point de ralentir tout le reste
La méthode de correction n'est pas mystérieuse. Chaque THotPDF, ainsi que chaque TStream ou TBitmap intermédiaire participant au travail PDF, doit être placé dans un bloc , try/finallyFree étant la dernière instruction. Configurez les pointeurs locaux sur nil avant try, de sorte que si l'initialisation échoue à mi-chemin, la branche finally puisse utiliser en toute sécurité if Assigned(x) then x.Free. C'est la discipline de propriété Delphi standard, et c'est tout ce que concerne ce type de problème
Une autre chose à vérifier dans les scénarios par lots : AddImage enregistre les images dans une liste interne qui persiste pendant toute la durée de vie de l'instance THotPDF. Si vous réutilisez la même instance sur de nombreux documents en appelant LoadFromFile de manière répétée, les enregistrements d'images des documents précédents resteront dans la liste. Créez une nouvelle instance pour chaque document, ou appelez la méthode de nettoyage de la liste d'images entre les documents
Mesurez avant d'effectuer des modifications
Mesurez avant d'utiliser l'un de ces modes. TStopwatch de System.Diagnostics de Delphi enveloppe QueryPerformanceCounter, offrant une précision suffisante pour l'analyse du temps d'exécution des E/S de fichiers. Enveloppez simplement LoadFromFile pour voir combien de temps il prend. S'il représente 90 % du temps total, la solution consiste à utiliser l'API de fichier direct ou à réduire le nombre d'analyses sur le même fichier. S'il est inférieur à 20 %, le goulot d'étranglement est ailleurs et vous vous trompez de cible
L'extraction de deux minutes mentionnée au début était entièrement due au modèle de chargements répétés. La structure du document n'y jouait aucun rôle ; un arbre de pages plat se comporterait de la même manière. Remplacer cela par un unique LoadFromFile suivi d'un appel à InsertPagesFromDocument a réduit le temps à 1,3 seconde sur le même matériel sans aucun autre changement
L'API de manipulation de pages présentée ici appartient au composant HotPDF Component pour Delphi et C++Builder