Article technique

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

Une équipe chargée du traitement des dossiers avait trente ans d'archives papier passant par un scanner à défilement. Le scanner crachait un JPEG par page dans un dossier, nommés 0001.jpg, 0002.jpg, et ainsi de suite. Ce dont les archives avaient réellement besoin, c'était un PDF par dossier de cas, avec les pages dans l'ordre, afin qu'un relecteur puisse ouvrir un seul document au lieu de cliquer sur une centaine de miniatures d'images. C'est cette dernière étape, transformer une pile numérotée de numérisations en un seul PDF ordonné, qui est l'objet de cet article.

PDFium VCL s'en charge 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 vierge de la taille voulue, déposer une image sur cette page en coordonnées de l'espace utilisateur, puis enregistrer. Toute la chaîne de traitement repose sur le composant TPdf, de sorte qu'un convertisseur par lot se réduit à une boucle sur les noms de fichiers plus quelques appels.

La structure de la conversion

Trois opérations sont nécessaires pour chaque numérisation. Vous choisissez la taille de page, vous placez l'image à l'intérieur de la page en laissant une marge, et vous passez à la page suivante. PDFium VCL fournit une méthode pour chacune : AddPage crée une page vierge à une taille donnée, AddImage (ou AddPicture si vous détenez déjà un TPicture) dessine le bitmap sur la page courante, et PageNumber indique au composant quelle page ciblent les appels de dessin suivants.

Le détail qui piège les développeurs est le système de coordonnées. L'espace utilisateur PDF place l'origine dans le coin inférieur gauche de la page, avec Y croissant vers le haut, à l'opposé des coordonnées écran auxquelles les développeurs Delphi ont le réflexe de recourir. Les valeurs X, Y passées à AddImage représentent le coin inférieur gauche du rectangle de l'image, et Width, Height sont la taille de placement en points, non la taille en pixels du fichier source. Inverser ces valeurs fait atterrir vos numérisations hors de la page ou à l'envers par rapport à l'endroit attendu.

Créer le document et 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 distincte. Ensuite, vous parcourez 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), le format standard pour la correspondance archivée.

procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
  PageW = 595.0;   // A4 width in points
  PageH = 842.0;   // A4 height in points
  Margin = 36.0;   // half-inch border around each scan
var
  I: Integer;
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                       // new, empty, already active
    for I := 0 to Files.Count - 1 do
    begin
      Pdf.AddPage(I + 1, PageW, PageH);       // 1-based page index
      Pdf.PageNumber := I + 1;                // make the new page current
      PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
    end;
    Pdf.SaveAs(OutputPath);
  finally
    Pdf.Free;
  end;
end;

Chaque itération crée une page et définit immédiatement PageNumber sur elle. Cette seconde ligne est importante : AddPage insère la page mais les méthodes de dessin agissent sur la page courante, donc définir PageNumber est ce qui oriente AddImage vers la page que vous venez de créer. Omettre cette ligne empile vos images sur la page qui se trouvait chargée avant.

Une hypothèse se cache dans cette boucle : l'ordre de Files. Un scanner nomme les pages 0001.jpg à 0100.jpg, mais une énumération de répertoire ne les retourne pas toujours triées, et dès que vous avez page9.jpg à côté de page10.jpg, un tri purement lexicographique place la page 10 avant la page 9. Triez la liste explicitement avant la boucle, et préférez des noms avec zéro de remplissage lors de la numérisation, de façon à ce que l'ordre lexical corresponde à l'ordre des pages. La séquence des pages est ce qu'un relecteur remarque immédiatement, et c'est l'erreur la moins coûteuse à prévenir.

Placer une numérisation en conservant son 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 la placez à sa taille réelle en pixels, elle déborde. La solution consiste à mettre à l'échelle selon le plus petit des deux rapports, ajustement en largeur ou en hauteur, puis à centrer ce qui reste. Comme l'origine se trouve dans le coin inférieur gauche, centrer signifie diviser l'espace restant en deux parties égales et les 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 the VCL graphics units

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

    // Fit inside the margins without distorting the scan.
    Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
    DrawW := Pic.Width * Scale;
    DrawH := Pic.Height * Scale;

    // Center: leftover space split evenly. Y measured from the page bottom.
    X := (PageW - DrawW) / 2;
    Y := (PageH - DrawH) / 2;

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

Ce code charge le fichier une seule fois pour lire ses dimensions en pixels, calcule une échelle uniforme unique, et passe le rectangle de placement à AddImage. AddImage accepte directement un chemin de fichier et le fait passer par le même pipeline d'image que AddPicture, de sorte que tout format reconnu par les unités graphiques VCL fonctionne sans traitement spécial. Si vous avez déjà l'image décodée dans un TPicture depuis un panneau d'aperçu, appelez AddPicture(Pic, X, Y, DrawW, DrawH) avec le même rectangle et évitez une seconde lecture du fichier.

Éviter 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 bitmap, puis PDFium le réencode à l'enregistrement : deux passes avec perte que vous n'avez pas besoin d'effectuer. AddJpegImage intègre directement les octets compressés d'origine dans la page depuis un flux, ce qui est à la fois plus rapide et visuellement plus propre pour un traitement en volume.

var
  Stream: TFileStream;
begin
  // ... after AddPage + PageNumber for the current page ...
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    // Embeds the JPEG bytes as-is; no decode/re-encode cycle.
    Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
  finally
    Stream.Free;
  end;
end;

Vous calculez quand même X, Y, DrawW et DrawH de la même façon, car vous avez besoin des dimensions en pixels pour l'échelle. Lisez-les depuis le fichier ou par un rapide parsage d'en-tête, puis passez le flux brut à AddJpegImage. Pour les numérisations PNG ou TIFF, le chemin via AddImage est le bon ; réservez le raccourci JPEG au format auquel il s'applique.

Légender chaque page

Les numérisations archivées sont plus faciles à auditer lorsque chaque page porte le nom de son fichier source. AddText dessine une chaîne à une coordonnée de l'espace utilisateur, de sorte qu'une légende se place juste sous l'image. N'oubliez pas l'axe Y inversé : pour placer une légende sous la numérisation, on soustrait depuis le bord inférieur de l'image au lieu d'y ajouter.

// Caption below the scan: Y decreases toward the page bottom.
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
  X, Y - 14, clGray);

Un dernier point concernant l'enregistrement. SaveAs est une fonction qui retourne un booléen ; dans du 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é échoue silencieusement sinon. Une fois la boucle terminée et le fichier écrit, vous obtenez exactement ce dont les archives avaient besoin : un PDF ordonné par dossier de cas, pages adaptées à la mise en page, prêt à lire dans n'importe quelle visionneuse.

Les mêmes blocs de construction couvrent des tâches connexes. Modifiez la règle de dimensionnement par page et vous obtenez un livre photo avec une image par feuille ; conservez la boucle mais lisez depuis une source TIFF multipage et vous avez un convertisseur d'archives de fax. Pour une vue d'ensemble de la création programmatique de PDFs, consultez créer des documents PDF à partir de zéro avec PDFium VCL ; pour restituer ensuite le résultat à l'écran, consultez convertir des pages PDF en images JPEG avec PDFium VCL.

Le composant PDFium VCL de loslab.com regroupe les API de création de documents, de rendu et de traitement de texte utilisées tout au long de cette série.