PDFium Component expose la fusion de PDF par une seule méthode : ImportPages. Le schéma est toujours le même : créer un document de destination vide, ouvrir chaque fichier source, appeler ImportPages pour y recopier les pages, fermer la source, et recommencer. Quand la boucle se termine, SaveAs écrit le résultat sur le disque. Il n'y a pas de mode de fusion particulier, aucune configuration à basculer. La complexité vit dans les cas limites, et quelques-uns mordent sans prévenir
La boucle centrale
Deux instances de TPdf suffisent. L'une porte le document de destination, créé vide avec CreateDocument. L'autre ouvre chaque fichier source à tour de rôle. Voici une procédure qui prend une liste de chemins de fichiers et écrit la sortie fusionnée vers un chemin unique :
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('Cannot open: %s', [FileList[I]]);
PdfDest.ImportPages(
PdfSrc,
'1-' + IntToStr(PdfSrc.PageCount), // plage du document entier
InsertAt);
Inc(InsertAt, PdfSrc.PageCount);
PdfSrc.Active := False;
end;
PdfDest.SaveAs(OutputPath);
finally
PdfSrc.Free;
PdfDest.Free;
end;
end;
Deux choses dans ce code se laissent facilement manquer à la 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 absent, endommagé ou protégé par mot de passe, PDFium intercepte l'erreur en interne et laisse Active à False. Sans le contrôle explicite de la ligne 10, un mauvais fichier disparaîtrait silencieusement de la fusion sans aucune trace dans la sortie. Le PDF final aurait moins de pages que prévu et vous ne sauriez pas quel fichier est en cause
La seconde est le compteur InsertAt. Le troisième argument d'ImportPages est la position, comptée à partir de 1, où atterrit dans la destination 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, si bien que le lot de pages suivant se greffe après le dernier. Oubliez de l'incrémenter et chaque source suivante écrase les pages à la position 1, ne vous laissant que 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 passée en deuxième argument suit un format simple à virgules et tirets : "1-3" prend les pages 1 à 3, "2,4,6" choisit trois pages précises, et "1-" signifie de la page 1 jusqu'à la fin du document. Les plages peuvent se combiner dans une seule chaîne, si bien que "1-3,5,7-" saute les pages 4 et 6. Une subtilité compte ici : les numéros renvoient toujours aux pages du document source, à partir de 1, quel que soit l'endroit où ces pages finissent dans la destination. Si vous voulez les pages 40 à 50 d'un catalogue de 200 pages, la chaîne de plage est "40-50", pas une position relative à ce que la destination contient déjà
// Extrait la couverture et un résumé exécutif 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;
Quand vous calculez l'incrément d'InsertAt, comptez les pages réellement importées, pas le nombre de pages de la source. Si vous passez '1,3-5' vous avez importé 4 pages : avancez donc de 4. Avancer de PdfSrc.PageCount laisserait un trou de positions vides dans la destination 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 emportent leur contenu visible intact. Le texte, les graphiques vectoriels, les images matricielles, les polices incorporées et les XObject de formulaire sont tous transférés au titre des flux de contenu de la page. Les annotations au niveau de la page, y compris les commentaires, les surlignages et les traits à main levée, suivent aussi, parce qu'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. Le titre, l'auteur, le sujet et les mots-clés du dictionnaire Info de la source restent en arrière. Le document de destination démarre avec des métadonnées vides après CreateDocument : si la sortie fusionnée a besoin de ces champs, vous devez donc les affecter directement à PdfDest avant d'appeler SaveAs. Les propriétés Title, Author, Subject, Keywords et Creator de TPdf prennent de simples chaînes et écrivent dans le dictionnaire Info à l'enregistrement
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 que dans les flux de pages individuels. Quand ImportPages copie une page qui contient des champs de formulaire, l'apparence visuelle de ces champs est transférée parce qu'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 texte d'un document source affichera la valeur qu'il avait au moment de l'import, mais il ne sera pas modifiable dans le fichier fusionné. Si vous avez besoin que les champs restent remplissables, aplatissez-les dans chaque document source avant l'import : cela cuit les valeurs courantes dans le flux de contenu et retire la couche interactive, ce qui donne un résultat visuel propre sans widgets cassés dans la sortie
Fichiers sources chiffrés
Les documents sources protégés par mot de passe s'ouvrent comme les autres, avec une propriété de plus à définir d'abord. Affectez le mot de passe à PdfSrc.Password avant de basculer Active := True, et PDFium s'en servira pendant l'ouverture :
PdfSrc.Password := 'user-password';
PdfSrc.FileName := 'protected.pdf';
PdfSrc.Active := True;
if not PdfSrc.Active then
raise Exception.Create('Wrong password or file cannot be opened');
PdfDest.ImportPages(PdfSrc, '1-' + IntToStr(PdfSrc.PageCount), InsertAt);
Inc(InsertAt, PdfSrc.PageCount);
PdfSrc.Active := False;
Un mauvais mot de passe produit le même résultat silencieux Active = False qu'un fichier absent : le contrôle explicite est donc tout aussi nécessaire ici. Le chiffrement ne se transmet pas à la destination : les pages importées d'une source protégée atterrissent dans la destination comme du contenu non protégé. Si la sortie fusionnée doit elle aussi être chiffrée, configurez-le sur PdfDest avant d'appeler SaveAs
Enregistrer le résultat
SaveAs sur TPdf accepte soit un chemin de fichier, soit un TStream. Pour la plupart des fusions, la surcharge fichier est celle que vous voulez :
PdfDest.SaveAs('merged-output.pdf');
Le deuxième argument facultatif est un TSaveOption qui contrôle le mode d'enregistrement. Le défaut, saNone, écrit une mise à jour incrémentielle si le document a été chargé depuis un fichier, ou une réécriture complète s'il a été créé de zéro. Comme une destination bâtie avec CreateDocument est toujours neuve, la sortie sera un fichier compact à révision unique. Le troisième argument, TPdfVersion, permet d'épingler l'en-tête de version PDF quand des consommateurs en aval exigent une version précise ; le laisser à pvUnknown laisse PDFium choisir selon le contenu
Les méthodes ImportPages et SaveAs présentées ici font partie de PDFium Component pour Delphi et C++Builder