Article technique

Imprimer les valeurs de formulaires PDF à la bonne échelle

Un utilisateur remplit un formulaire PDF dans un viewer Delphi, appuie sur Imprimer, et la feuille sort avec tous les champs vides et la page Letter étirée pour remplir le A4. Les deux défauts se logent dans la boucle d'impression : PDFium Component ne peint les valeurs de champs que dans un bitmap (ou les cuit dans une copie aplatie), et chaque page demande un seul facteur d'échelle qui préserve les proportions

La couche de formulaire est une seconde passe qui demande un bitmap

Sur Windows, la surcharge contexte de périphérique TPdf.RenderPage(DC, Left, Top, Width, Height, Rotation, Options) remet le HDC de l'imprimante directement à FPDF_RenderPage. Cet appel peint le contenu de la page, plus les annotations de marquage quand reAnnotations est posé. La couche de formulaire interactive n'atteint jamais le DC — elle appartient à l'environnement de remplissage de formulaires de PDFium et est dessinée par FPDF_FFLDraw, dont la seule cible est un FPDF_BITMAP. Pas de variante HDC. Les deux surcharges bitmap de TPdf.RenderPage exécutent FPDF_RenderPageBitmap et FPDF_FFLDraw sur les mêmes pixels dès que le document a été ouvert avec FormFill à True, le défaut. Avec rePrinting dans les options, PDFium Component retire la surbrillance des champs avant la passe formulaire et restaure ensuite la couleur et l'alpha de surbrillance par défaut, si bien que les boîtes teintées autour des champs éditables restent à l'écran. rePrinting se mappe aussi sur le drapeau FPDF_PRINTING du moteur, qui marque la sortie comme impression — le cas que l'ISO 32000-1 §12.5.3 (Table 165) traite avec le bit 3 du drapeau d'annotation : un lecteur conforme laisse hors du papier une annotation dont le bit Print est effacé, quelle que soit sa tête à l'écran

La plateforme décide du reste. Dans le moteur PDFium, FPDF_RenderPage dessine à travers le GDI de Windows dans un contexte de périphérique et n'existe que dans les builds Windows, tandis que FPDF_FFLDraw dessine dans un bitmap et rien d'autre, sur toutes les plateformes. Sur Linux et les autres cibles non Windows, il n'y a pas d'appel GDI à faire, si bien que la surcharge HDC est servie en rendant un bitmap — couche formulaire comprise — et en le dessinant sur le canvas derrière le handle. L'appel contexte de périphérique porte donc les valeurs de champs hors Windows et les laisse de côté sous Windows. Gardez la branche explicite dans le code partagé entre Delphi et Lazarus : quand le document a un environnement de formulaire et que ses valeurs doivent finir sur papier, demandez un bitmap sur chaque plateforme

Impression des champs de formulaires avec PDFium Component en Delphi : TPdf.RenderPage sur le HDC de l'imprimante appelle FPDF_RenderPage sans la couche formulaire, tandis que la surcharge TBitmap ajoute une passe FPDF_FFLDraw avant StretchDIBits
Sur Windows, seule la surcharge bitmap de RenderPage exécute la passe formulaire FPDF_FFLDraw, voilà pourquoi les valeurs de champs tapées atteignent le papier par cette voie et disparaissent par celle du contexte de périphérique

Quelle voie un travail d'impression doit-il prendre ?

Choisissez selon ce que le papier doit porter. Les valeurs de formulaire vivantes pointent vers la voie bitmap, la sortie vectorielle vers la voie HDC, et une copie aplatie vous donne les deux pour le prix d'un enregistrement et d'une relecture. SetPdfPrintMode vaut pour toute la bibliothèque, donc changez-le entre travaux, jamais entre pages

VoieValeurs de formulaire sur papierSortieCoût
RenderPage(DC, …)Non, sous WindowsGDI vectoriel ; SetPdfPrintMode choisit EMF (le défaut) ou un niveau PostScriptFile d'attente minimale, le plus rapide
RenderPage vers un TBitmap, puis StretchDIBitsOui, y compris les éditions non enregistréesRaster à la taille en pixels que vous demandezLargeur × Hauteur × 4 octets par page, plus la copie DIB
RenderPageBandedOuiRaster, une bande à la foisUne bande en mémoire au lieu de la feuille entière
Copie aplatie, puis RenderPage(DC, …)Oui, comme contenu de page cuitVectorielEnregistrement, relecture, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT)

