Article technique

Fusion de plusieurs fichiers PDF en un seul document avec le composant PDFium

Le composant PDFium expose la fusion PDF via une seule méthode : ImportPages. Le modèle est toujours le même : créer un document de destination vide, ouvrir chaque fichier source, appeler ImportPages pour copier les pages, fermer la source et répéter. Lorsque la boucle se termine, SaveAs écrit le résultat sur le disque. Il n'y a pas de mode de fusion spécial, pas de configuration à basculer. La complexité réside dans les cas extrêmes (edge cases), et il y en a quelques-uns qui mordent sans avertissement

La boucle de base

Deux instances TPdf sont tout ce dont vous avez besoin. L'une contient le document de destination, créé vide avec CreateDocument. L'autre ouvre chaque fichier source à tour de rôle. Ci-dessous, une procédure qui prend une liste de chemins de fichiers et écrit la sortie fusionnée dans un seul chemin :

procedure MergeFiles(const FileList: TStrings; const OutputPath: string);
var
  PdfDest, PdfSrc: TPdf;
  InsertAt, I: Integer;
begin
  PdfDest := TPdf.Create(nil);
  PdfSrc  := TPdf.Create(nil);
  try
    PdfDest.CreateDocument;
    InsertAt := 1;  // ImportPages utilise une position de destination basée sur 1

    for I := 0 to FileList.Count - 1 do
    begin
      PdfSrc.FileName := FileList[I];
      PdfSrc.Active   := True;

      if not PdfSrc.Active then
        raise Exception.CreateFmt('Impossible d''ouvrir : %s', [FileList[I]]);

      PdfDest.ImportPages(
        PdfSrc,
        '1-' + IntToStr(PdfSrc.PageCount),  // plage complète du document
        InsertAt);

      Inc(InsertAt, PdfSrc.PageCount);
      PdfSrc.Active := False;
    end;

    PdfDest.SaveAs(OutputPath);
  finally
    PdfSrc.Free;
    PdfDest.Free;
  end;
end;

Deux choses dans ce code sont faciles à négliger lors d'une première lecture. La première est la façon dont PDFium signale les échecs de chargement. Active := True ne lève jamais d'exception : si le fichier est manquant, endommagé ou protégé par mot de passe, PDFium intercepte l'erreur en interne et laisse Active à False. Sans la vérification explicite à la ligne 10, un mauvais fichier serait silencieusement exclu de la fusion sans aucune indication dans la sortie. Le PDF final aurait moins de pages que prévu et vous ne sauriez pas quel fichier était le coupable

La seconde est le compteur InsertAt. Le troisième argument de ImportPages est la position (en base 1) dans la destination où atterrit la première page importée. Commencer à 1 place le premier document source au début d'un fichier par ailleurs vide. Après chaque source, le compteur avance de PdfSrc.PageCount, de sorte que le lot suivant de pages s'ajoute (appends) après le dernier. Oubliez de l'incrémenter et chaque source suivante écrase les pages à la position 1, vous donnant le dernier document de la liste et rien d'autre

Plages de pages sélectives

Vous n'êtes pas obligé de prendre toutes les pages d'une source. La chaîne de plage (range string) transmise en tant que deuxième argument suit un format simple avec des virgules et des traits d'union : "1-3" prend les pages 1 à 3, "2,4,6" choisit trois pages spécifiques et "1-" signifie de la page 1 à la fin du document. Les plages peuvent être combinées dans une seule chaîne, donc "1-3,5,7-" ignore les pages 4 et 6. Une subtilité compte ici : les nombres font toujours référence aux pages du document source, en commençant par 1, peu importe où ces pages atterrissent dans la destination. Si vous voulez les pages 40 à 50 d'un catalogue de 200 pages, la chaîne de plage est "40-50", et non une position relative à ce qui se trouve déjà dans la destination

// Extraire la couverture plus un résumé de trois pages d'un long rapport
PdfSrc.FileName := 'annual-report.pdf';
PdfSrc.Active   := True;
if PdfSrc.Active then
begin
  // La page 1 est la couverture ; les pages 3 à 5 sont le résumé
  PdfDest.ImportPages(PdfSrc, '1,3-5', InsertAt);
  Inc(InsertAt, 4);  // 1 couverture + 3 pages de résumé = 4 pages ajoutées
  PdfSrc.Active := False;
end;

