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
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é
// 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
| Sortie | DPI | Qualité JPEG |
|---|---|---|
| Vignette de liste | 72 | 60-70 |
| Aperçu à l'écran | 96-150 | 80-85 |
| Consultation détaillée | 200-300 | 85-95 |
| Master d'impression | 300-600 | 90-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
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