Article technique

Rendre des pages PDF en Bitmap en Delphi avec HotPDF

HotPDF effectue le rendu d'une page PDF chargée dans un TBitmap Delphi via un seul appel : RenderLoadedPageToBitmap(PageIndex, DPI). La fonction interprète le flux de contenu de la page et renvoie un bitmap RGB 24 bits appartenant à l'appelant à la résolution choisie, ce qui correspond exactement aux besoins d'une bande de miniatures, d'un aperçu avant impression ou d'un pipeline d'exportation de PDF vers image. Cet article présente l'API, puis la partie qui distingue un moteur de rendu exploitable d'un simple gadget : dessiner le texte à partir des programmes de polices intégrés eux-mêmes plutôt qu'à partir de polices système ressemblantes

Pourquoi le rendu d'une page PDF est-il plus difficile que le tracé d'une image ?

Une page PDF n'est pas une image. C'est un programme : un flux d'opérateurs qui construisent des chemins, sélectionnent des polices, définissent des couleurs et placent des glyphes, exécutés selon le modèle graphique défini par l'ISO 32000-1 §8. Rien dans le fichier ne décrit à quoi ressemble chaque pixel. Pour produire un bitmap, vous devez exécuter ce programme — maintenir une matrice de transformation courante, une pile d'état graphique pour q/Q, un chemin de détourage, des espaces colorimétriques de remplissage et de tracé — et rastériser le résultat. C'est pourquoi « afficher simplement la page 3 sous forme d'image » requiert un interpréteur de flux de contenu, et non une simple conversion de format de fichier

Le moteur de rendu de HotPDF, introduit dans la version 2.253.0, est structuré en six unités distinctes calquées sur ce modèle : un noyau de matrice affine pour l'algèbre de transformation PDF [a b c d e f], une pile d'état graphique, un résolveur d'espace colorimétrique (DeviceRGB, DeviceGray, DeviceCMYK, Indexed), un générateur de chemin qui fait le pont entre les opérateurs de chemin PDF et GDI, une couche de métriques de police qui lit les tableaux /Widths pour des avances correctes, et l'interpréteur qui distribue les opérateurs et pilote les cinq autres. Les images XObject passent par la même pile de décodage que celle utilisée par la bibliothèque pour l'extraction, de sorte que chaque filtre d'image que HotPDF peut décoder pour l'extraction — y compris les images JPEG 2000 compressées en JPXDecode — apparaît également dans le rendu de sortie

Rendre une page chargée dans un TBitmap

RenderLoadedPageToBitmap prend un index de page de base zéro et une valeur DPI, où 72 DPI fait correspondre une unité de l'espace utilisateur PDF à un pixel. Il renvoie nil en cas d'échec (index hors limites, ressources manquantes) au lieu de lever une exception, de sorte qu'une visionneuse peut ignorer une mauvaise page et continuer. L'appelant est propriétaire du bitmap renvoyé et doit le libérer

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('report.pdf') > 0 then
    begin
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);  // page 1 at 144 DPI
      if Bmp <> nil then
      try
        Image1.Picture.Assign(Bmp);
      finally
        Bmp.Free;  // caller owns the bitmap
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

