Article technique

Rendre des pages PDF en JPEG sous Delphi avec PDFium

Rendre une page PDF en JPEG représente deux opérations que l'on a tendance à lancer ensemble puis à déboguer séparément. D'abord vous rastérisez la page en un bitmap de pixels à la résolution que vous choisissez. Ensuite vous confiez ce bitmap à un encodeur JPEG et vous choisissez une qualité. PDFium Component possède la première moitié à travers RenderPage ; la seconde moitié est de la VCL pure, TJPEGImage de Vcl.Imaging.jpeg. La couture entre les deux est l'endroit où vivent les décisions intéressantes, car la résolution choisie côté rendu et la qualité choisie côté encodage se compensent l'une l'autre et jouent sur la taille du fichier de manières faciles à rater

Ce qu'il faut intérioriser avant tout code : une page PDF n'a pas de pixels. Elle est décrite en points, un point valant 1/72 de pouce, et la page est un dessin vectoriel mesuré dans ces points. Quand vous demandez un rendu à PDFium, vous choisissez sur combien de pixels projeter ce dessin, et ce choix est le DPI. Ratez l'arithmétique et vous rendez soit une vignette floue alors que vous vouliez un master d'impression, soit un bitmap de 200 mégapixels pour quelque chose destiné à devenir un aperçu de 120 pixels

L'appel RenderPage de PDFium Component rastérise une page PDF en un TBitmap que le code VCL Delphi encode en JPEG avec TJPEGImage
RenderPage possède la moitié rastérisation et vous rend un TBitmap ; l'encodeur VCL en fait un JPEG une fois la qualité définie

Des DPI aux dimensions en pixels

RenderPage veut une Width et une Height entières en pixels, pas un DPI. Le premier travail est donc la conversion. Une page indique sa taille en points via PageWidth et PageHeight (tous deux Double), et la conversion est celle qu'utilise tout rastériseur : les pixels valent les points multipliés par le DPI cible et divisés par 72. Une page US Letter fait 612 sur 792 points. À 150 DPI cela devient 1275 sur 1650 pixels ; à 72 DPI cela reste 612 sur 792, un pixel par point, le cas dont on oublie qu'il est simplement l'identité

La formule points vers pixels multiplie les points de la page par le DPI divisé par 72 pour dimensionner la sortie de RenderPage en Delphi
RenderPage veut des pixels entiers, donc les points de la page sont multipliés par le DPI et divisés par 72 avant chaque appel
// Pdf.PageNumber doit déjà pointer sur la page voulue.
PixelW := Round(Pdf.PageWidth  * Dpi / 72);
PixelH := Round(Pdf.PageHeight * Dpi / 72);
Bitmap := Pdf.RenderPage(0, 0, PixelW, PixelH, ro0, [], clWhite);
// ... utiliser Bitmap ...
Bitmap.Free;   // la forme fonction de RenderPage vous en cède la propriété

Deux détails dans ces quatre lignes décident si le code est correct. Le premier est que la forme fonction de RenderPage renvoie un TBitmap dont vous êtes propriétaire. PDFium l'a alloué puis s'en est allé ; si vous ne le libérez pas avec Free à chaque itération, un traitement par lot sur quelques centaines de pages fuit quelques centaines de bitmaps et le processus enfle jusqu'à ce que quelque chose lâche. Le second est l'argument Color, ici clWhite. Les pages PDF sont en général dessinées en supposant un support blanc opaque, et une page avec transparence rendue sur la mauvaise couleur de fond produit des bords sales ou des halos sombres parasites. Le blanc est le bon défaut pour presque tous les documents ; le paramètre existe pour le cas rare où il ne l'est pas

Les 0, 0 sont les décalages Left et Top dans la page, exprimés dans l'espace de coordonnées mis à l'échelle, et vous les laissez à zéro sauf si vous rognez. Le ro0 est la rotation : laissez-le à zéro et PDFium honore la rotation que la page déclare déjà dans son entrée /Rotate, si bien qu'une page composée en paysage ressort en paysage sans rien faire de votre part

Encoder le bitmap en JPEG