Lors du calcul de l'incrément de InsertAt, comptez les pages que vous avez réellement importées, pas le nombre de pages de la source. Si vous passez '1,3-5', vous avez importé 4 pages, alors avancez de 4. Avancer de PdfSrc.PageCount laisserait un espace (gap) de positions de destination vides et placerait le document source suivant plus loin dans le fichier que prévu

Ce qu'ImportPages préserve et ce qu'il ne préserve pas

Les pages copiées par ImportPages portent leur contenu visible intact. Le texte, les graphiques vectoriels, les images matricielles (raster images), les polices intégrées et les XObjects de formulaire (form XObjects) sont tous transférés dans le cadre des flux de contenu de la page. Les annotations au niveau de la page, y compris les commentaires, les surbrillances et les traits d'encre (ink strokes), sont également transmises (come across too), car elles sont stockées dans le dictionnaire de page plutôt qu'au niveau du document

Les métadonnées au niveau du document sont une autre histoire. Les chaînes de titre, d'auteur, de sujet et de mots-clés dans le dictionnaire Info de la source restent derrière. Le document de destination démarre avec des métadonnées vides après CreateDocument, par conséquent, si la sortie fusionnée doit voir ces champs remplis (populated), vous devez les affecter directement à PdfDest avant d'appeler SaveAs. Les propriétés Title, Author, Subject, Keywords et Creator sur TPdf prennent des chaînes simples et écrivent dans le dictionnaire Info lors de la sauvegarde

Les champs de formulaire interactifs sont plus compliqués. Les définitions de champs AcroForm vivent dans un dictionnaire au niveau du document plutôt qu'à l'intérieur de flux de pages individuels. Lorsque ImportPages copie une page qui contient des champs de formulaire, l'apparence visuelle de ces champs est transférée car elle est rendue dans le flux de contenu de la page, mais les widgets de champ qui les rendent interactifs font partie de la structure AcroForm et ne suivent pas. Dans une fusion typique, un champ de texte d'un document source affichera la valeur qu'il avait au moment de l'importation, mais il ne sera pas modifiable dans le fichier fusionné. Si vous avez besoin que les champs restent remplissables (fillable), aplatissez-les (flatten them) dans chaque document source avant d'importer : cela fige (bakes) les valeurs courantes dans le flux de contenu et supprime la superposition interactive (interactive overlay), vous donnant un résultat visuel propre sans widgets cassés dans la sortie

Fichiers sources cryptés

Les documents sources protégés par mot de passe s'ouvrent de la même manière que ceux non cryptés, avec une propriété supplémentaire à définir en premier. Affectez le mot de passe à PdfSrc.Password avant de basculer Active := True, et PDFium l'utilisera lors de l'ouverture :

PdfSrc.Password := 'user-password';
PdfSrc.FileName := 'protected.pdf';
PdfSrc.Active   := True;
if not PdfSrc.Active then
  raise Exception.Create('Mot de passe incorrect ou fichier impossible à ouvrir');

PdfDest.ImportPages(PdfSrc, '1-' + IntToStr(PdfSrc.PageCount), InsertAt);
Inc(InsertAt, PdfSrc.PageCount);
PdfSrc.Active := False;

Un mot de passe incorrect entraîne le même résultat silencieux Active = False qu'un fichier manquant, la vérification explicite est donc tout aussi nécessaire ici. Le cryptage ne se transfère pas à la destination : les pages importées d'une source protégée atterrissent dans la destination en tant que contenu non protégé. Si la sortie fusionnée a également besoin d'un cryptage, configurez-la sur PdfDest avant d'appeler SaveAs

Sauvegarde du résultat

SaveAs sur TPdf accepte soit un chemin de fichier, soit un TStream. Pour la plupart des fusions, la surcharge de fichier est ce que vous voulez :

PdfDest.SaveAs('merged-output.pdf');

Le deuxième argument facultatif est un TSaveOption qui contrôle le mode de sauvegarde. La valeur par défaut, saNone, écrit une mise à jour incrémentielle (incremental update) si le document a été chargé à partir d'un fichier ou une réécriture complète s'il a été créé à neuf. Puisqu'une destination construite avec CreateDocument est toujours neuve (fresh), la sortie sera un fichier compact à révision unique (single-revision file). Le troisième argument, TPdfVersion, vous permet d'épingler (pin) l'en-tête de version PDF lorsque vous avez des consommateurs en aval qui nécessitent une version spécifique ; le laisser à pvUnknown permet à PDFium de choisir en fonction du contenu

Les méthodes ImportPages et SaveAs présentées ici font partie du Composant PDFium pour Delphi et C++Builder