Article technique

Rendu PDF monochrome 1 bit dans Delphi

Une passerelle fax ne veut pas de votre rendu de page en 24 bits. Pas plus que le pipeline d’archivage qui stocke un million de factures scannées, ni que le front-end OCR qui réduit tout au noir et blanc avant même de chercher un caractère. Les trois veulent la même chose : un bitmap 1 bit propre, un bit par pixel, où chaque point n’est que de l’encre ou du papier. Si vous leur donnez un BMP couleur pleine, ils jetteront de toute façon 23 bits par pixel, souvent avec un tramage moins bon que celui que vous auriez pu faire vous-même. La vraie question est de savoir où cette conversion doit se produire, et la réponse dans PDF Library for Delphi dit quelque chose d’utile sur la façon d’étendre un moteur de rendu que vous préférez ne pas réécrire

PDF Library for Delphi est une bibliothèque PDF native en Object Pascal pour Delphi et C++Builder. Son moteur de rendu rasterise une page en bitmap et peut produire BMP, PNG, JPEG, WMF et quelques autres formats. Ce qu’il ne faisait pas jusqu’à récemment, c’était renvoyer un vrai bitmap monochrome ou rendre seulement une partie d’une page. Les deux ont été ajoutés en v3.83.0, et les deux ont été construits comme de minces couches de commodité au-dessus du moteur existant, plutôt que comme des modifications du rasterizer lui-même. Cette contrainte est toute l’histoire

Pourquoi convertir après le rendu, et non dans le moteur

La manière évidente de produire une image 1 bit consiste à demander au rasterizer de dessiner en 1 bit. C’est aussi la façon de tout casser par ailleurs. Le bitmap interne du moteur est créé avec un PixelFormat := pf24bit dans le PDFlibRenderer constructeur PDFlibRenderer, et cette surface 24 bits est partagée par tous les chemins de rendu : export PNG, aperçu via contexte de périphérique, sortie JPEG, tout. Si vous le basculez vers pf1bit à la source, vous n’ajoutez pas une fonction monochrome, vous dégradez la fidélité couleur pour tous les appelants de la bibliothèque et vous vous inscrivez pour déboguer une douzaine de régressions en aval

Ainsi, RenderPageToMonochromeFile prend le chemin inverse. Il rend la page normalement, dans un BMP temporaire 24 bits, puis seulement ensuite la réduit à 1 bit comme étape de post-traitement. Le moteur reste intact. Le comportement monochrome vit entièrement dans la méthode de commodité, ce qui signifie qu’il ne peut affecter personne qui ne l’appelle pas. C’est le genre de compromis qu’il vaut la peine de nommer explicitement : un post-traitement paie une allocation de bitmap supplémentaire et un fichier temporaire, et en échange il garde hors du périmètre un cœur qui porte tout. Pour une fonctionnalité destinée aux cas limites fax et archivage, c’est le bon côté du bilan

Pipeline PDF Library for Delphi montrant une page PDF rendue en BMP 24 bits temporaire, réduite par un blit GDI HALFTONE en bitmap monochrome 1 bit, puis consommée par des workflows fax, archivage et OCR
Les nouvelles méthodes rendent d'abord une page en couleurs complètes, puis convertissent vers le bas le raster achevé en dehors du moteur de rendu. Les passerelles fax, les magasins d'archivage et les fronts OCR reçoivent un véritable bitmap pf1bit
var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.LoadFromFile('invoice.pdf');
    // 200 DPI est la résolution classique du fax Groupe 4 ; l'index de page est basé sur 1
    Pdf.RenderPageToMonochromeFile(200, 1, 'invoice-page1.bmp');
  finally
    Pdf.Free;
  end;
end;

Comment la conversion en 1 bit se produit réellement

La conversion s’appuie sur GDI plutôt que sur une boucle de seuil écrite à la main, et ce choix compte pour la qualité de sortie. À l’intérieur de la méthode, le bitmap temporaire 24 bits est chargé dans un TBitmap, un second TBitmap est créé avec PixelFormat := pf1bit aux mêmes dimensions, et les pixels passent d’un coup de blit :

