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
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
| Voie | Valeurs de formulaire sur papier | Sortie | Coût |
|---|---|---|---|
RenderPage(DC, …) | Non, sous Windows | GDI vectoriel ; SetPdfPrintMode choisit EMF (le défaut) ou un niveau PostScript | File d'attente minimale, le plus rapide |
RenderPage vers un TBitmap, puis StretchDIBits | Oui, y compris les éditions non enregistrées | Raster à la taille en pixels que vous demandez | Largeur × Hauteur × 4 octets par page, plus la copie DIB |
RenderPageBanded | Oui | Raster, une bande à la fois | Une bande en mémoire au lieu de la feuille entière |
Copie aplatie, puis RenderPage(DC, …) | Oui, comme contenu de page cuit | Vectoriel | Enregistrement, 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é
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
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/AppDefaultou une entrée absente. La norme veut qu'il soit honoré même quand aucun dialogue d'impression ne se montrePrintPaperHandling(TPrintPaperHandling) porte/DuplexcommephSimplex,phDuplexFlipShortEdge,phDuplexFlipLongEdgeouphUndefined.SetPdfPrintPaperHandlingDevModel'écrit dansDEVMODE.dmDuplexet poseDM_DUPLEX; pourphUndefinedil renvoie False et laisse l'enregistrement tranquille, si bien que le défaut du pilote survitPrintCopies(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