La fusion et le découpage sont les deux opérations sur les pages vers lesquelles tout le monde se tourne en premier, et elles couvrent de nombreux besoins. Cependant, elles ne couvrent pas tout. Il existe une autre catégorie de tâches qui réorganisent les pages plutôt que de manipuler des fichiers entiers : disposer quatre diapositives sur une seule feuille pour un document à distribuer, faire glisser une page de la fin vers le début d'un document ou extraire les pages 3, 7 et 12 dans un court extrait sans toucher au reste. PDFium expose trois méthodes prévues exactement pour cela, et chacune se comporte différemment de la fusion et du découpage classiques. Cet article présente leur rôle, l'emplacement des points de sortie et un détail de propriété de ressource qui a provoqué des plantages sur le terrain.
Ces trois méthodes sont ImportNPagesToOne pour l'imposition N-up, MovePages pour la réorganisation sur place, et ImportPagesByIndex pour l'extraction de sous-ensembles. La fusion assemble les documents bout à bout et renvoie un nombre de pages égal à la somme des entrées. Le découpage écrit plusieurs fichiers de sortie à partir d'une seule entrée. Les trois opérations présentées ici se situent entre les deux : l'une modifie le nombre de pages sources partageant une feuille, une autre modifie l'ordre au sein d'un même document, et la dernière copie une sélection de pages dans un autre document. Savoir les distinguer évite de mettre en œuvre une procédure complexe de fusion et suppression là où un simple appel suffit.
Ce que fait réellement l'imposition N-up
L'imposition est le terme d'imprimerie pour désigner la disposition de plusieurs pages sources sur une feuille plus grande, afin que le résultat imprimé et plié se lise dans le bon ordre. La version courante est le document à distribuer sur 2 pages, le cahier de livret sur 4 pages, ou la planche-contact qui regroupe une douzaine de vignettes sur une page. PDFium gère cette géométrie via un unique appel :
function ImportNPagesToOne(
OutputWidth, OutputHeight: Single;
NumX, NumY : Cardinal): TPdf;
NumX et NumY décrivent la grille. Une valeur de 2, 1 places deux pages sources côte à côte ; 2, 2 en regroupe quatre dans une disposition en quadrants ; 4, 3 construit une planche-contact de douze pages. PDFium lit les pages sources dans l'ordre, réduit chacune d'elles pour l'adapter à sa cellule et remplit la grille de gauche à droite et de haut en bas, créant une nouvelle feuille de sortie dès que la grille courante est pleine. Les pages sources ne sont pas modifiées. Ce que vous obtenez en retour est un nouveau document dont les pages sont des composites.
La taille de sortie est exprimée en points, pas en pixels
OutputWidth et OutputHeight sont des unités utilisateur PDF. Une unité utilisateur PDF correspond à un point, soit un soixante-douzième de pouce. Cette unité définit la taille physique de la feuille de sortie et n'a aucun rapport avec les pixels de l'écran ou la résolution de rendu (DPI). C'est l'erreur la plus classique lors de l'imposition : un développeur habitué aux bitmaps utilise un nombre de pixels et se retrouve avec une feuille de la taille d'un timbre-poste ou d'un panneau publicitaire.
Les valeurs qui méritent d'être mémorisées sont les deux tailles de pages les plus couramment utilisées. Le format US Letter mesure 612 par 792 points (8,5 pouces multipliés par 72 donnent 612, et 11 pouces multipliés par 72 donnent 792). Le format A4 mesure environ 595 par 842 points, d'après ses dimensions de 210 par 297 millimètres. L'en-tête de la liaison énonce clairement cette règle : une unité équivaut à un soixante-douzième de pouce, et la liaison fournit une constante PointsPerInch égale à 72 si vous préférez calculer une taille à partir de pouces dans votre code plutôt que d'écrire une valeur littérale.
const
LetterW = 612.0; // 8.5 in * 72
LetterH = 792.0; // 11 in * 72
var
Source, Composite: TPdf;
begin
Source := TPdf.Create(nil);
Composite := nil;
try
Source.FileName := 'slides.pdf';
Source.Active := True;
// Four source pages per Letter sheet, 2 by 2 grid.
Composite := Source.ImportNPagesToOne(LetterW, LetterH, 2, 2);
if Composite = nil then
raise Exception.Create('PDFium rejected the imposition arguments');
Composite.SaveAs('slides-4up.pdf');
finally
Composite.Free; // see the next section: this is mandatory
Source.Free;
end;
end;
Le descripteur renvoyé doit être libéré par vos soins
Lisez à nouveau la signature de la méthode. ImportNPagesToOne renvoie un TPdf, pas un booléen. Cette valeur de retour est un tout nouveau descripteur de document, alloué indépendamment de la source, et c'est l'appelant qui en est propriétaire. Le TPdf source sur lequel vous avez appelé la méthode reste inchangé et conserve son propre descripteur ; le composite est un second objet autonome. Si vous laissez le TPdf renvoyé sortir de la portée de votre code sans le libérer, vous provoquez une fuite de tout un document PDFium.
L'erreur inverse est encore plus dangereuse. En interne, la méthode demande à PDFium un nouveau FPDF_DOCUMENT via FPDF_ImportNPagesToOne, puis enveloppe ce descripteur brut dans le TPdf renvoyé de sorte que la durée de vie du wrapper régit celle du descripteur. À partir de ce moment, il y a exactement un seul propriétaire pour ce descripteur et un seul endroit où il doit être fermé : lors de l'appel à Free sur l'objet renvoyé. Un traitement d'erreur négligent qui libère le wrapper tout en appelant FPDF_CloseDocument sur le descripteur brut va fermer le même document PDFium deux fois. Il s'agit d'une double libération, et c'est le bug spécifique qui s'est produit par le passé. La règle pour l'éviter est simple. Fermez le document par un unique chemin, en libérant le TPdf que la méthode vous a fourni, et ne tentez jamais de contourner le wrapper pour fermer directement le descripteur qu'il a pris en charge.
Deux corollaires en découlent. Premièrement, la méthode renvoie nil lorsque PDFium rejette les arguments (comme un zéro sur l'un des axes de la grille ou un échec d'allocation), une vérification de valeur non nulle (nil) s'impose donc avant de manipuler le résultat. Deuxièmement, initialisez votre variable de sortie à nil avant le bloc try et libérez-la dans la clause finally, comme dans l'exemple ci-dessus, afin qu'un échec en cours de route ne vous amène pas à libérer une référence non définie ou à omettre sa libération.
Réorganiser les pages sans les réécrire
L'imposition génère un nouveau document. La réorganisation modifie un document en place. MovePages extrait un ensemble de pages de leurs positions actuelles et les place à une destination, décalant tous les autres éléments autour du bloc déplacé de sorte que le nombre de pages reste identique :
function MovePages(
const PageIndices: array of Integer;
DestPageIndex : Integer): Boolean;
Les index commencent à zéro. PageIndices liste les pages à déplacer dans leur ordre final, et DestPageIndex est l'index où la première page déplacée va se positionner une fois le déplacement terminé. Comme PDFium déplace les pages plutôt que de copier et recompresser leur contenu, l'opération est rapide et sans perte : les objets de page conservent leurs flux, leurs ressources et leur intégrité. C'est l'appel utilisé pour un panneau de réorganisation des pages par glisser-déposer, où l'utilisateur déplace une vignette vers un nouvel emplacement et vous validez ce nouvel ordre en un seul appel. La fonction renvoie False si un index est hors limites. Il convient donc de valider le résultat plutôt que de présumer de la réussite du déplacement.
var
Doc: TPdf;
begin
Doc := TPdf.Create(nil);
try
Doc.FileName := 'report.pdf';
Doc.Active := True;
// Move the last page (index 4 in a 5-page file) to the very front.
if not Doc.MovePages([4], 0) then
raise Exception.Create('MovePages rejected the index');
Doc.SaveAs('report-reordered.pdf');
finally
Doc.Free;
end;
end;
Extraire un sous-ensemble par index
La troisième opération copie un ensemble explicite de pages d'un document vers un autre. ImportPagesByIndex prend le document source et un tableau d'index à base zéro, et insère ces pages dans le document cible à la position choisie :
function ImportPagesByIndex(
Source : TPdf;
const PageIndices: array of Integer;
InsertAt : Integer= 0): Boolean;
Vous l'appelez sur le document cible et transmettez le document source en premier argument. PageIndices spécifie les pages sources à extraire dans l'ordre de votre choix ; InsertAt correspond à l'emplacement à base zéro dans la cible où sera insérée la première page importée. Ainsi, 0 les place avant la première page existante tandis que les autres s'ajoutent à la suite. Un tableau vide importe la totalité des pages, ce qui équivaut à faire une copie complète. La méthode renvoie False si l'un des index est hors limites dans la source.
C'est ici que la différence avec le découpage (split) prend tout son sens. Le découpage écrit des fichiers distincts, produisant plusieurs fichiers physiques sur le disque en une seule opération. ImportPagesByIndex effectue le travail inverse : il regroupe une sélection de pages dans un unique document cible en mémoire, que vous enregistrez ensuite en une seule fois. Lorsque la consigne est "donnez-moi les pages 3, 7 et 12 dans un seul PDF court", c'est la voie la plus directe. Elle enveloppe en interne la fonction FPDF_ImportPagesByIndex.
var
Source, Excerpt: TPdf;
begin
Source := TPdf.Create(nil);
Excerpt := TPdf.Create(nil);
try
Source.FileName := 'manual.pdf';
Source.Active := True;
Excerpt.CreateDocument; // start an empty target
// Pull pages 3, 7 and 12 (zero-based 2, 6, 11) into the excerpt.
if not Excerpt.ImportPagesByIndex(Source, [2, 6, 11], 0) then
raise Exception.Create('A requested page index is out of range');
Excerpt.SaveAs('manual-excerpt.pdf');
finally
Excerpt.Free;
Source.Free;
end;
end;
Mettre le tout en œuvre proprement
La structure globale est identique pour ces trois méthodes : ouvrir la source en définissant FileName et en passant Active à True, exécuter l'opération, enregistrer avec SaveAs et libérer les objets alloués. Le seul point délicat consiste à savoir quels appels allouent un nouveau document. MovePages modifie le document existant, il n'y a donc qu'un seul objet à libérer. ImportPagesByIndex écrit dans une cible que vous avez instanciée, vous devez donc libérer la source et la cible que vous avez ouvertes. ImportNPagesToOne est le cas à part, car le nouveau document est renvoyé par la méthode et n'a pas été instancié par vous. Oublier qu'il s'agit d'un descripteur distinct appartenant à l'appelant est la cause des fuites et des doubles libérations. Initialisez le résultat à nil, testez-le après l'appel et libérez-le par un chemin unique.
Si le travail vous devez assembler des fichiers entiers plutôt que de réorganiser des pages, consultez notre article sur la fusion de plusieurs fichiers PDF en un seul document. Si vous devez faire l'inverse et découper un document en plusieurs fichiers, reportez-vous au guide sur le découpage de documents PDF en plusieurs fichiers. Les méthodes d'imposition et de réorganisation présentées ici sont fournies avec le composant PDFium pour Delphi et C++Builder, aux côtés des API de rendu et d'édition abordées par ailleurs sur ce blog.