PDF Library for Delphi : détail de la réduction GDI comparant un stretch blit HALFTONE qui trame les gris en motifs de points au seuil par défaut BLACKONWHITE crénelé
Dans RenderPageToMonochromeFile, un seul StretchBlt déplace chaque pixel sur une surface pf1bit de taille identique. Avec HALFTONE actif, les gris deviennent des motifs tramés de points d'encre au lieu des formes anguleuses produites par le seuil par défaut
// à l'intérieur de RenderPageToMonochromeFile, après le chargement du ColorBmp 24 bits
MonoBmp.PixelFormat := pf1bit;
MonoBmp.Width  := ColorBmp.Width;
MonoBmp.Height := ColorBmp.Height;
// HALFTONE indique à GDI de tramer la source 24 bits en 1 bit
SetStretchBltMode(MonoBmp.Canvas.Handle, HALFTONE);
StretchBlt(MonoBmp.Canvas.Handle, 0, 0, MonoBmp.Width, MonoBmp.Height,
  ColorBmp.Canvas.Handle, 0, 0, ColorBmp.Width, ColorBmp.Height, SRCCOPY);
MonoBmp.SaveToFile('out.bmp');

L’astuce, c’est SetStretchBltMode avec HALFTONE. Même si la source et la destination ont la même taille, donc sans changement d’échelle, le mode d’étirement continue de gouverner la manière dont GDI mappe les couleurs dans la palette 1 bit. HALFTONE lui fait appliquer un tramage demi-teinte, transformant les zones grises et les contours de texte antialiasés en motifs de points noirs et blancs plutôt qu’en une coupure nette vers la couleur la plus proche. Supprimez l’appel du mode, ou utilisez le défaut BLACKONWHITE, et le contenu en niveaux de gris se posterise en formes massives à seuil. Pour les sorties destinées aux documents scannés et au prétraitement OCR, le résultat tramé est presque toujours ce qu’il faut

Un détail est non négociable et facile à rater : le rendu temporaire doit être un BMP. RenderPageToMonochromeFile appelle le moteur général avec un code d’options à 0, qui correspond au BMP. L’argument d’options de RenderPageToFile est une petite énumération entière, et les valeurs ne sont pas interchangeables pour cet usage : 0 est BMP, 1 JPEG, 2 WMF, 3 EMF, 5 PNG, et ainsi de suite. Le convertisseur effectue ensuite TBitmap.LoadFromStream sur le fichier temporaire. Donnez-lui un WMF, en passant 2, et ce chargement lève "Bitmap image is not valid", parce qu’un Windows Metafile est un flux d’enregistrements vectoriels, pas un DIB. La conversion monochrome est une opération raster de bout en bout, donc l’intermédiaire doit lui aussi être un format raster

Rendre seulement une sous-région d’une page

La seconde méthode, RenderPageRegionToFile, rend seulement un rectangle de la page au lieu de la page entière. Les cas d’usage sont familiers dès qu’on a déjà construit une visionneuse documentaire : découper un bloc de signature dans un contrat, générer une tuile pour une carte zoomée d’un grand dessin, ou extraire une zone tamponnée pour une miniature sans payer le raster complet à haute résolution. La signature est simple :

PDF Library for Delphi : rendu de clip de région en points PDF : un rectangle 72,72,180,72 sur une page PDF rendue en pleine résolution devient un bitmap de 375 par 150 pixels à 150 DPI parce que le clip rogne au lieu de redimensionner
RenderPageRegionToFile découpe une fenêtre mesurée en points PDF dans un rendu pleine résolution. Le bitmap de sortie est dimensionné à partir de la largeur et de la hauteur multipliées par le DPI divisé par 72, jamais d'une page entière réduite
// Clip est "Left,Top,Width,Height" en points PDF (72 pt = 1 pouce)
// Ici : une boîte de 2,5 x 1 pouce, à un pouce du coin supérieur gauche de la page
Pdf.RenderPageRegionToFile(150, 1, '72,72,180,72', 'sig-block.bmp');

La chaîne de découpe est composée de quatre doubles séparés par des virgules en points PDF, analysés manuellement dans la méthode pour contourner les particularités locales et celles de DelimitedText. À partir de la largeur et de la hauteur, la méthode calcule la taille du bitmap de sortie comme Round(Width * DPI / 72) par Round(Height * DPI / 72), alloue un bitmap mémoire pf24bit exactement à cette taille, puis le rend dans son contexte de périphérique via RenderPageToDCClip. Le fichier résultat ne contient que le rectangle découpé, dimensionné à la région plutôt qu’à la page entière

Le paramètre de découpe qui ne faisait rien

C’est ici que le travail était plus net qu’il n’y paraît. RenderPageToDCClip portait depuis longtemps un paramètre Clip, et c’était un mensonge. L’appel acceptait l’argument, le transmettait à TPDFPageTree.RenderPageToDC, et cette implémentation l’ignorait complètement, sans jamais le remettre au moteur. Vous pouviez passer n’importe quel rectangle et obtenir la page entière. Quiconque avait branché RenderPageToDCClip en attendant un recadrage obtenait un rendu plein format et, selon sa mise en page, pouvait ne pas s’en être aperçu