Une feuille A4 entière à 600 dpi fait environ 4960 × 7016 pixels de périphérique, et à 32 bits par pixel cela fait 139 197 440 octets pour une page ; la copier dans un DIB pour StretchDIBits double le pic, une charge lourde pour un processus 32 bits. Pour le bouton Imprimer d'un viewer, la voie bitmap reste le bon défaut : un utilisateur qui tape dans un champ et appuie sur Imprimer s'attend à voir cette valeur sur papier, et le bandage garde la mémoire bornée. Les documents qui quittent l'application, comme des relevés archivés, sont mieux servis par la copie aplatie — les valeurs deviennent du contenu de page ordinaire et l'imprimante reçoit une sortie vectorielle

uses
  System.SysUtils, System.IOUtils, PDFium, FPdfUtils;  // FPdfUtils : FLAT_PRINT

function OpenFlattenedPrintCopy(Source: TPdf; out TempName: string): TPdf;
begin
  TempName := TPath.Combine(TPath.GetTempPath, TPath.GetRandomFileName + '.pdf');
  if not Source.SaveAs(TempName) then
    raise Exception.Create('Could not write the print copy');
  Result := TPdf.Create(nil);
  try
    Result.FileName := TempName;   // FormFill reste True (le défaut)
    Result.Password := Source.Password;  // la copie garde son chiffrement
    Result.Active := True;
    if not Result.Active then      // un chargement raté laisse TPdf inactif
      raise Exception.Create('Could not reopen the print copy');
    Result.GenerateFormAppearances;          // reconstruit /AP depuis les /V courants
    if not Result.FlattenAllPages(FLAT_PRINT) then
      raise Exception.Create('Flattening failed');
  except
    Result.Free;
    TFile.Delete(TempName);
    raise;
  end;
end;

La voie aplatie a une règle d'ordre qui compte plus que le drapeau. FlattenAllPages cuit le flux d'apparence de chaque widget (ISO 32000-1 §12.5.5) dans le contenu de la page, et un flux d'apparence est ce qu'a écrit le dernier producteur ; la valeur du champ dans /V n'y joue aucun rôle. Un formulaire rempli par un outil qui n'a jamais regénéré ses apparences s'aplatit vers le vieux texte ou vers rien, si bien que GenerateFormAppearances tourne d'abord : il reconstruit les apparences des champs texte depuis les valeurs courantes et synchronise les états /AS des cases à cocher et boutons radio, et il demande que FormFill ait été True à l'ouverture du document. Faites tout cela sur une copie, parce que l'aplatissement réécrit les pages du TPdf sur lequel il tourne, et un viewer ouvert veut que ses champs restent éditables. La copie passe par la branche vectorielle de la routine d'impression ci-dessous avec la couche formulaire coupée ; libérez-la et supprimez le fichier temporaire une fois le travail terminé

Copie d'impression aplatie avec PDFium Component en Delphi : SaveAs vers un fichier temporaire, réouverture avec FormFill, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT), puis impression vectorielle via RenderPage sur le contexte de périphérique
Appeler GenerateFormAppearances avant FlattenAllPages fait correspondre les flux d'apparence cuits aux valeurs de champs courantes au lieu de ce que le dernier producteur a laissé dans /AP

Ajuster chaque page à la zone imprimable

Mettez à l'échelle une fois, par le plus petit des deux facteurs, et centrez le résultat. Étirer vers Printer.PageWidth × Printer.PageHeight déforme chaque page dont la forme diffère du papier, une page Letter (612 × 792 points) sur du A4 ou une page paysage sur une feuille portrait. Le côté imprimante est la zone imprimable en pixels de périphérique (HORZRES par VERTRES), les marges du pilote déjà exclues. Côté PDF, TPdf.PageWidth et PageHeight rapportent déjà la page telle qu'affichée : une page A4 portrait stockée avec /Rotate 90 revient en 842 × 595 points. L'ISO 32000-1 §7.7.3.3 (Table 30) dit que la page doit être tournée par /Rotate à l'affichage ou à l'impression, PDFium l'applique à l'intérieur de l'appel de rendu, et l'argument Rotation de RenderPage n'est que le tour supplémentaire qu'ajoute un viewer quand l'utilisateur tourne la vue. Passez-y le PageRotation propre de la page et la page tourne deux fois. Échangez largeur et hauteur pour le calage seulement quand cette rotation supplémentaire vaut 90 ou 270 degrés, la même paire de règles qui gouverne le calage et le zoom des vignettes sous rotation ; la routine ci-dessous étend la boucle de base de l'impression de documents PDF avec PDFium Component avec une branche valeurs de formulaire et un chemin d'annulation qui jette le travail

