La fusion (merge) et la division (split) sont les deux opérations sur les pages vers lesquelles tout le monde se tourne en premier, et elles couvrent un large champ. Elles ne couvrent pas tout. Il existe une famille distincte de travaux qui réorganisent les pages plutôt que de déplacer des fichiers entiers : placer quatre diapositives sur une seule feuille pour un document à distribuer (handout), faire glisser une page de la fin d'un document vers le début, ou extraire les pages 3, 7 et 12 dans un court extrait sans toucher au reste. PDFium expose trois méthodes précisément pour cela, et chacune se comporte différemment de la fusion et de la division que vous connaissez déjà. Cet article explique ce qu'elles font, où se trouvent les points de sortie, et un détail de propriété qui a causé un plantage sur le terrain
Les trois méthodes sont ImportNPagesToOne pour l'imposition N-up, MovePages pour la réorganisation sur place (in-place), et ImportPagesByIndex pour l'extraction de sous-ensembles. La fusion empile les documents bout à bout et laisse le nombre de pages égal à la somme des entrées. La division é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 d'elles modifie le nombre de pages source partageant une feuille, l'une d'elles modifie l'ordre au sein d'un document unique, et la dernière copie une poignée de pages choisies dans un autre document. Savoir laquelle utiliser vous évite d'imposer une danse fusion-et-suppression là où un seul appel suffirait
Ce que fait réellement l'imposition N-up
L'imposition est le terme de prépresse (prepress) pour organiser plusieurs pages source sur une feuille plus grande de sorte que le résultat imprimé et plié se lise dans le bon ordre. La version quotidienne est le document à distribuer de 2 pages (2-up), le cahier de livret (booklet signature) de 4 pages, ou la planche contact qui fait tenir une douzaine de miniatures sur une page. PDFium gère la géométrie via un seul appel :
function ImportNPagesToOne(
OutputWidth, OutputHeight: Single;
NumX, NumY : Cardinal): TPdf;
NumX et NumY décrivent la grille. Une valeur de 2, 1 place deux pages source côte à côte ; 2, 2 en regroupe quatre dans une disposition en quadrant ; 4, 3 construit une planche contact de douze pages (twelve-up). PDFium lit les pages source dans l'ordre, réduit chacune d'elles pour l'adapter à sa cellule, et remplit la grille de gauche à droite, de haut en bas, en commençant une nouvelle feuille de sortie chaque fois que la grille actuelle est pleine. Les pages source ne sont pas modifiées. Ce que vous récupérez est un nouveau document dont les pages sont des composites
La taille de sortie est en points, pas en pixels
OutputWidth et OutputHeight sont des unités utilisateur PDF (user units), et une unité utilisateur PDF correspond à un point, soit un soixante-douzième de pouce. L'unité déclare la taille physique de la feuille de sortie, et elle n'a rien à voir avec les pixels de l'écran ou le DPI de rendu. C'est l'endroit de loin le plus courant où l'on se trompe sur une imposition, car un développeur habitué aux bitmaps opte pour un nombre de pixels et se retrouve avec une feuille de la taille d'un timbre-poste ou d'un panneau d'affichage
Les nombres à mémoriser sont les deux tailles de page que vous utiliserez le plus. Le format US Letter fait 612 par 792 points, car 8,5 pouces multipliés par 72 font 612 et 11 pouces multipliés par 72 font 792. Le format A4 fait environ 595 par 842 points, d'après ses dimensions de 210 par 297 millimètres. L'en-tête de la liaison lui-même énonce clairement la règle, à savoir qu'une unité équivaut à un soixante-douzième de pouce, et l'unité fournit une constante PointsPerInch égale à 72 si vous préférez calculer une taille à partir de pouces dans le code plutôt que d'écrire le littéral
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;
// Quatre pages source par feuille Letter, grille 2 par 2.
Composite := Source.ImportNPagesToOne(LetterW, LetterH, 2, 2);
if Composite = nil then
raise Exception.Create('PDFium a rejete les arguments dimposition');
Composite.SaveAs('slides-4up.pdf');
finally
Composite.Free; // voir la section suivante : ceci est obligatoire
Source.Free;
end;
end;
Le descripteur (handle) renvoyé vous appartient et vous devez le libérer
Lisez à nouveau la signature. ImportNPagesToOne renvoie un TPdf, pas un booléen. Cette valeur de retour est un tout nouveau descripteur de document, alloué séparément de la source, et c'est l'appelant qui le possède. Le TPdf source sur lequel vous avez appelé la méthode est intact et possède toujours son propre descripteur ; le composite est un deuxième objet indépendant. Si vous laissez le TPdf renvoyé sortir de la portée (out of scope) sans le libérer, vous provoquez une fuite (leak) d'un document PDFium entier
L'erreur la plus dangereuse va dans l'autre sens. Sous le capot, la méthode demande à PDFium un nouveau FPDF_DOCUMENT via FPDF_ImportNPagesToOne, puis enveloppe ce descripteur brut (raw handle) à l'intérieur du TPdf renvoyé afin que la durée de vie de l'enveloppe régisse celle du descripteur. À partir de ce moment, il y a exactement un propriétaire du descripteur, et exactement un endroit où il doit être fermé : lorsque vous faites un Free sur l'objet renvoyé. Un chemin d'erreur négligent qui libère à la fois l'enveloppe et appelle également FPDF_CloseDocument sur le descripteur brut qu'il a capturé ferme le même document PDFium deux fois. Il s'agit d'une double libération (double-free), et c'est le bogue spécifique qui a déjà posé problème à un appelant ici. La règle pour l'éviter est courte. Fermez le document par un seul chemin, en libérant le TPdf que la méthode vous a remis, et ne passez jamais outre l'enveloppe pour fermer le descripteur qu'elle a déjà adopté
Deux corollaires en découlent. Premièrement, la méthode renvoie nil lorsque PDFium rejette les arguments, par exemple un zéro sur l'un des axes de la grille ou un échec d'allocation, une vérification de nil s'impose donc avant de toucher au résultat. Deuxièmement, initialisez votre variable de sortie à nil avant le try et libérez-la dans le finally, comme le fait l'exemple ci-dessus, de sorte qu'une défaillance en cours de route ne puisse pas vous amener à libérer une référence non définie ou à ignorer purement et simplement la libération
Réorganiser les pages sans les réécrire
L'imposition construit un nouveau document. La réorganisation modifie un document sur place (in place). MovePages extrait un ensemble de pages de leurs positions actuelles et les dépose vers une destination, en décalant tout le reste autour du bloc déplacé de sorte que le nombre de pages reste le même :
function MovePages(
const PageIndices: array of Integer;
DestPageIndex : Integer): Boolean;
Les index commencent à zéro (zero-based). PageIndices liste les pages à déplacer, dans l'ordre où elles doivent se retrouver, et DestPageIndex est l'index sur lequel atterrit la première page déplacée une fois le déplacement stabilisé. Étant donné que PDFium déplace les pages plutôt que de copier et de recompresser leur contenu, l'opération est peu coûteuse et sans perte : les objets de page conservent leurs flux, leurs ressources et leur fidélité. C'est l'appel derrière un panneau de pages glisser-pour-réorganiser, où un utilisateur tire une miniature vers un nouvel emplacement et où vous validez le nouvel ordre d'un seul mouvement. La méthode renvoie False lorsqu'un index est hors limites, validez donc le résultat au lieu de supposer que la réorganisation a été prise en compte
var
Doc: TPdf;
begin
Doc := TPdf.Create(nil);
try
Doc.FileName := 'report.pdf';
Doc.Active := True;
// Déplacer la dernière page (index 4 dans un fichier de 5 pages) tout à l'avant.
if not Doc.MovePages([4], 0) then
raise Exception.Create('MovePages a rejete l''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 à un autre. ImportPagesByIndex prend le document source et un tableau d'index de base zéro, et insère ces pages dans la cible à une position choisie :
function ImportPagesByIndex(
Source : TPdf;
const PageIndices: array of Integer;
InsertAt : Integer= 0): Boolean;
Vous l'appelez sur le document cible et transmettez la source comme premier argument. PageIndices nomme les pages source à extraire, dans l'ordre où vous les souhaitez ; InsertAt est l'emplacement de base zéro dans la cible où va la première page importée, donc 0 les place avant la première page existante et le nombre actuel de pages de la cible est ajouté à la suite. Un tableau vide importe toutes les pages, ce qui fait de l'appel une copie complète lorsque vous en avez besoin. Elle renvoie False si l'un des index est hors limites dans la source
C'est ici que le contraste avec la division (split) importe. La division écrit des fichiers distincts, une opération produisant de nombreuses sorties sur le disque. ImportPagesByIndex effectue la forme inverse de travail : elle rassemble un ensemble de pages choisi dans un seul document cible en mémoire, que vous enregistrez ensuite une fois. Lorsque la tâche consiste à "donnez-moi les pages 3, 7 et 12 sous la forme d'un court document PDF", c'est la voie directe, et elle enveloppe FPDF_ImportPagesByIndex en dessous
var
Source, Excerpt: TPdf;
begin
Source := TPdf.Create(nil);
Excerpt := TPdf.Create(nil);
try
Source.FileName := 'manual.pdf';
Source.Active := True;
Excerpt.CreateDocument; // démarrer une cible vide
// Extraire les pages 3, 7 et 12 (2, 6, 11 en base zéro) dans l'extrait.
if not Excerpt.ImportPagesByIndex(Source, [2, 6, 11], 0) then
raise Exception.Create('Un index de page demande est hors limites');
Excerpt.SaveAs('manual-excerpt.pdf');
finally
Excerpt.Free;
Source.Free;
end;
end;
Rassembler le tout proprement
La forme de bout en bout est la même pour les trois : ouvrez la source en définissant FileName et en passant Active à True, effectuez l'opération, enregistrez avec SaveAs, et libérez ce qui vous appartient. La seule ramification qui nécessite de l'attention est de savoir quels appels allouent un nouveau document. MovePages fait muter le document que vous détenez déjà, il y a donc un seul objet à libérer. ImportPagesByIndex écrit dans une cible que vous avez créée vous-même, vous libérez donc la source et la cible que vous avez ouvertes. ImportNPagesToOne est l'exception, car le nouveau document est la valeur de retour de la méthode plutôt que quelque chose que vous avez construit, et oublier qu'il s'agit d'un descripteur distinct appartenant à l'appelant est la façon dont se produisent à la fois la fuite et la double libération. Initialisez le résultat à nil, vérifiez-le après l'appel, et libérez-le sur un seul chemin
Si la tâche que vous avez réellement à accomplir consiste à combiner des fichiers entiers plutôt qu'à réorganiser des pages, consultez fusion de plusieurs fichiers PDF en un seul document. S'il s'agit de l'inverse, à savoir diviser un document en plusieurs fichiers, consultez division de documents PDF en plusieurs fichiers. Les méthodes d'imposition et de réorganisation décrites ici sont livrées dans le cadre du Composant PDFium pour Delphi et C++Builder, aux côtés des API de chargement, de rendu et d'édition abordées ailleurs sur ce blog