Article technique

Fractionner des documents PDF en plusieurs fichiers avec PDFium VCL en Delphi

PDFium VCL vous offre une seule méthode pour le fractionnement de PDF : ImportPages. Tout le reste, qu'il s'agisse d'isoler une page unique, de découper selon des limites arbitraires ou de suivre la structure des signets du document, n'est que différentes façons de décider quels numéros de page entrent dans chaque fichier de sortie. La mécanique reste la même. Comprendre cela dès le départ évite bien des erreurs de parcours.

Fonctionnement de la boucle de fractionnement

Le schéma est identique quelle que soit la façon dont vous divisez le document source. Créez une nouvelle instance TPdf, appelez CreateDocument pour initialiser un PDF vide en mémoire, importez les pages souhaitées avec ImportPages, enregistrez le résultat, puis réinitialisez Active à False avant l'itération suivante. C'est cette dernière étape que l'on oublie le plus souvent : sans réinitialisation, le prochain appel à CreateDocument s'ajoute au document encore en mémoire plutôt que de repartir à zéro. L'instance TPdf externe est réutilisée à chaque itération, ce qui maintient la pression d'allocation à un niveau bas pour les gros traitements.

Voici ce à quoi ressemble le fractionnement 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 is a 1-based page number string; insertion point 1 = first position
      PdfOut.ImportPages(Source, IntToStr(I), 1);

      OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
      PdfOut.SaveAs(OutFile);

      PdfOut.Active := False;   // reset before next CreateDocument
    end;
  finally
    PdfOut.Free;
  end;
end;

Le paramètre Range de ImportPages utilise le même format de chaîne qu'en interne PDFium : une liste de numéros de page séparés par des virgules ou des plages délimitées par un tiret, toutes en 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 en base 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 le comportement souhaité ici.

Fractionnement par plages de pages

Lorsque l'appelant fournit une liste du type 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;
      PdfOut.ImportPages(Source, RangeList[I], 1);
      OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
      PdfOut.SaveAs(OutFile);
      PdfOut.Active := False;
    end;
  finally
    PdfOut.Free;
  end;
end;

La validation avant d'atteindre ImportPages est importante ici. ImportPages retourne 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 détectable par son nom seul. Vérifiez la valeur de retour de SaveAs et journalisez les échecs séparément ; une plage produisant un fichier de sortie vide n'est pas manifestement incorrecte tant qu'on ne l'ouvre pas.

Fractionnement aux limites 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 s'étend de cette page jusqu'à la précédant le signet suivant, ou jusqu'à la fin du document pour le dernier élément.

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;
      PdfOut.ImportPages(Source, RangeStr, 1);

      SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
      SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
      OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
      PdfOut.SaveAs(OutFile);

      PdfOut.Active := False;
    end;
  finally
    PdfOut.Free;
  end;
end;

Un document sans signets n'est pas une condition d'erreur qu'il faille signaler à l'utilisateur ; cela signifie simplement que ce mode de fractionnement n'a rien sur quoi travailler. La garde Length(Bm) = 0 le gère silencieusement. Ce qui mérite d'être signalé, c'est lorsque le numéro de page d'un signet est en dehors de la plage du document, ce qui se produit dans les fichiers malformés où la table des matières n'a jamais été mise à jour après la suppression de pages. La vérification des bornes sur StartPage et EndPage ignore ces entrées plutôt que de transmettre une plage invalide à ImportPages.

Nommage des fichiers de sortie et réinitialisation du drapeau Active

La sécurité des noms de fichiers issus des signets mérite une attention explicite. Les titres de signets peuvent contenir des caractères valides dans une chaîne PDF mais interdits 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 boucle simple sur un ensemble fixe les couvre sans recourir à une expression régulière.

La ligne Active := False à la fin de chaque itération mérite qu'on y insiste, car c'est la seule exigence non évidente du schéma. CreateDocument ne ferme pas implicitement ce qui est ouvert. Si Active est encore True quand CreateDocument s'exécute à nouveau, PDFium abandonne le document en cours et en commence un nouveau sans signaler d'erreur, mais le comportement est défini par l'implémentation dans les cas limites et l'intention est plus claire lorsque vous réinitialisez explicitement. Considérez-la comme la contrepartie du bloc try/finally : le bloc finally libère l'objet externe ; Active := False réinitialise l'état du document interne entre les itérations de la boucle.

L'empreinte mémoire d'un grand travail de fractionnement reste stable avec cette approche, car vous ne conservez jamais plus d'un document de sortie en mémoire à la fois. Le document source reste ouvert en lecture seule tout au long du processus ; 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 comportement souhaité pour une sortie fractionnée distribuée à différents destinataires.

Une dernière remarque sur SaveAs : il retourne un Boolean. Un répertoire de sortie inexistant, un chemin contenant des caractères rejetés par le système d'exploitation ou un disque plein feront tous retourner False à SaveAs sans lever d'exception. Dans un traitement par lot qui fractionne un document de 200 pages en 200 fichiers d'une page chacun, un échec silencieux à la page 147 est facile à manquer. Vérifiez la valeur de retour à chaque appel et comptez les succès par rapport au total attendu à la fin de la boucle.

Les méthodes ImportPages et CreateDocument présentées ici font partie de PDFium VCL pour Delphi et C++Builder.