Article technique

Impression de documents PDF avec le composant PDFium dans Delphi

Les coordonnées PDF sont en points, les coordonnées de l'imprimante sont en unités de périphérique (device units), et les deux n'ont rien à voir l'une avec l'autre tant que vous ne les avez pas converties délibérément. Cette inadéquation est à l'origine de la plupart des mauvaises sorties d'impression dans les applications Delphi : le code envoie le bon fichier mais la page sort rognée, étirée ou vierge. Le composant PDFium gère le côté rendu proprement ; la plomberie de l'imprimante est la VCL standard. Les deux s'emboîtent avec une quantité modeste de code une fois que vous comprenez ce que chaque côté attend

Comment fonctionne le pipeline de rendu puis impression

Le composant PDFium ne parle pas directement aux imprimantes. Le modèle (pattern) est le suivant : rendez une page sur un TBitmap à la résolution souhaitée, puis transférez ce bitmap sur le canevas de l'imprimante avec StretchDIBits. TPdf.RenderPage renvoie un bitmap appartenant à l'appelant (caller-owned), vous contrôlez donc les dimensions en pixels. Passez [rePrinting] dans l'ensemble d'options et PDFium bascule son chemin de rendu vers un chemin qui omet les effets réservés à l'écran, tels que le lissage des sous-pixels LCD (LCD subpixel hinting), et gère correctement la MediaBox de la page pour la sortie d'impression. Laissez rePrinting de côté et ce que vous envoyez à l'imprimante est un rendu d'écran, ce qui semble correct sur un moniteur mais a tendance à produire une sortie plus floue (softer) sur les imprimantes à haute résolution (high-DPI) car les décisions de lissage (hinting) prises pour les écrans 96 DPI ne conviennent pas à l'impression 300 ou 600 DPI

TPdf.Active est la seule porte à vérifier avant de toucher à toute propriété de page. Le composant avale les erreurs de chargement silencieusement : définir Active := True sur un fichier endommagé ou protégé par mot de passe ne lève pas d'exception ; il laisse simplement Active à False. Vérifiez-le toujours après l'affectation. La lecture de PageCount ou PageWidth sur un document inactif renvoie zéro, ce qui produit des no-ops silencieux qui sont très difficiles à diagnostiquer une fois qu'ils atteignent le spooler

Une boucle d'impression minimale

Le cas d'utilisation le plus simple charge un fichier, ouvre un travail d'impression, itère les pages et se ferme. Le seul détail délicat est que Printer.NewPage ne doit pas être appelé avant la première page, d'où l'indicateur FirstPage. Le transfert StretchDIBits passe par GetDIBSizes et GetDIB pour extraire les bits indépendants du périphérique à partir du handle de bitmap, puis les peint sur le canevas de l'imprimante à la taille de page entière :

procedure PrintPdfFile(const FileName: string);
var
  Pdf: TPdf;
  I: Integer;
  Bitmap: TBitmap;
  InfoHeaderSize, ImageSize: DWORD;
  InfoHeader: PBitmapInfo;
  Image: Pointer;
  FirstPage: Boolean;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;  // le chargement a échoué silencieusement ; abandonner
  
    Printer.Title := Pdf.Title;
    Printer.BeginDoc;
    try
      FirstPage := True;
      for I := 1 to Pdf.PageCount do
      begin
        if FirstPage then
          FirstPage := False
        else
          Printer.NewPage;
  
        Pdf.PageNumber := I;
  
        // Rendu à la résolution de l'imprimante ; rePrinting ajuste le chemin de rendu
        Bitmap := Pdf.RenderPage(
          0, 0,
          Printer.PageWidth,
          Printer.PageHeight,
          ro0,
          [rePrinting]
        );
        try
          GetDIBSizes(Bitmap.Handle, InfoHeaderSize, ImageSize);
          InfoHeader := AllocMem(InfoHeaderSize);
          try
            Image := AllocMem(ImageSize);
            try
              GetDIB(Bitmap.Handle, 0, InfoHeader^, Image^);
              StretchDIBits(
                Printer.Canvas.Handle,
                0, 0, Printer.PageWidth, Printer.PageHeight,
                0, 0, Bitmap.Width, Bitmap.Height,
                Image, InfoHeader^, DIB_RGB_COLORS, SRCCOPY
              );
            finally
              FreeMem(Image);
            end;
          finally
            FreeMem(InfoHeader);
          end;
        finally
          Bitmap.Free;
        end;
      end;
    finally
      Printer.EndDoc;
    end;
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Le passage de Printer.PageWidth et Printer.PageHeight comme dimensions du bitmap signifie que vous effectuez le rendu à la taille de pixel native de l'imprimante, qui tient déjà compte du DPI du périphérique. L'appel StretchDIBits mappe ensuite ces pixels 1:1 sur la page. Cela vous donne la meilleure fidélité réalisable sans aucune arithmétique DPI explicite, mais cela ne fonctionne que lorsque la page PDF et le papier physique ont la même taille. Lorsqu'ils diffèrent, vous avez besoin d'une mise à l'échelle (scaling) explicite

Mise à l'échelle lorsque les tailles de page et de papier diffèrent