Calage de page pour l'impression avec PDFium Component en Delphi : une page Letter étirée vers Printer.PageWidth et PageHeight contre une seule mise à échelle préservant les proportions centrée dans la zone imprimable, avec le /Rotate déjà dans TPdf.PageWidth
Mettre à l'échelle par le plus petit des deux facteurs garde chaque page sans distorsion et centrée, et largeur et hauteur s'échangent seulement pour une rotation viewer supplémentaire ro90 ou ro270
uses
  Winapi.Windows, System.SysUtils, Vcl.Graphics, Vcl.Forms, Vcl.Printers, PDFium;

procedure FitRect(PageW, PageH: Double; Swap: Boolean;
  AreaW, AreaH: Integer; out X, Y, W, H: Integer);
var
  T, Scale: Double;
begin
  if Swap then
  begin
    T := PageW; PageW := PageH; PageH := T;
  end;
  if (PageW <= 0) or (PageH <= 0) then
  begin
    X := 0; Y := 0; W := AreaW; H := AreaH;
    Exit;
  end;
  Scale := AreaW / PageW;
  if AreaH / PageH < Scale then
    Scale := AreaH / PageH;
  W := Round(PageW * Scale);
  H := Round(PageH * Scale);
  X := (AreaW - W) div 2;
  Y := (AreaH - H) div 2;
end;

procedure BlitBitmap(DC: HDC; Bitmap: TBitmap; X, Y: Integer);
var
  InfoSize, ImageSize: DWORD;
  Info: PBitmapInfo;
  Bits: Pointer;
begin
  GetDIBSizes(Bitmap.Handle, InfoSize, ImageSize);
  Info := AllocMem(InfoSize);
  try
    Bits := AllocMem(ImageSize);
    try
      GetDIB(Bitmap.Handle, 0, Info^, Bits^);
      StretchDIBits(DC, X, Y, Bitmap.Width, Bitmap.Height, 0, 0,
        Bitmap.Width, Bitmap.Height, Bits, Info^, DIB_RGB_COLORS, SRCCOPY);
    finally
      FreeMem(Bits);
    end;
  finally
    FreeMem(Info);
  end;
end;

function PrintPages(Pdf: TPdf; FromPage, ToPage: Integer; FormLayer: Boolean;
  ExtraRotation: TRotation; var Cancel: Boolean): Boolean;
const
  Opts: TRenderOptions = [reAnnotations, rePrinting];
var
  Page, X, Y, W, H: Integer;
  Bitmap: TBitmap;
begin
  Result := False;
  Printer.BeginDoc;
  try
    for Page := FromPage to ToPage do
    begin
      if Page > FromPage then
        Printer.NewPage;
      Pdf.PageNumber := Page;
      FitRect(Pdf.PageWidth, Pdf.PageHeight, ExtraRotation in [ro90, ro270],
        Printer.PageWidth, Printer.PageHeight, X, Y, W, H);
      if FormLayer then
      begin
        Bitmap := Pdf.RenderPage(0, 0, W, H, ExtraRotation, Opts);
        if Bitmap <> nil then          // nil pour une taille de page dégénérée
        try
          BlitBitmap(Printer.Canvas.Handle, Bitmap, X, Y);
        finally
          Bitmap.Free;
        end;
      end
      else
        Pdf.RenderPage(Printer.Canvas.Handle, X, Y, W, H, ExtraRotation, Opts);
      Application.ProcessMessages;     // laisse un bouton Annuler poser le drapeau
      if Cancel then
        Break;
    end;
    Result := not Cancel;
  finally
    if Result then
      Printer.EndDoc                   // livre le travail au spooler
    else
      Printer.Abort;                   // annulation ou exception : rien ne sort
  end;
end;