La v3.83.0 a raccordé le fil. RenderPageToDC analyse désormais le même rectangle en points "Left,Top,Width,Height" et l’applique comme une véritable région de découpe GDI sur le contexte de périphérique cible avant que le moteur ne dessine. La conversion des points vers les pixels du périphérique suit le facteur d’échelle habituel DPI / 72, appliqué aux quatre bords. La séquence autour du rendu est la danse classique save/clip/restore :

// à l'intérieur de TPDFPageTree.RenderPageToDC, quand Clip est non vide
ScaleFactor := DPI / 72;
SaveDC(TargetDC);
IntersectClipRect(TargetDC,
  Round(ClipLeft * ScaleFactor),
  Round(ClipTop * ScaleFactor),
  Round((ClipLeft + ClipWidth) * ScaleFactor),
  Round((ClipTop + ClipHeight) * ScaleFactor));
// ... le moteur de rendu dessine la page ici ...
// dans le bloc finally :
RestoreDC(TargetDC, -1);

La paire SaveDC / RestoreDC(-1) est ce qui permet de l’appeler plusieurs fois sans risque : la région de découpe est poussée sur la pile d’état du DC, la page est dessinée, puis la découpe d’origine est restaurée quel que soit le mode de sortie du rendu. RestoreDC(TargetDC, -1) rétablit l’état sauvegardé le plus récent, ce qui est l’idiome standard pour un save/restore équilibré. Oubliez la restauration et un appelant qui réutilise le même DC pour un rendu pleine page suivant le verra mystérieusement découpé selon la dernière région. La correction du paramètre mort a également corrigé RenderPageRegionToFile gratuitement, puisque cette nouvelle méthode emprunte exactement ce chemin

Un point de comportement à garder en tête : le découpage recadre, il ne met pas à l’échelle. La page est toujours rasterisée à la résolution demandée, dans sa position normale, et la région découpée élimine simplement tout ce qui est hors du rectangle. Vous ne zoomez pas la région pour remplir la sortie ; vous coupez une fenêtre dans le rendu pleine résolution. Si vous voulez agrandir une région, augmentez le DPI. Les coordonnées du rectangle sont interprétées dans l’espace périphérique après mise à l’échelle points vers pixels, mesurées depuis le coin supérieur gauche de la surface rendue, donc planifiez Left et Top à partir du haut de la page vers le bas. Pour un tour plus approfondi de la façon dont PDF Library for Delphi pilote un contexte de périphérique pour la sortie à l’écran, l’article compagnon sur l’aperçu avant impression et la sortie via contexte de périphérique détaille la même plomberie côté affichage

La limite honnête : BMP 1 bit, pas TIFF G4

Il serait facile de survendre cela comme une « sortie prête pour le fax », alors posons la limite clairement. RenderPageToMonochromeFile produit un BMP pf1bit. Il ne produit pas un TIFF CCITT Group 4, qui est le format qu’un vrai flux fax ou une archive TIFF attend généralement. La raison est concrète plutôt qu’un oubli : l’unité CCITT de PDF Library for Delphi décode actuellement les flux G4 mais ne possède pas d’encodeur G4. Sans encodeur, il n’y a nulle part où écrire des runs monochromes compressés, donc le chemin monochrome s’arrête à un DIB 1 bit non compressé

En pratique, cela reste utile. Un BMP 1 bit est le bon format pixel, tramé et prêt, et la plupart des chaînes fax, archivage ou OCR l’ingèrent volontiers ou le convertissent elles-mêmes en G4 en une étape aval. Mais si votre exigence est littéralement un TIFF Group 4 directement sorti de la bibliothèque, ce n’est pas encore cela, et vous devez prévoir votre propre étape de compression. Savoir où une fonctionnalité s’arrête vaut autant que savoir ce qu’elle fait

Les deux méthodes sont volontairement petites, et c’est la leçon de conception à retenir de cette page : une API de commodité au-dessus d’un moteur de rendu peut ajouter une vraie capacité, sortie monochrome, recadrage de région, sans toucher au rasterizer et déstabiliser tous les autres appelants. Quand vous devez choisir entre plusieurs moteurs pour le raster sous-jacent, la vue d’ensemble sur le rendu PDF multi-moteurs dans Delphi détaille les compromis. Pour voir toute la surface de rendu et le reste de l’API, la page produit PDF Library for Delphi Delphi PDF Library donne le tableau complet