Article technique

Combiner des images numérisées en un seul PDF avec le composant PDFium dans Delphi

Une équipe de traitement des réclamations avait trente ans de dossiers papier passant par un scanner à défilement (sheet-fed scanner). Le scanner a craché un JPEG par page dans un dossier, nommé 0001.jpg, 0002.jpg, et ainsi de suite. Ce dont l'archive avait réellement besoin, c'était d'un PDF par dossier, avec les pages dans l'ordre, afin qu'un réviseur puisse ouvrir un seul document au lieu de cliquer sur une centaine de miniatures d'images. Cette dernière étape, transformer une pile numérotée de numérisations en un seul PDF ordonné, est le travail ici

Le composant PDFium le gère directement. Au-delà du rendu et de l'extraction de texte, le composant peut construire un PDF à partir de zéro : créer un document vide, ajouter une page blanche de la taille de votre choix, déposer une image sur cette page dans les coordonnées de l'espace utilisateur, puis l'enregistrer. Tout le pipeline (pipeline) réside sur le composant TPdf, de sorte qu'un convertisseur par lots (batch converter) est une boucle sur les noms de fichiers plus une poignée d'appels

La forme de la conversion

Trois choses doivent se produire pour chaque numérisation. Vous décidez de la taille de la page, vous placez l'image à l'intérieur de la page en laissant une marge, et vous avancez à la page suivante. Le composant PDFium vous donne une méthode pour chacune : AddPage crée une page blanche à une taille donnée, AddImage (ou AddPicture si vous détenez déjà un TPicture) dessine le bitmap dans la page courante, et PageNumber indique au composant quelle page les appels de dessin (draw calls) suivants ciblent

Le détail qui fait trébucher (trips people up) les gens est le système de coordonnées. L'espace utilisateur PDF (user space) place l'origine dans le coin inférieur gauche de la page, Y augmentant vers le haut, à l'opposé des coordonnées d'écran que les développeurs Delphi recherchent par réflexe. Le X, Y que vous transmettez à AddImage est le coin inférieur gauche du rectangle de l'image, et Width, Height sont la taille d'emplacement en points, et non la taille en pixels du fichier source. Si vous vous trompez de sens, vos numérisations atterrissent en dehors de la page ou à l'envers par rapport à l'endroit où vous les attendiez

Création du document et d'une page par numérisation

Commencez par un document vide. CreateDocument alloue un nouveau PDF et laisse le composant actif, il n'y a donc pas d'étape d'ouverture séparée. De là, vous parcourez (walk) la liste des fichiers numérisés, et pour chacun, vous ajoutez une page, la rendez courante, et placez l'image. Les dimensions de page ici sont A4 en points (595 × 842 portrait), la taille de feuille standard pour la correspondance archivée

procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
  PageW = 595.0;   // largeur A4 en points
  PageH = 842.0;   // hauteur A4 en points
  Margin = 36.0;   // bordure d'un demi-pouce autour de chaque numérisation
var
  I: Integer;
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                       // nouveau, vide, déjà actif
    for I := 0 to Files.Count - 1 do
    begin
      Pdf.AddPage(I + 1, PageW, PageH);       // index de page basé sur 1
      Pdf.PageNumber := I + 1;                // rendre la nouvelle page courante
      PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
    end;
    Pdf.SaveAs(OutputPath);
  finally
    Pdf.Free;
  end;
end;

Chaque itération crée une page et y affecte (sets) immédiatement PageNumber. Cette deuxième ligne compte : AddPage insère la page mais les méthodes de dessin (draw methods) agissent sur la page courante, de sorte que la définition de PageNumber est ce qui vise (aims) AddImage sur la page que vous venez de créer. Sautez-la et vos images s'empilent sur n'importe quelle page qui se trouvait être chargée auparavant

Une hypothèse se cache dans cette boucle : l'ordre des Files. Un scanner nomme les pages 0001.jpg à 0100.jpg, mais une énumération de répertoire ne les renvoie pas toujours triées, et au moment où vous atteignez page9.jpg à côté de page10.jpg, un simple tri de chaîne (string sort) place la page 10 avant la page 9. Triez la liste explicitement avant la boucle, et préférez les noms remplis de zéros (zero-padded) au moment de la numérisation afin que l'ordre lexical corresponde à l'ordre des pages. La séquence des pages est la seule chose qu'un réviseur remarque immédiatement, et c'est l'erreur la moins chère (cheapest) à prévenir

Placement d'une numérisation et maintien de ses proportions (aspect ratio)

Une numérisation a rarement la même forme que la page. Si vous l'étirez pour remplir la feuille, vous déformez le texte ; si vous le placez en taille de pixel réelle, il déborde. La solution (fix) consiste à mettre à l'échelle (scale) par le plus petit des deux rapports (ratios), l'ajustement de la largeur (width-fit) ou l'ajustement de la hauteur (height-fit), et à centrer ce qui reste. Parce que l'origine se trouve en bas à gauche, le centrage signifie diviser (splitting) l'espace restant de manière égale et l'ajouter à la fois à X et à Y