Un viewer appelle ceci avec Pdf.FormFill comme FormLayer ; la copie aplatie passe False. EndDoc est la validation, pas une étape de nettoyage. Après BeginDoc, Printer.EndDoc remet tout ce qui a été rendu au spooler comme travail valide, si bien qu'un EndDoc inconditionnel dans finally transforme une impression annulée en demi-impression. La routine décide dans finally depuis le drapeau de succès à la place : EndDoc après la dernière page, Abort après une annulation ou une exception. La sortie de boucle demande un soin similaire, parce que Delphi et Free Pascal évaluent tous deux une seule fois la valeur de fin d'une boucle for, à l'entrée. Mettre ToPage ou un compte de copies à zéro depuis un gestionnaire Annuler n'arrête pas une boucle qui tourne déjà ; testez le drapeau et Break, et avec des boucles de copies et de collation imbriquées, sortez de chaque niveau. Et puisque Application.ProcessMessages laisse aussi les raccourcis de menu partir, désactivez Ouvrir, Enregistrer et Imprimer pendant que le travail tourne

Garder la voie raster dans la mémoire

TPdf.RenderPageBanded rend une page en bandes horizontales d'une hauteur que vous choisissez et remet chaque bande à un callback. Chaque bande est une tranche de la page pleine taille — rien n'est remis à l'échelle — et elle porte les mêmes valeurs de champs qu'un bitmap pleine page. Avec des bandes de 512 lignes, une page de 4960 pixels de large demande environ 10 Mo par bande au lieu de 139 Mo pour la feuille. À peu près un quatorzième de la mémoire. TPdfBandCallback est un pointeur de méthode qui reçoit l'indice de bande, le décalage haut de la bande en pixels de page et le bitmap, et renvoie True pour continuer ; RenderPageBanded renvoie le compte de bandes, 0 en échec

type
  TBandPrinter = class
  private
    FDC: HDC;
    FLeft, FTop: Integer;
  public
    constructor Create(DC: HDC; Left, Top: Integer);
    function PrintBand(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean;
  end;

constructor TBandPrinter.Create(DC: HDC; Left, Top: Integer);
begin
  inherited Create;
  FDC := DC;
  FLeft := Left;
  FTop := Top;
end;

function TBandPrinter.PrintBand(BandIndex, BandTopY: Integer;
  Bitmap: TBitmap): Boolean;
begin
  BlitBitmap(FDC, Bitmap, FLeft, FTop + BandTopY);
  Result := True;                      // False stoppe les bandes restantes
end;

// appelé depuis la boucle de pages à la place de la branche bitmap pleine page
procedure PrintPageBanded(Pdf: TPdf; Page, X, Y, W, H: Integer;
  ExtraRotation: TRotation);
var
  Bands: TBandPrinter;
begin
  Bands := TBandPrinter.Create(Printer.Canvas.Handle, X, Y);
  try
    if Pdf.RenderPageBanded(W, H, 512, Bands.PrintBand, ExtraRotation,
      [reAnnotations, rePrinting]) = 0 then
      raise Exception.CreateFmt('Page %d could not be rendered', [Page]);
  finally
    Bands.Free;
  end;
end;

Que devrait faire l'hôte avec ViewerPreferences et les bits de permission ?

Lisez-les et agissez dessus dans votre propre code ; PDFium les rapporte et rend la page quoi qu'il arrive. Un producteur peut stocker des indices d'impression dans le dictionnaire /ViewerPreferences du catalogue (ISO 32000-1 §12.2, Table 150), et TPdf expose ceux qui donnent forme à un dialogue d'impression :

  • PrintScaling (Boolean) est False quand le fichier dit /PrintScaling /None, ce qui veut dire imprimer à la taille réelle, et True pour /AppDefault ou une entrée absente. La norme veut qu'il soit honoré même quand aucun dialogue d'impression ne se montre
  • PrintPaperHandling (TPrintPaperHandling) porte /Duplex comme phSimplex, phDuplexFlipShortEdge, phDuplexFlipLongEdge ou phUndefined. SetPdfPrintPaperHandlingDevMode l'écrit dans DEVMODE.dmDuplex et pose DM_DUPLEX ; pour phUndefined il renvoie False et laisse l'enregistrement tranquille, si bien que le défaut du pilote survit
  • PrintCopies (Integer) est /NumCopies, le compte de copies à mettre dans le dialogue

Les bits de permission suivent la même règle. Pour le gestionnaire de sécurité standard, l'ISO 32000-1 §7.6.3.2 (Table 22) assigne le bit 3 de /P à l'impression et, pour les gestionnaires révision 3 et après, le bit 12 à l'impression pleine fidélité ; avec le bit 3 posé et le bit 12 effacé, l'impression est limitée à une représentation bas niveau de l'apparence, possiblement dégradée. TPdf.GrantedPermissions décode le masque en permPrint et permPrintHighQuality, et HasPermission teste une valeur ; le moteur rend ce que vous demandez, si bien que la contrainte est le travail de l'hôte, en général un bouton Imprimer désactivé. Quand seul permPrint est accordé, la lecture qui colle à la norme est la voie bitmap à une résolution modeste plutôt que la sortie vectorielle ou PostScript qu'une concession complète permet. Les bits décrivent le document tel qu'ouvert : un fichier déverrouillé avec son mot de passe owner rapporte un accès complet, et UserHasPermission répond pour les bits niveau user seuls

uses
  Winapi.Windows, System.SysUtils, Vcl.Dialogs, Vcl.Printers, PDFium;

procedure PreparePrintJob(Pdf: TPdf; Dialog: TPrintDialog;
  out ActualSize, HighQuality: Boolean);
var
  Device, Driver, Port: array[0..MAX_PATH] of Char;
  DevModeHandle: THandle;
  DevMode: PDeviceMode;
begin
  if not Pdf.HasPermission(permPrint) then
    raise Exception.Create('This document does not permit printing');
  HighQuality := Pdf.HasPermission(permPrintHighQuality);
  ActualSize := not Pdf.PrintScaling;            // /PrintScaling /None

  Printer.GetPrinter(Device, Driver, Port, DevModeHandle);
  if DevModeHandle <> 0 then
  begin
    DevMode := GlobalLock(DevModeHandle);
    try
      SetPdfPrintPaperHandlingDevMode(DevMode^, Pdf.PrintPaperHandling);
    finally
      GlobalUnlock(DevModeHandle);
    end;
    Printer.SetPrinter(Device, Driver, Port, DevModeHandle);  // le dialogue part de là
  end;

  Dialog.MinPage := 1;
  Dialog.MaxPage := Pdf.PageCount;
  Dialog.Options := Dialog.Options + [poPageNums];
  if Pdf.PrintCopies > 1 then
    Dialog.Copies := Pdf.PrintCopies;
end;

// utilisé à la place de FitRect quand ActualSize vaut True
procedure ActualSizeRect(Pdf: TPdf; out X, Y, W, H: Integer);
begin
  // 72 points par pouce côté PDF, pixels de périphérique par pouce côté imprimante
  W := Round(Pdf.PageWidth / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSX));
  H := Round(Pdf.PageHeight / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSY));
  X := (Printer.PageWidth - W) div 2;  // négatif quand la page est plus grande
  Y := (Printer.PageHeight - H) div 2; // que la zone imprimable : les bords rognent
