Article technique

Rendre des pages PDF en images JPEG dans Delphi avec le composant PDFium

Le rendu d'une page PDF en JPEG représente deux opérations que les gens ont tendance à exécuter ensemble puis à déboguer séparément. Tout d'abord, vous pixellisez (rasterize) la page en un bitmap de pixels à une résolution que vous choisissez. Ensuite, vous remettez ce bitmap à un encodeur JPEG et choisissez une qualité. Le composant PDFium possède la première moitié via RenderPage ; la seconde moitié est du pur VCL, TJPEGImage de Vcl.Imaging.jpeg. La couture (seam) entre eux est l'endroit où se trouvent les décisions intéressantes, car la résolution que vous choisissez du côté du rendu et la qualité que vous choisissez du côté de l'encodage se font des compromis (trade off) les unes par rapport aux autres et par rapport à la taille du fichier d'une manière avec laquelle il est facile de se tromper

La chose à intérioriser (internalize) avant tout code : une page PDF n'a pas de pixels. Elle est décrite en points, où un point vaut 1/72 de pouce, et la page est un dessin vectoriel mesuré dans ces points. Lorsque vous demandez à PDFium de faire le rendu, vous choisissez sur combien de pixels projeter ce dessin, et ce choix est le DPI. Trompez-vous dans l'arithmétique et soit vous rendez une miniature floue alors que vous vouliez un original (print master) d'impression, soit vous allouez un bitmap de 200 mégapixels pour quelque chose destiné à être un aperçu de 120 pixels

Du DPI aux dimensions en pixels

RenderPage veut des entiers Width et Height en pixels, pas un DPI. Le premier travail est donc la conversion. Une page signale sa taille en points via PageWidth et PageHeight (tous deux Double), et la conversion est la même que celle utilisée par chaque pixelliseur (rasterizer) : les pixels sont égaux aux points multipliés par le DPI cible divisé par 72. Une page lettre US (US Letter) mesure 612 par 792 points. À 150 DPI, cela devient 1275 par 1650 pixels ; à 72 DPI, il reste 612 par 792, un pixel par point, ce qui est le cas que les gens oublient qui n'est que l'identité

// Pdf.PageNumber doit déjà pointer vers la page que vous voulez.
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 (function-form) RenderPage vous donne 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 que vous possédez. PDFium l'a alloué et s'en est allé ; si vous ne le libérez pas (Free) à chaque itération, un lot (batch) de quelques centaines de pages fuit de quelques centaines de bitmaps et le processus gonfle jusqu'à ce que quelque chose tombe en panne. Le second est l'argument Color, clWhite ici. Les pages PDF sont généralement dessinées en supposant un substrat blanc opaque, et une page avec une transparence rendue sur la mauvaise couleur d'arrière-plan produit des bords boueux (muddy edges) ou des halos sombres parasites (stray dark halos). Le blanc est la bonne valeur par défaut pour presque tous les documents ; le paramètre existe pour le cas rare où ce n'est pas le cas

Les 0, 0 sont les décalages (offsets) Left et Top dans la page, dans l'espace de coordonnées mis à l'échelle (scaled coordinate space), et vous les laissez à zéro à moins que vous ne recadriez (cropping). 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, de sorte qu'une page créée en paysage sort en paysage sans que vous n'ayez rien à faire

Encodage du bitmap en JPEG

Une fois que le bitmap existe, le JPEG est la partie facile, et c'est du pur Delphi. TJPEGImage.Assign y copie le bitmap, CompressionQuality définit 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 régit l'encodage (encode) 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é semble pointilleux pour un assistant (helper) d'une page, et il est tout à fait adapté pour un lot (batch). Le bloc interne libère l'encodeur, le bloc externe libère le bitmap, et si l'un ou l'autre se déclenche sur une exception, il libère toujours ce qu'il possède. Réduisez-les (Collapse) en un seul et une exception pendant l'encodage peut bloquer (strand) le bitmap. Sur le long terme, c'est la différence entre un convertisseur qui se termine et un autre qui meurt à la page 300 avec un fichier corrompu et une boîte de dialogue de mémoire insuffisante (out-of-memory dialog)

Choisir le DPI et la qualité ensemble

Les deux boutons (knobs) ne sont pas indépendants de l'objectif de la sortie, et l'erreur commune est de les tourner tous les deux vers le haut par prudence. Une miniature web (web thumbnail) rendue à 300 DPI et enregistrée à la qualité 95 fait plusieurs centaines de kilo-octets prétendant être une image de 120 pixels ; le navigateur en jette la quasi-totalité lors de la réduction de l'échelle (downscale). Faites correspondre 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
Miniature de liste7260-70
Aperçu à l'écran96-15080-85
Visualisation très détaillée200-30085-95
Original d'impression300-60090-100