L'argument DPI effectue le travail de mise à l'échelle pour chaque scénario courant. Une bande de miniatures s'affiche à 36 ou 48 DPI pour obtenir de petits bitmaps rapides ; un aperçu à l'écran à 96 ou 144 DPI correspond à la densité d'affichage classique ; un processus d'exportation à 300 DPI produit des images de qualité d'impression. La rotation de la page à partir de l'entrée /Rotate et l'inversion de l'origine de la /MediaBox (le PDF place l'origine en bas à gauche, GDI en haut à gauche) sont gérées à l'intérieur de la matrice page-vers-périphérique, de sorte qu'une page US Letter à 72 DPI est renvoyée sous la forme d'exactement 612×792 pixels dans le bon sens

Pourquoi les miniatures de PDF générées affichent-elles de mauvais glyphes ?

Des glyphes erronés ou approximatifs dans le rendu d'un PDF signifient presque toujours que le moteur de rendu remplace une police système au lieu d'utiliser la police intégrée dans le fichier. Le premier moteur de rendu de HotPDF faisait exactement cela : il supprimait le préfixe de sous-ensemble de /BaseFont (transformant ABCDEF+Arial en Arial), demandait à GDI une police système portant ce nom et dessinait le texte avec celle-ci. Pour un document qui utilise Arial ou Times New Roman avec un encodage standard, le résultat semble proche. Mais c'est une approximation, et elle échoue dans des cas bien précis

Les polices de sous-ensemble intégrées représentent le pire des cas. Une police de sous-ensemble peut ne contenir que la quarantaine de glyphes réellement utilisés par un document, avec des codes de caractères attribués dans un ordre propre à ce fichier — le code 1 peut être « T », le code 2 « h », et ainsi de suite. Une police système ne sait rien de cette attribution privée, de sorte que le texte disparaît ou s'affiche sous forme de caractères totalement erronés. Les encodages personnalisés, les polices de symboles, les polices de codes-barres et toute police non installée sur la machine de rendu échouent de la même manière. Un moteur de rendu qui se limite à la substitution de polices système produit des miniatures qui ressemblent à la page — jusqu'à ce que la page utilise les polices qui ont justement rendu l'intégration indispensable au départ

Rendu des glyphes intégrés : dessiner à partir du programme de polices lui-même

HotPDF a comblé cette lacune à travers temps de versions (v2.268.0 à v2.272.0) en analysant les programmes de polices intégrés et en reproduisant leurs contours de glyphes sous forme de chemins vectoriels GDI remplis. Le texte d'une page rendue provient désormais des mêmes données de contour que celles utilisées par une visionneuse conforme, ce qui signifie que les polices de sous-ensemble, les encodages personnalisés et les polices non installées s'affichent avec leurs formes exactes. La prise en charge a été construite par type de police :

Pour les polices Type0/CIDFontType2 contenant un programme TrueType intégré (FontFile2), le moteur de rendu analyse directement les tables glyf and loca : les contours quadratiques sont convertis en courbes de Bézier cubiques comprises par GDI, les points sur la courbe implicites entre deux points consécutifs hors de la courbe sont reconstruits, et les glyphes composites sont reproduits de manière récursive. Les dispositions Identity et les dispositions de flux explicites CIDToGIDMap sont prises en charge, et les avances CID respectent les entrées de largeur /W et /DW, de sorte que le texte Identity-H sur deux octets se déplace correctement

Les programmes CFF (FontFile3, qu'il s'agisse de CIDFontType0C, Type1C ou d'un conteneur OpenType) bénéficient d'un interpréteur charstring Type 2 complet : lignes, courbes, famille flex, masques d'indices (hint masks) et appels de sous-programmes locaux/globaux avec le décalage (bias) de sous-programme approprié. Les programmes CFF à clé CID associent les codes de caractères via le jeu de caractères (charset) de la police, ce qui est important pour les polices de sous-ensemble dont l'ordre des glyphes diffère de l'ordre CID, et la sélection de dictionnaire de polices (font-DICT) par glyphe via FDArray/FDSelect est respectée. Les polices TrueType simples (non-CID) résolvent les codes d'un octet via la table cmap propre de la police intégrée avec une chaîne de sous-tables robuste — les formats Unicode 4 et 12 en premier, puis les sous-tables de symboles avec le miroir d'utilisation privée F000, puis les formats Macintosh hérités — tandis que les polices Type1 simples se résolvent via l'encodage intégré du programme CFF

Deux affinements complètent le tableau. Premièrement, les dictionnaires /Encoding des polices simples sont résolus selon la priorité prescrite par l'ISO 32000-1 §9.6.6 : les tableaux /Differences l'emportent sur l'encodage de base, qui l'emporte lui-même sur la table propre au programme de polices — le chemin dont dépendent les chaînes d'outils dérivées de TeX et PostScript, avec des noms de glyphes se résolvant via la liste de glyphes Adobe (Adobe Glyph List), le jeu de caractères CFF ou la table cmap TrueType. Deuxièmement, les polices Type3, dont les glyphes sont eux-mêmes de petits flux de contenu, sont reproduites par le moteur de rendu avec composition de la matrice de police, de la taille de police et de la matrice de texte ; les largeurs de l'espace des glyphes /Widths sont interprétées via la /FontMatrix comme l'exige l'ISO 32000-1 §9.6.5, et les procédures de glyphes déclarant une boîte de délimitation d1 y sont détourées, de sorte qu'un glyphe de code-barres malformé ne puisse pas déborder de sa cellule. Lorsqu'un code ne peut pas être mappé — un programme endommagé, un caractère non mappé — le moteur de rendu se rabat sur le tracé par police système pour ce glyphe au lieu d'ignorer tout le bloc de texte

Comment rendre rapides les rendus répétés ?

La réponse apportée par HotPDF est un cache de pages les moins récemment utilisées (MRU) : RenderLoadedPageToBitmapCached conserve jusqu'à RenderCacheCapacity pages rendues (par défaut 8) indexées par l'index de page et le DPI, et un succès de cache renvoie une copie fraîche appartenant à l'appelant sans toucher au flux de contenu — généralement des milliers de fois plus rapide que de réinterpréter la page. Ce modèle correspond parfaitement aux besoins des visionneuses : un utilisateur basculant entre deux pages, ou un événement de redimensionnement qui redemande la même page au même DPI, sollicite le cache à chaque fois

// Thumbnail strip: first pass renders, scrolling back hits the cache
for I := 0 to ThumbCount - 1 do
begin
  Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 48);
  if Bmp <> nil then
  try
    ThumbList.AddThumbnail(I, Bmp);
  finally
    Bmp.Free;
  end;
end;

// After editing a loaded page in place:
Pdf.InvalidateRenderedPageCache;  // next render reflects the change

Soyez réaliste quant à la consommation de mémoire avant d'augmenter la capacité. Une page US Letter à 300 DPI représente 2550×3300 pixels, soit environ 25 Mo sous forme de bitmap 24 bits, donc huit pages mises en cache à la résolution d'exportation occupent environ 200 Mo. Au DPI des miniatures, ces mêmes huit entrées coûtent bien moins d'un mégaoctet. Dimensionnez RenderCacheCapacity pour le DPI auquel vous effectuez réellement la mise en cache, et appelez InvalidateRenderedPageCache après toute modification sur place — le cache est indexé uniquement par page et par DPI, et il ne peut pas détecter que le contenu sous-jacent a changé. Le chargement d'un nouveau document l'efface automatiquement

Un second cache fonctionne sous le cache de pages : les images XObject décodées sont conservées dans un espace de stockage au budget d'octets limité par ImageCacheMaxBytes (par défaut 32 Mo) avec éviction des moins récemment utilisées (LRU). Une image de logo ou d'en-tête répétée sur chaque page est décodée une seule fois par chargement de document au lieu de l'être à chaque opérateur Do, ce qui réduit de moitié environ le temps de rendu des pages à images partagées et accélère d'autant l'exportation TIFF multipage. InvalidateRenderedPageCache efface également ce cache

Ce qui s'affiche encore de manière approximative

Le moteur de rendu cible le sous-ensemble courant des documents PDF, et il est important de savoir où se situent les limites. Les espaces colorimétriques CalRGB, Lab et basés sur ICC sont approximés plutôt que gérés en couleur — les espaces colorimétriques du périphérique, les palettes indexées et les tables de correspondance de couleurs de fonctions échantillonnées de type 0 sont pris en charge, mais un fichier destiné à l'impression professionnelle s'appuyant sur des intentions de rendu ICC ne sera pas exact en colorimétrie. Les motifs d'ombrage (shading patterns, sh) et les modes de fusion au-delà du simple canal alpha sont également hors de portée, et la récursion d'objet Form XObject est limitée en profondeur comme protection contre les boucles. Pour les factures, les rapports, les contrats et les formulaires — des pages composées de texte, de tracés et d'images — le rendu est fidèle ; pour une épreuve de conception riche en dégradés et en groupes de transparence, considérez le bitmap comme un aperçu, non comme une épreuve

La conclusion pratique : si votre pipeline génère des documents avec HotPDF ou consomme des PDF d'affaires classiques, RenderLoadedPageToBitmap effectue le cycle complet avec les formes exactes de glyphes intégrées, des avances CID correctes et une géométrie de page exacte. Les approximations résident dans les zones du modèle graphique que les documents professionnels visitent rarement

RenderLoadedPageToBitmap, sa variante mise en cache et le pipeline de rendu de glyphes intégrés décrit ici sont livrés dans le composant HotPDF Component pour Delphi et C++Builder — une bibliothèque VCL native sans dépendance à des DLL externes, couvrant la création, l'édition, l'extraction de texte et le rendu de pages PDF dans un seul package