end;

Exécuter les actions will-print et did-print à la vraie frontière d'impression

Les scripts d'impression au niveau document tournent quand l'hôte les exécute. Le dictionnaire d'actions supplémentaires du catalogue (ISO 32000-1 §12.6.3, Table 197) définit WP, une action JavaScript exécutée avant impression, et DP, exécutée après. PDFium Component expose les deux via ExecuteDocumentLifecycleAction(dlaWillPrint, ErrorMessage) et dlaDidPrint, et les garde hors des appels de rendu : vignettes, instantanés et conversions rendent aussi, et aucun ne doit ressembler à une impression d'utilisateur pour un script de document. L'ordre est à votre charge de bien le faire. Un script will-print qui tamponne une date ou remplit un champ "imprimé par" change des valeurs de formulaire, donc exécutez-le avant que la première page soit rendue et, sur la voie aplatie, avant que la copie soit enregistrée ; déclenchez did-print seulement après que EndDoc a livré le travail. L'appel renvoie False avec un message quand l'environnement de formulaire manque ou que LifecycleActionPolicy le refuse ; pour des fichiers non fiables, lapDisabled éteint les actions de document. Le tableau plus large du scripting se trouve dans l'exécution de JavaScript AcroForm avec les événements hôte

Sur Windows, un travail qui doit porter des valeurs de formulaire appartient à la branche bitmap : RenderPage dans un TBitmap aux résolutions ordinaires, RenderPageBanded à 600 dpi et au-dessus. Gardez la surcharge contexte de périphérique du PDFium Component pour les copies aplaties et les documents sans champs, où sa sortie vectorielle est le meilleur résultat