procedure TArchiveForm.PlaceScan(Pdf: TPdf; const FileName: string;
  PageW, PageH, Margin: Double);
var
  Pic: TPicture;
  AvailW, AvailH, Scale, DrawW, DrawH, X, Y: Double;
begin
  Pic := TPicture.Create;
  try
    Pic.LoadFromFile(FileName);              // BMP, JPG, PNG, etc. via les unités graphiques VCL

    AvailW := PageW - 2 * Margin;
    AvailH := PageH - 2 * Margin;

    // Ajuster à l'intérieur des marges sans déformer la numérisation.
    Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
    DrawW := Pic.Width * Scale;
    DrawH := Pic.Height * Scale;

    // Centrer : l'espace restant (leftover space) est divisé de manière égale. Y mesuré à partir du bas de la page.
    X := (PageW - DrawW) / 2;
    Y := (PageH - DrawH) / 2;

    Pdf.AddImage(FileName, X, Y, DrawW, DrawH);
  finally
    Pic.Free;
  end;
end;

Ceci charge le fichier une fois pour lire ses dimensions en pixels, calcule une échelle uniforme unique (single uniform scale) et passe le rectangle d'emplacement à AddImage. AddImage accepte directement un chemin de fichier et l'achemine via le même pipeline d'images qu'AddPicture, de sorte que tout format reconnu par les unités graphiques VCL fonctionne sans cas particulier (special-casing). Si vous avez déjà l'image décodée dans un TPicture à partir d'un volet de prévisualisation (preview pane), appelez AddPicture(Pic, X, Y, DrawW, DrawH) avec le même rectangle et sautez la deuxième lecture de fichier

Sauter le décodage pour les numérisations JPEG

Les scanners émettent presque toujours du JPEG. Charger un JPEG dans un TPicture le décode en un bitmap, puis PDFium le ré-encode lors de l'enregistrement, deux allers-retours (round trips) avec perte (lossy) dont vous n'avez pas besoin. AddJpegImage intègre (embeds) les octets compressés d'origine directement dans la page à partir d'un flux (stream), ce qui est à la fois plus rapide et visuellement plus propre pour un lot à volume élevé

var
  Stream: TFileStream;
begin
  // ... après AddPage + PageNumber pour la page courante ...
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    // Intègre les octets JPEG tels quels ; pas de cycle décodage/ré-encodage.
    Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
  finally
    Stream.Free;
  end;
end;

Vous calculez toujours X, Y, DrawW et DrawH de la même manière, car vous avez besoin des dimensions en pixels pour la mise à l'échelle. Lisez-les à partir du fichier ou d'une analyse rapide d'en-tête (quick header parse), puis remettez le flux brut (raw stream) à AddJpegImage. Pour les numérisations PNG ou TIFF, le chemin AddImage est le bon ; réservez le raccourci JPEG pour le format auquel il s'applique réellement

Étiquetage de chaque page

Les numérisations archivées sont plus faciles à vérifier (audit) lorsque chaque page porte son nom de fichier source. AddText dessine une chaîne (string) à une coordonnée de l'espace utilisateur, de sorte qu'une légende se trouve juste sous l'image. N'oubliez pas l'axe Y inversé : pour mettre une étiquette (label) en dessous de la numérisation, vous soustrayez du bord inférieur (bottom edge) de l'image plutôt que d'y ajouter

// Légende en dessous de la numérisation : Y diminue vers le bas de la page.
Pdf.AddText('Fichier : ' + ExtractFileName(FileName), 'Helvetica', 9,
  X, Y - 14, clGray);

Un dernier point concernant l'enregistrement. SaveAs est une fonction qui renvoie un booléen, donc dans le code de production, vérifiez son résultat plutôt que de supposer que l'écriture a réussi ; un disque plein ou un chemin de sortie verrouillé (locked output path) échoue silencieusement (fails quietly) autrement. Une fois la boucle terminée et le fichier écrit, vous avez exactement ce dont l'archive avait besoin : un PDF ordonné par dossier, des pages mises à l'échelle (scaled to fit), prêtes à être lues dans n'importe quelle visionneuse

Les mêmes éléments de base (building blocks) couvrent des tâches connexes. Échangez la règle de dimensionnement par page et vous obtenez un livre photo avec une image par feuille ; gardez la boucle mais lisez à partir d'une source multipage TIFF et vous obtenez un convertisseur d'archives de fax. Si vous voulez avoir une vue d'ensemble (wider picture) de la création de PDF par programmation, consultez la création de documents PDF à partir de zéro avec le composant PDFium ; pour rendre (render) le résultat à l'écran ultérieurement, consultez la conversion de pages PDF en images JPEG avec le composant PDFium

Le Composant PDFium de loslab.com regroupe (bundles) les API de création de documents, de rendu et de texte utilisées tout au long de cette série