Une fois le bitmap créé, le JPEG est la partie facile, et c'est du Delphi pur. TJPEGImage.Assign y copie le bitmap, CompressionQuality fixe la qualité sur une échelle de 1 à 100, et SaveToFile écrit le fichier. La seule règle d'ordre est que la qualité doit être définie avant l'enregistrement, car elle gouverne l'encodage que SaveToFile déclenche

uses
  Vcl.Graphics, Vcl.Imaging.jpeg, PDFium;

procedure SavePageAsJpeg(Pdf: TPdf; PageNumber, Dpi, Quality: Integer;
  const FileName: string);
var
  Bitmap: TBitmap;
  Jpeg: TJPEGImage;
begin
  Pdf.PageNumber := PageNumber;
  Bitmap := Pdf.RenderPage(0, 0,
    Round(Pdf.PageWidth  * Dpi / 72),
    Round(Pdf.PageHeight * Dpi / 72),
    ro0, [], clWhite);
  try
    Jpeg := TJPEGImage.Create;
    try
      Jpeg.Assign(Bitmap);
      Jpeg.CompressionQuality := Quality;   // 1..100
      Jpeg.SaveToFile(FileName);
    finally
      Jpeg.Free;
    end;
  finally
    Bitmap.Free;
  end;
end;

Ce try/finally imbriqué paraît tatillon pour une aide sur une seule page, et il est exactement ce qu'il faut pour un lot. Le bloc interne libère l'encodeur, le bloc externe libère le bitmap, et l'un comme l'autre, déclenché par une exception, relâche encore ce qu'il possède. Fusionnez-les en un seul et une exception pendant l'encodage peut abandonner le bitmap. Sur une longue série, c'est la différence entre un convertisseur qui va au bout et un convertisseur qui meurt à la page 300 avec un fichier corrompu et une boîte de dialogue de mémoire insuffisante

Choisir le DPI et la qualité ensemble

Les deux boutons ne sont pas indépendants de la finalité de la sortie, et l'erreur courante consiste à pousser les deux par prudence. Une vignette web rendue à 300 DPI et enregistrée en qualité 95 représente plusieurs centaines de kilo-octets déguisés en image de 120 pixels ; le navigateur en jette presque tout au moment de la réduction. Accordez la résolution aux pixels dont la sortie a réellement besoin, puis choisissez une qualité qui survit à la compression avec perte du JPEG sans artefacts visibles

SortieDPIQualité JPEG
Vignette de liste7260-70
Aperçu à l'écran96-15080-85
Consultation détaillée200-30085-95
Master d'impression300-60090-100

La qualité JPEG mérite à elle seule une mise en garde. Ce n'est pas un cadran linéaire. Le saut de 70 à 85 achète une vraie amélioration visuelle pour une croissance de fichier modeste ; le saut de 95 à 100 double à peu près le fichier pour une différence que presque personne ne voit, car la qualité 100 n'est toujours pas sans perte, elle cesse simplement de jeter grand-chose. Sur les pages chargées de texte, la compression par blocs du JPEG étale les arêtes nettes des glyphes en un léger halo, ce qui explique pourquoi une qualité inférieure à 80 environ donne un texte d'allure scannée là où la sortie devrait être nette. Si les pages sont surtout du texte et que vous pouvez changer de format, le PNG rend ce texte sans le halo ; le JPEG gagne sa place sur le contenu photographique et mixte, où sa compression est réellement plus petite

Des vignettes plus rapides et plus légères

Quand la cible est une vignette plutôt qu'une reproduction fidèle, vous pouvez demander au moteur de rendu d'en faire moins. Le paramètre Options prend un ensemble d'indicateurs TRenderOption, dont quelques-uns échangent la fidélité contre la vitesse exactement comme un petit aperçu le souhaite. reGrayscale abandonne la couleur, ce qui accélère le rendu et produit un bitmap plus petit à encoder. reNoSmoothImage et reNoSmoothPath sautent un anticrénelage de toute façon invisible à l'échelle de la vignette

Une page PDF paysage est ajustée dans une boîte de vignette de 200 sur 280 avec les options reGrayscale et reNoSmoothImage de PDFium Component
Le Min des deux rapports d'ajustement garde les pages paysage et portrait dans la même boîte englobante, et les indicateurs de rendu échangent la fidélité contre la vitesse
function RenderThumbnail(Pdf: TPdf; PageNumber, MaxW, MaxH: Integer): TBitmap;
var
  Scale: Double;