Une page PDF en format A4 portrait ne s'adapte pas automatiquement à une imprimante US Letter, et une page en mode paysage introduite dans une imprimante orientée portrait sera tronquée (clip). L'approche standard consiste à calculer un facteur d'échelle uniforme à partir du rapport entre les pixels de l'imprimante et les points PDF, puis à l'appliquer aux deux dimensions afin que les proportions (aspect ratio) soient préservées. Pdf.PageWidth et Pdf.PageHeight exposent les dimensions de la page courante en points, où un point équivaut à 1/72 de pouce. Multiplier par un DPI cible et diviser par 72 convertit en pixels à cette résolution. Prenez Min des ratios X et Y pour obtenir la plus grande échelle qui tient toujours dans la zone imprimable :

// Adapter la page PDF à la zone imprimable, en préservant les proportions
var
  ScaleX, ScaleY, Scale: Double;
  DestWidth, DestHeight: Integer;
  Dpi: Integer;
begin
  Dpi := 300;  // résolution de rendu cible
  Pdf.PageNumber := PageIndex;
  
  ScaleX := Printer.PageWidth  / (Pdf.PageWidth  * Dpi / 72);
  ScaleY := Printer.PageHeight / (Pdf.PageHeight * Dpi / 72);
  Scale  := Min(ScaleX, ScaleY);
  
  // Bloquer (clamp) à 1.0 pour réduire afin d'ajuster uniquement (pas d'agrandissement)
  if Scale > 1.0 then Scale := 1.0;
  
  DestWidth  := Round(Pdf.PageWidth  * Dpi / 72 * Scale);
  DestHeight := Round(Pdf.PageHeight * Dpi / 72 * Scale);
  
  Bitmap := Pdf.RenderPage(0, 0, DestWidth, DestHeight, ro0,
    [rePrinting, reAnnotations]);
  // ... transférer avec StretchDIBits comme ci-dessus
end;

Le rendu à Dpi = 300 convient à la plupart des imprimantes de bureau. À 600 DPI, le bitmap d'une seule page A4 s'élève à environ 34 mégapixels, soit environ 100 Mo sous forme de bitmap 32 bits ; le gain de qualité pour les documents texte ordinaires est minime et le coût en mémoire par page est significatif. Réservez le 600 DPI pour les ateliers d'impression ou les dessins techniques riches en vecteurs où cela compte vraiment

L'indicateur reAnnotations dans le deuxième bloc de code est indépendant de rePrinting. Incluez-le lorsque l'utilisateur s'attend à ce que les tampons, les surbrillances et les zones de commentaire apparaissent sur le papier. Omettez-le pour une sortie contenant uniquement le contenu. Les deux indicateurs peuvent être combinés librement

Rotation de la page

PDFium stocke la rotation de la page dans le PDF en tant qu'entrée /Rotate, accessible via Pdf.PageRotation, qui renvoie une valeur TRotation (ro0, ro90, ro180, ro270). Le système de coordonnées de l'imprimante inverse les rotations de 90 et 270 degrés par rapport à l'écran. Si vous passez la valeur brute PageRotation directement à RenderPage sans aucun ajustement, les pages en mode paysage intégrées dans un document portrait s'imprimeront à l'envers sur la plupart des pilotes d'imprimante Windows. Le correctif est un simple échange avant l'appel de rendu : mappez ro90 vers ro270 et ro270 vers ro90, en laissant ro0 et ro180 inchangés

Vérifiez ce comportement sur votre imprimante cible spécifique avant de livrer. Le comportement des pilotes autour de la rotation n'est pas uniforme d'un fournisseur à l'autre, et certains pilotes appliquent leur propre correction de rotation au niveau GDI. Si vous voyez une double rotation, supprimez l'échange ; si vous ne voyez aucune correction du tout, ajoutez-la. Un document à orientation mixte avec des pages portrait et paysage alternées est le moyen le plus rapide de détecter l'un ou l'autre des modes de défaillance pendant les tests

Gestion de la mémoire tout au long d'un travail d'impression long

Chaque appel à RenderPage alloue un nouveau TBitmap que l'appelant possède et doit libérer. Dans la boucle ci-dessus, le bloc try/finally Bitmap.Free gère cela correctement pour une page à la fois. N'accumulez pas les bitmaps d'une page à l'autre : un rendu 300 DPI d'un document de 200 pages consommerait des gigaoctets avant que la première page n'atteigne le spooler. Libérez chaque bitmap avant de passer à la page suivante

La paire AllocMem / FreeMem à l'intérieur du bloc de transfert suit la même règle. GetDIBSizes vous indique la quantité de mémoire dont l'en-tête DIB et les données de pixels ont besoin ; vous allouez, remplissez, peignez et libérez le tout dans le cadre d'une page. Laisser fuir l'un ou l'autre bloc amènera le travail d'impression à épuiser le tas (heap) du processus sur des documents de plus de quelques dizaines de pages

Si vous devez exécuter des travaux d'impression sur un thread d'arrière-plan, conservez TPdf et tous les appels d'imprimante VCL sur le même thread. TPdf lui-même n'est pas thread-safe (sécurisé pour les threads) entre les instances partageant l'état global de la DLL PDFium ; le modèle le plus sûr est un TPdf par thread, chacun chargeant sa propre copie du fichier

L'API de rendu et de document présentée ici fait partie du Composant PDFium pour Delphi et C++Builder