La qualité JPEG mérite un mot de prudence en soi. Ce n'est pas un cadran linéaire. Le saut de 70 à 85 offre une réelle amélioration visuelle pour une croissance modeste du fichier ; le saut de 95 à 100 double approximativement le fichier pour une différence que presque personne ne peut voir, car la qualité 100 n'est toujours pas sans perte (lossless), elle cesse simplement d'en rejeter beaucoup. Pour les pages contenant beaucoup de texte, la compression basée sur des blocs du JPEG étale (smears) les bords nets (sharp edges) des glyphes en un léger bourdonnement (faint ringing), c'est pourquoi une qualité inférieure à environ 80 donne l'impression d'un texte numérisé sur ce qui devrait être une sortie nette. Si les pages sont principalement composées de texte et que vous pouvez changer de format, le PNG rend ce texte sans bourdonnement (ringing) ; le JPEG gagne sa place sur le contenu photographique et mixte où sa compression est véritablement plus petite

Des miniatures plus rapides et plus petites

Lorsque la cible est une miniature (thumbnail) plutôt qu'une reproduction fidèle, vous pouvez demander au moteur de rendu de faire moins de travail. Le paramètre Options prend un ensemble d'indicateurs (flags) TRenderOption, et quelques-uns d'entre eux échangent (trade) la fidélité contre la vitesse exactement de la manière qu'un petit aperçu le souhaite. reGrayscale supprime la couleur, ce qui permet à la fois un rendu plus rapide et de produire un bitmap plus petit à encoder. reNoSmoothImage et reNoSmoothPath sautent l'anticrénelage (anti-aliasing) qui est de toute façon invisible à l'échelle de la miniature

function RenderThumbnail(Pdf: TPdf; PageNumber, MaxW, MaxH: Integer): TBitmap;
var
  Scale: Double;
begin
  Pdf.PageNumber := PageNumber;
  // Ajuster la page à l'intérieur de MaxW x MaxH tout en préservant les proportions (aspect ratio).
  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 des miniatures montre également la manière la plus propre de penser au dimensionnement. Au lieu de passer par le DPI, calculez un seul facteur d'échelle (single scale factor) qui s'adapte à la page à l'intérieur d'une boîte englobante (bounding box) et préserve les proportions (aspect ratio), ce qui est ce que fait le Min des deux rapports. Une page portrait et une page paysage se retrouvent toutes deux à l'intérieur de la même boîte sans distorsion, et vous n'avez jamais à raisonner sur quel DPI correspond à "ajuster dans 200 par 280". Une mise en garde avec reGrayscale : il convertit le contenu de l'image matricielle (raster image) en gris, mais les remplissages vectoriels (vector fills) et le texte conservent leurs valeurs de couleur dans le moteur, de sorte qu'une page qui est principalement constituée d'art vectoriel peut revenir moins monochrome que le nom de l'indicateur (flag's name) ne le suggère. Pour un véritable résultat en niveaux de gris complets, la conversion du bitmap rendu avec GrayscalePdfBitmap est le chemin fiable

Traitement par lots d'un document entier

Assembler le tout pour un document complet est une boucle sur PageCount, avec PageNumber déplacé une page à la fois. Les pages sont basées sur 1 : la première page est PageNumber := 1, et la boucle s'exécute jusqu'à PageCount inclus, et non PageCount - 1. L'autre chose que le lot (batch) doit respecter est le contrat de chargement silencieux (silent-load contract). Définir Active := True ne lève jamais d'exception sur un fichier endommagé ou un mauvais mot de passe ; il laisse juste Active à False. Vérifiez-le avant de rendre une seule page, ou le premier RenderPage fonctionne 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('Impossible d'ouvrir ' + PdfPath);

    Digits := Length(IntToStr(Pdf.PageCount));   // remplissage de zéros (zero-pad) pour que les fichiers soient bien triés
    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 de zéros (zero-padding) via Digits est une petite chose qui sauve un après-midi plus tard. Nommez les fichiers page_1.jpg à page_10.jpg et tout outil qui les trie sous forme de chaînes place page_10 juste après page_1, brouillant (scrambling) l'ordre. Le remplissage (Padding) jusqu'à la largeur du numéro de page le plus élevé, de sorte qu'un document de 300 pages donne page_001.jpg, maintient l'ordre lexical et l'ordre des pages identiques partout en aval

Pour les documents suffisamment volumineux pour que la conversion prenne un temps notable, exécutez-la en dehors du thread de l'interface utilisateur ou pompez (pump) les messages entre les pages afin que l'application reste réactive, et donnez à l'utilisateur un moyen de s'arrêter. Si vous affichez de très grandes pages et que vous souhaitez une annulation qui mord à mi-page (bites mid-page) plutôt qu'uniquement entre les pages, le composant PDFium dispose d'un chemin de rendu progressif avec un jeton d'annulation (cancellation token) ; c'est un mécanisme plus lourd (heavier) que ce dont la plupart des exportations par lots ont besoin, mais il est là lorsqu'une seule page à 600 DPI est elle-même suffisamment lente pour bloquer

Un dernier couplage (pairing) qu'il vaut la peine de connaître. La pixellisation d'une page rejette sa couche de texte : le JPEG est constitué de pixels, et les mots qu'il contient ne sont plus ni sélectionnables ni consultables. Lorsque vous avez besoin à la fois d'une image et du texte sous-jacent, effectuez le rendu pour l'image et extrayez le texte séparément, ce que couvre l'article d'accompagnement sur l'extraction de texte à partir de documents PDF avec le composant PDFium. Les surcharges (overloads) de RenderPage et les options de rendu présentées ici font partie du Composant PDFium pour Delphi et C++Builder