PDFium Component vous donne une seule méthode pour découper un PDF : ImportPages. Tout le reste, que vous isoliez une seule page, que vous coupiez à des frontières arbitraires ou que vous suiviez la structure de signets du document, se résume à différentes façons de décider quels numéros de page vont dans chaque fichier de sortie. La mécanique reste la même. Le comprendre tôt évite beaucoup de fausses pistes
Comment fonctionne la boucle de découpage
Le motif est le même quelle que soit la manière dont vous divisez le document source. Créez une nouvelle instance TPdf, appelez CreateDocument dessus pour initialiser un PDF vide en mémoire, importez les pages voulues avec ImportPages, enregistrez le résultat, puis remettez Active à False avant l'itération suivante. C'est cette dernière étape que l'on oublie : CreateDocument ne ferme pas implicitement le document encore en mémoire, vous devez donc enregistrer votre sortie et remettre Active := False explicitement avant de l'appeler à nouveau ; réinitialiser d'abord garde un état propre et bien défini. L'instance TPdf extérieure est réutilisée sur toutes les itérations, ce qui limite la pression d'allocation sur les traitements volumineux
Voici à quoi ressemble le découpage page par page réduit à l'essentiel :
procedure SplitIntoPages(Source: TPdf; const OutputDir: string);
var
I: Integer;
PdfOut: TPdf;
OutFile: string;
begin
PdfOut := TPdf.Create(nil);
try
for I := 1 to Source.PageCount do
begin
PdfOut.CreateDocument;
// Range est une chaîne de numéros de page à base 1 ; point d'insertion 1 = première position
if not PdfOut.ImportPages(Source, IntToStr(I), 1) then
raise Exception.CreateFmt('Failed to import page %d', [I]);
OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False; // réinitialiser avant le prochain CreateDocument
end;
finally
PdfOut.Free;
end;
end;
Le paramètre Range de ImportPages utilise le même format de chaîne que PDFium en interne : une liste de numéros de page séparés par des virgules ou de plages délimitées par des traits d'union, toutes à base 1. '3' importe la page 3. '1-5' importe les pages 1 à 5 dans l'ordre. '2,5,8' importe ces trois pages. Le troisième paramètre est la position d'insertion à base 1 dans le document de destination ; passer 1 place toujours les pages importées au début d'un fichier par ailleurs vide, ce qui est bien ce que vous voulez ici
Découper par plages de pages
Quand l'appelant fournit une liste comme 1-12,13-24,25-36, vous l'analysez en paires début/fin et exécutez la même boucle, en construisant la chaîne de plage à partir de chaque paire :
procedure SplitByRanges(Source: TPdf; const RangeList: array of string;
const OutputDir: string);
var
I: Integer;
PdfOut: TPdf;
OutFile: string;
begin
PdfOut := TPdf.Create(nil);
try
for I := 0 to High(RangeList) do
begin
PdfOut.CreateDocument;
if not PdfOut.ImportPages(Source, RangeList[I], 1) then
raise Exception.Create('Invalid page range: ' + RangeList[I]);
OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
La validation en amont de ImportPages compte ici. ImportPages renvoie False quand un numéro de page de la chaîne de plage dépasse Source.PageCount, mais il ne déclenche pas d'exception et ne produit pas de fichier de sortie partiel que vous puissiez repérer au seul nom. Vérifiez la valeur de retour de SaveAs et journalisez les échecs séparément ; une plage qui produit un fichier de sortie vide ne paraît pas fautive tant que personne ne l'ouvre
Découper aux frontières des signets
La troisième approche utilise la structure propre du document plutôt qu'une liste fournie de l'extérieur. Chaque signet de premier niveau porte un numéro de page cible ; la section qu'il définit va de cette page à celle qui précède la page du signet suivant, ou jusqu'à la fin du document pour la dernière entrée
procedure SplitByBookmarks(Source: TPdf; const OutputDir: string);
var
Bm: TBookmarks;
I, StartPage, EndPage: Integer;
PdfOut: TPdf;
RangeStr, OutFile, SafeTitle: string;
begin
Bm := Source.Bookmarks;
if Length(Bm) = 0 then
Exit;
PdfOut := TPdf.Create(nil);
try
for I := 0 to High(Bm) do
begin
StartPage := Bm[I].PageNumber;
if I < High(Bm) then
EndPage := Bm[I + 1].PageNumber - 1
else
EndPage := Source.PageCount;
if (StartPage < 1) or (EndPage < StartPage) then
Continue;
RangeStr := Format('%d-%d', [StartPage, EndPage]);
PdfOut.CreateDocument;
if not PdfOut.ImportPages(Source, RangeStr, 1) then
begin
PdfOut.Active := False;
Continue; // ignorer une section mal formée au lieu d'écrire un fichier vide
end;
SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
Un document sans signets n'est pas une condition d'erreur qui mérite d'être présentée à l'utilisateur comme telle ; cela signifie seulement que ce mode de découpage n'a rien sur quoi travailler. La garde Length(Bm) = 0 traite ce cas en silence. Ce qui mérite d'être signalé, c'est un numéro de page de signet situé hors de la plage du document, ce qui arrive dans les fichiers mal formés où le plan n'a jamais été mis à jour après la suppression de pages. Le contrôle de bornes sur StartPage et EndPage ignore ces entrées plutôt que de passer une plage aberrante à ImportPages
Nommage des fichiers de sortie et réinitialisation de Active
La sûreté des noms de fichiers dérivés des signets demande une attention explicite. Les titres de signets peuvent contenir des caractères valides dans une chaîne PDF mais pas dans un chemin de système de fichiers. Au minimum, remplacez la barre oblique, la barre oblique inverse et les deux-points avant de construire le chemin de sortie. Sous Windows, *, ?, ", <, > et | sont également interdits ; une simple boucle sur un ensemble fixe les couvre sans faire entrer une expression régulière
La ligne Active := False à la fin de chaque itération mérite d'être soulignée, car c'est la seule exigence non évidente du motif. CreateDocument ne ferme pas implicitement ce qui est ouvert. Si Active vaut encore True quand CreateDocument s'exécute de nouveau, le document encore en mémoire n'a jamais été correctement fermé ni enregistré, et vous ne pouvez pas compter sur un comportement bien défini dans cet état ; enregistrez et réinitialisez donc explicitement avant de commencer le document suivant. Voyez cela comme le pendant du try/finally : le bloc finally libère l'objet extérieur, tandis que Active := False réinitialise l'état du document intérieur entre deux itérations de boucle
L'utilisation mémoire reste stable sur un gros travail de découpage avec cette approche, parce que vous ne gardez jamais plus d'un document de sortie en mémoire à la fois. Le document source reste ouvert et en lecture seule d'un bout à l'autre ; ImportPages copie les données de page dans le nouveau document sans modifier la source. Si la source est chiffrée, ouvrez-la avec son mot de passe avant la boucle et les pages copiées dans chaque fichier de sortie seront non chiffrées, ce qui est généralement le bon comportement pour des sorties de découpage distribuées à des destinataires différents
Encore un point à propos de SaveAs : il renvoie un Boolean. Un répertoire de sortie inexistant, un chemin comportant des caractères refusés par le système ou un disque plein feront tous renvoyer False à SaveAs sans déclencher d'exception. Dans un traitement par lots qui découpe un document de 200 pages en 200 fichiers d'une page, un échec silencieux à la page 147 passe facilement inaperçu. Vérifiez la valeur de retour à chaque appel et comparez le nombre de succès au total attendu à la fin de la boucle
Les méthodes ImportPages et CreateDocument présentées ici font partie de PDFium Component pour Delphi et C++Builder