Le composant PDFium vous offre une méthode pour le fractionnement de PDF : ImportPages. Tout le reste, que vous isoliez une seule page, que vous coupiez sur des limites arbitraires ou que vous suiviez la propre structure de signets du document, n'est que des façons différentes de décider quels numéros de page vont dans chaque fichier de sortie. La mécanique reste la même. Comprendre cela tôt évite beaucoup de fausses pistes (wrong turns)
Comment fonctionne la boucle de fractionnement
Le modèle est le même, quelle que soit la façon dont vous divisez le document source. Créez une nouvelle instance TPdf, appelez CreateDocument sur celle-ci pour initialiser un PDF vide en mémoire, importez les pages que vous souhaitez avec ImportPages, enregistrez le résultat, puis réinitialisez Active à False avant la prochaine itération. Cette dernière étape est celle que les gens oublient : CreateDocument ne ferme pas implicitement le document toujours en mémoire, vous devez donc sauvegarder votre sortie et réinitialiser explicitement Active := False avant de l'appeler à nouveau ; la réinitialisation en premier maintient l'état propre et bien défini. L'instance externe TPdf est réutilisée à travers toutes les itérations, ce qui maintient la pression d'allocation (allocation pressure) basse sur les gros travaux
Voici à quoi ressemble le fractionnement page par page, réduit à son 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 basée sur 1 ; point d'insertion 1 = première position
if not PdfOut.ImportPages(Source, IntToStr(I), 1) then
raise Exception.CreateFmt('Échec de l''importation de la page %d', [I]);
OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Impossible de sauvegarder ' + OutFile);
PdfOut.Active := False; // réinitialiser avant le prochain CreateDocument
end;
finally
PdfOut.Free;
end;
end;
Le paramètre Range pour ImportPages est le même format de chaîne que celui utilisé par PDFium en interne : une liste de numéros de page séparés par des virgules ou des plages délimitées par des traits d'union, toutes basées sur 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 basée sur 1 dans le document de destination ; passer 1 place toujours les pages importées au début d'un fichier autrement vide, ce qui est ce que vous voulez ici
Fractionnement par plages de pages
Lorsque l'appelant fournit une liste comme 1-12,13-24,25-36, vous l'analysez (parse) en paires de 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('Plage de pages invalide : ' + RangeList[I]);
OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Impossible de sauvegarder ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
La validation avant d'atteindre ImportPages compte ici. ImportPages renvoie False lorsqu'un numéro de page dans la chaîne de plage dépasse Source.PageCount, mais il ne lève pas d'exception et ne produit pas de fichier de sortie partiel que vous pouvez détecter par le nom seul. Vérifiez la valeur de retour de SaveAs et consignez (log) les échecs séparément ; une plage qui produit un fichier de sortie vide n'est pas évidemment fausse jusqu'à ce que quelqu'un l'ouvre
Fractionnement aux limites des signets
La troisième approche utilise la propre structure du document plutôt qu'une liste fournie en externe. Chaque signet (bookmark) de niveau supérieur porte un numéro de page cible ; la section qu'il définit s'étend de cette page à celle précédant la page du signet suivant, ou à 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('Impossible de sauvegarder ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
Un document qui ne comporte pas de signets n'est pas une condition d'erreur valant la peine d'être présentée à l'utilisateur comme telle ; cela signifie simplement que ce mode de fractionnement n'a rien sur quoi travailler. La garde (guard) Length(Bm) = 0 gère cela silencieusement. Ce qui vaut la peine d'être signalé, c'est lorsqu'un numéro de page d'un signet se situe en dehors de la plage du document, ce qui se produit dans des fichiers mal formés où le plan (outline) n'a jamais été mis à jour après la suppression de pages. La vérification des limites (bounds check) sur StartPage et EndPage ignore ces entrées plutôt que de transmettre une plage de déchets (garbage range) à ImportPages
Nommage du fichier de sortie et réinitialisation de Active
La sécurité des noms de fichiers dérivés de signets nécessite une attention explicite. Les titres des signets peuvent contenir des caractères qui sont valides dans une chaîne PDF mais pas dans un chemin de système de fichiers. Au minimum, remplacez la barre oblique (forward slash), la barre oblique inverse (backslash) 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 recourir à une expression régulière (regex)
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 modèle. CreateDocument ne ferme pas implicitement ce qui est ouvert. Si Active est toujours True lorsque CreateDocument s'exécute à nouveau, le document toujours en mémoire n'a jamais été correctement fermé ou enregistré, et vous ne pouvez pas compter sur un comportement bien défini dans cet état, vous devez donc sauvegarder et réinitialiser explicitement avant de démarrer le document suivant. Considérez-le comme la paire de try/finally : le bloc finally libère l'objet externe ; le Active := False réinitialise l'état interne du document entre les itérations de la boucle
L'utilisation de la mémoire sur un gros travail de fractionnement reste stable (flat) avec cette approche car vous ne conservez jamais plus d'un document de sortie en mémoire à la fois. Le document source reste ouvert et en lecture seule tout du long ; ImportPages copie les données de la page dans le nouveau document sans modifier la source. Si la source est cryptée, ouvrez-la avec son mot de passe avant la boucle et les pages copiées dans chaque fichier de sortie ne seront pas cryptées, ce qui est généralement le bon comportement pour une sortie fractionnée distribuée à différents destinataires
Encore une chose à propos de SaveAs : il renvoie un Boolean. Un répertoire de sortie qui n'existe pas, un chemin avec des caractères que le système d'exploitation rejette ou une condition de disque plein feront tous que SaveAs renvoie False sans lever d'exception. Dans un travail par lots (batch job) qui divise un document de 200 pages en 200 fichiers d'une page, un échec silencieux à la page 147 est facile à négliger. Vérifiez la valeur de retour à chaque appel et comptez les succès par rapport au total attendu lorsque la boucle se termine
Les méthodes ImportPages et CreateDocument présentées ici font partie du Composant PDFium pour Delphi et C++Builder