begin
  Pdf.PageNumber := PageNumber;
  // Ajuste la page dans MaxW x MaxH en préservant le rapport d'aspect.
  Scale := Min(MaxW / Pdf.PageWidth, MaxH / Pdf.PageHeight);
  Result := Pdf.RenderPage(0, 0,
    Round(Pdf.PageWidth  * Scale),
    Round(Pdf.PageHeight * Scale),
    ro0, [reGrayscale, reNoSmoothImage], clWhite);
end;

Le cas de la vignette montre aussi la façon la plus propre de penser le dimensionnement. Au lieu de passer par le DPI, calculez un seul facteur d'échelle qui ajuste la page dans une boîte englobante en préservant le rapport d'aspect, ce que fait le Min des deux rapports. Une page portrait et une page paysage finissent toutes deux dans la même boîte sans déformation, et vous n'avez jamais à raisonner sur le DPI qui correspond à « tenir dans 200 sur 280 ». Une réserve avec reGrayscale : il convertit le contenu image matriciel en gris, mais les aplats vectoriels et le texte gardent leurs valeurs de couleur dans le moteur, si bien qu'une page surtout composée de dessin vectoriel peut revenir moins monochrome que le nom de l'indicateur ne le laisse croire. Pour un vrai résultat entièrement en niveaux de gris, convertir le bitmap rendu avec GrayscalePdfBitmap est le chemin fiable

Traiter tout un document par lot

Assembler tout cela pour un document complet est une boucle sur PageCount, avec PageNumber avancé d'une page à la fois. Les pages commencent à 1 : la page une est PageNumber := 1, et la boucle va jusqu'à PageCount inclus, pas PageCount - 1. L'autre chose que le lot doit respecter est le contrat de chargement silencieux. Poser Active := True ne lève jamais rien sur un fichier endommagé ou un mauvais mot de passe ; cela laisse simplement Active à False. Vérifiez-le avant de rendre la moindre page, sinon le premier RenderPage travaille contre un document qui n'a jamais été ouvert

procedure ExportAllPages(const PdfPath, OutDir: string; Dpi, Quality: Integer);
var
  Pdf: TPdf;
  I, Digits: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := PdfPath;
    Pdf.Active := True;
    if not Pdf.Active then
      raise Exception.Create('Could not open ' + PdfPath);

    Digits := Length(IntToStr(Pdf.PageCount));   // complète par des zéros pour un tri correct
    for I := 1 to Pdf.PageCount do
      SavePageAsJpeg(Pdf, I, Dpi, Quality,
        Format('%s\page_%.*d.jpg', [OutDir, Digits, I]));
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Le remplissage par zéros via Digits est une petite chose qui épargne un après-midi plus tard. Nommez les fichiers page_1.jpg jusqu'à page_10.jpg et tout outil qui les trie comme des chaînes place page_10 juste après page_1, brouillant l'ordre. Compléter à la largeur du numéro de page le plus élevé, si bien qu'un document de 300 pages donne page_001.jpg, garde l'ordre lexical et l'ordre des pages identiques partout en aval

Pour les documents assez gros pour que la conversion prenne un temps sensible, exécutez-la hors du thread d'interface ou faites circuler les messages entre les pages afin que l'application reste réactive, et donnez à l'utilisateur un moyen d'arrêter. Si vous rendez de très grandes pages et voulez une annulation qui morde en cours de page plutôt qu'entre deux pages seulement, PDFium Component dispose d'un chemin de rendu progressif avec jeton d'annulation ; c'est un mécanisme plus lourd que ce dont la plupart des exports par lot ont besoin, mais il est là quand une seule page à 600 DPI est déjà assez lente pour bloquer

Un dernier appariement à connaître. Rastériser une page abandonne sa couche de texte : le JPEG, ce sont des pixels, et les mots qui s'y trouvent ne sont plus sélectionnables ni cherchables. Quand vous avez besoin à la fois de l'image et du texte sous-jacent, faites le rendu pour l'image et récupérez le texte à part, ce que couvre l'article compagnon sur l'extraction de texte des documents PDF avec PDFium Component. Les surcharges de RenderPage et les options de rendu présentées ici font partie de PDFium Component pour Delphi et C++Builder