Article technique

Aperçu avant impression et contexte de périphérique Delphi

Rendre une page PDF sur un contexte de périphérique Windows pour un aperçu avant impression met trois systèmes de coordonnées dans la même ligne de code, et ils sont rarement d'accord. La page PDF se mesure en points avec l'origine en bas à gauche. Le DC écran se mesure en pixels avec l'origine en haut à gauche et un facteur de zoom que vous choisissez. Le DC imprimante, celui que l'aperçu est censé prédire, mesure des pixels à la résolution du périphérique mais place son origine au coin de la zone imprimable, pas au coin de la feuille. Trompez-vous sur l'un des trois et l'aperçu a l'air correct tandis que la page imprimée sort décalée, redimensionnée ou rognée le long d'un bord. Le symptôme habituel est un formulaire encadré qui s'affiche centré à l'aperçu et s'imprime avec les filets du haut et de gauche tranchés, parce que l'imprimante laser ne peut pas déposer d'encre dans les quelques millimètres extérieurs et que personne ne l'a dit à l'aperçu. losLab PDF Library (PDF Library for Delphi) couvre tout le chemin avec des appels de rendu sur contexte de périphérique, une couche de configuration d'imprimante virtuelle et des bitmaps d'aperçu générés à partir des métriques de l'imprimante elle-même, ce qui est la partie qui rend l'aperçu honnête au sujet de cette marge

La géométrie du papier n'est pas la géométrie imprimable

Deux rectangles décrivent toute cible d'impression, et le décalage entre eux est là où vivent la plupart des bugs d'aperçu. Le rectangle papier est la feuille physique. Le rectangle imprimable est la région plus petite que le moteur d'impression peut réellement atteindre, en retrait d'une marge matérielle qui diffère selon le modèle d'imprimante et parfois selon le bac. La couche d'impression de la bibliothèque mesure les deux. La classe sous-jacente TPLPrinter expose PageWidth et PageHeight pour la zone imprimable, FullPageWidth et FullPageHeight pour la feuille entière, et PrintOffsetX avec PrintOffsetY pour l'écart entre leurs origines, le tout en pixels périphérique à la résolution que rapporte GetDPI. Un aperçu honnête réduit ces mêmes nombres à la résolution de l'écran au lieu de peindre la page dans le rectangle dont le contrôle dispose par hasard. Sautez cette étape et l'aperçu suppose en silence une marge nulle, la seule valeur qu'aucune imprimante réelle n'utilise

Diagramme PDF Library for Delphi de la feuille de papier complète face au rectangle imprimable plus petit, avec PrintOffsetX et PrintOffsetY marquant la marge matérielle entre leurs origines
Le rectangle papier est la feuille physique tandis que le rectangle imprimable est ce que le moteur d'impression peut atteindre, et l'écart entre leurs origines est là où vivent la plupart des bugs d'aperçu

Aperçu à l'écran via RenderPageToDC

Pour un contrôle d'aperçu à l'écran, RenderPageToDC(DPI, Page, DC) dessine une page du document chargé directement sur n'importe quel contexte de périphérique GDI, que ce soit un canevas TPaintBox, un bitmap hors écran ou un DC de métafichier. L'argument DPI fixe le zoom. 96 approche une vue à 100 % sur un écran classique, et le doubler double la taille rendue

procedure TPreviewForm.PreviewBoxPaint(Sender: TObject);
begin
  // ces trois réglages sont un état persistant de la bibliothèque, pas des paramètres par appel :
  FPdf.SetRenderDCOffset(FOffsetX, FOffsetY);
  FPdf.SetRenderDCErasePage(1);
  FPdf.SetRenderCropType(0);
  FPdf.RenderPageToDC(FPreviewDpi, FCurrentPage, PreviewBox.Canvas.Handle);
end;

Le piège est que le chemin de rendu sur DC est piloté par un état persistant de la bibliothèque, non par des paramètres passés à chaque appel. SetRenderDCOffset, SetRenderDCErasePage et SetRenderCropType persistent chacun jusqu'à ce que quelque chose les change, si bien qu'une boucle de vignettes exécutée après que l'utilisateur a ajusté la vue zoomée hérite du décalage ou du rognage laissés par le chemin de code précédent. Le symptôme est un aperçu qui ne dérive que dans certaines séquences de navigation, ce qui est à peu près aussi pénible à reproduire qu'un bug puisse l'être. Poser tous les réglages concernés en tête du gestionnaire de peinture, comme ci-dessus, ne coûte rien et supprime toute la famille. Un second multiplicateur se cache à côté. La résolution de sortie effective est le facteur de rendu multiplié par l'argument DPI, et bien que SetRenderScale vaille 1.0 par défaut, il persiste lui aussi une fois changé, si bien qu'une fonction d'export qui l'a augmenté redimensionne discrètement chaque aperçu ultérieur jusqu'à ce que quelque chose le remette en place

Les visionneuses défilantes et les repeintures partielles ont une variante dédiée. RenderPageToDCClip prend une spécification de découpe en plus du contexte de périphérique, si bien qu'invalider une bande de la fenêtre ne repeint que cette bande au lieu de re-rastériser la page entière. À fort zoom sur des pages grand format, c'est la différence entre une visionneuse qui suit la barre de défilement et une qui traîne derrière

Un travail d'impression conforme à l'aperçu

Le côté impression passe par une imprimante virtuelle. NewCustomPrinter clone une imprimante système dans une configuration privée à la bibliothèque, et SetupPrinter ajuste ce clone sans toucher au DevMode de la machine : le papier entre comme réglage 1 (une constante DMPAPER_*) et l'orientation comme réglage 11. Le gain est l'isolation. Un service peut imprimer des étiquettes A4 pendant que l'imprimante par défaut de l'hôte reste en Letter, et il n'y a rien à restaurer ensuite

PDF Library for Delphi : flux allant de l'imprimante système par défaut vers NewCustomPrinter et SetupPrinter jusqu'à un travail d'impression isolé qui ne touche jamais au DevMode de la machine
SetupPrinter recible un clone privé à la bibliothèque pour qu'un service imprime en A4 pendant que l'imprimante par défaut de l'hôte garde son DevMode Letter intact
var
  Pdf: TPDFlib;
  Virt: WideString;
  Opt: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('report.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    Virt := Pdf.NewCustomPrinter(Pdf.GetDefaultPrinterName);
    Pdf.SetupPrinter(Virt, 1, 9);        // réglage 1 = papier, DMPAPER_A4
    Pdf.SetupPrinter(Virt, 11, 1);       // réglage 11 = orientation, 1 = portrait
    Opt := Pdf.PrintOptions(1, 1, 'Monthly Report');  // ajuster au papier, rotation auto + centrage
    Pdf.PrintDocument(Virt, 1, Pdf.PageCount, Opt);
  finally
    Pdf.Free;
  end;
end;

PrintOptions mérite une lecture attentive. Elle renvoie un handle d'options que vous devez passer à PrintDocument ou PrintPages ; ce n'est pas un état ambiant. Construire les options puis oublier de passer le handle échoue en silence. Le travail s'imprime avec les valeurs par défaut, et personne ne s'en aperçoit avant qu'une politique d'ajustement au papier ait été attendue et qu'une page surdimensionnée soit sortie rognée à la place. L'argument de mise à l'échelle de page est là où vit cette politique. Aucune mise à l'échelle préserve l'exactitude dimensionnelle, ce qui compte pour les formulaires que l'on mesure à la règle. L'ajustement au papier redimensionne tout à la feuille. La réduction des grandes pages laisse les pages normales tranquilles et n'intervient que lorsqu'une page dépasse la zone imprimable, ce qui est en général le bon défaut pour un ensemble de documents hétérogène. L'indicateur de rotation automatique et de centrage gère les pages paysage sans second chemin de code

Les applications qui gèrent déjà un TPrinter via le flux de dialogue VCL peuvent le transmettre directement. PrintDocumentToPrinterObject et PrintPagesToPrinterObject acceptent l'instance TPrinter configurée, ce qui garde la boîte de dialogue d'impression standard comme surface de configuration face à l'utilisateur pendant que la bibliothèque se charge du rendu des pages. Mélanger les deux approches dans un même chemin de code tend à réintroduire la dérive géométrique que tout ce travail visait à supprimer, alors choisissez-en une. La voie de l'imprimante virtuelle convient aux services sans surveillance ; la voie TPrinter convient aux applications interactives

La sortie sélective fonctionne de la même manière. PrintPages prend une chaîne de plage, si bien que passer le nom de l'imprimante virtuelle, '2-5,12' et le handle d'options imprime les pages 2 à 5 et 12 avec le contrat géométrique intact, et la même syntaxe pilote les variantes d'impression vers fichier. Ces variantes fichier sont la réponse pratique pour un environnement sans surveillance dépourvu de périphérique physique : tester en régression la géométrie d'impression sur un serveur de build qui n'a aucune file de pilote. Rendez le même document avec les mêmes options vers un artefact fichier à chaque build, et une régression géométrique devient un diff plutôt qu'un signalement client trois semaines plus tard

Bitmaps d'aperçu avec les métriques de l'imprimante

Un aperçu rendu à 96 DPI face à une taille de page supposée répond à la mauvaise question. Il montre à quoi ressemble la page, pas ce que cette imprimante déposera sur ce papier. GetPrintPreviewBitmapToString comble l'écart en construisant l'aperçu à partir de la même imprimante personnalisée et du même handle d'options que le travail final, si bien que taille de papier, orientation, politique de mise à l'échelle, rotation et décalage matériel alimentent tous le bitmap. Ce qui revient est ce que la feuille montrera

PDF Library for Delphi : contraste entre un aperçu écran fondé sur une taille de page supposée qui fausse les marges et un bitmap fidèle à l'imprimante construit depuis l'imprimante et le handle d'options du travail lui-même
GetPrintPreviewBitmapToString dessine l'aperçu depuis la même imprimante personnalisée et le même handle d'options que le travail final, de sorte que le bitmap montre les marges et la rotation que la feuille recevra vraiment
procedure ShowPrinterTruePreview(Pdf: TPDFlib; const Virt: WideString; Opt: Integer);
var
  Data: AnsiString;
  Strm: TMemoryStream;
  Bmp: TBitmap;
begin
  Data := Pdf.GetPrintPreviewBitmapToString(Virt, 1, Opt, 1200, 0);
  Strm := TMemoryStream.Create;
  try
    Strm.WriteBuffer(PAnsiChar(Data)^, Length(Data));
    Strm.Position := 0;
    Bmp := TBitmap.Create;
    try
      Bmp.LoadFromStream(Strm);
      PreviewImage.Picture.Assign(Bmp);
    finally
      Bmp.Free;
    end;
  finally
    Strm.Free;
  end;
end;

L'argument MaxDimension plafonne le grand côté du bitmap. 1200 pixels reste net pour une boîte de dialogue d'aperçu et garde la mémoire modeste même pour des plans techniques au format E, où un rendu pleine résolution aux 600 DPI de l'imprimante atteindrait les gigaoctets

Se souvenir des choix d'imprimante de l'utilisateur

Les boîtes de dialogue d'impression qui oublient leurs réglages d'une session à l'autre génèrent leurs propres tickets d'assistance. Le couple DevMode, GetPrinterDevModeToString et SetPrinterDevModeFromString, sérialise la configuration complète du pilote d'une imprimante en une chaîne opaque que vous pouvez ranger dans les préférences utilisateur et restaurer à la session suivante, options spécifiques au pilote comprises, celles qu'aucune API générique ne prend la peine de modéliser. Persistez l'imprimante par son nom issu de GetPrinterNames, jamais par son index de liste. L'ordre des index change chaque fois qu'une imprimante est ajoutée ou retirée, si bien qu'un index enregistré pointe discrètement vers le mauvais périphérique dès que la liste bouge. GetDefaultPrinterName couvre le repli quand le périphérique mémorisé a entièrement disparu

La sélection du bac complète le tableau de la persistance. GetPrinterBins rapporte les sources de papier qu'un pilote expose, ce qui compte pour les flux à papier à en-tête où la première page tire du bac en-tête et le reste du papier ordinaire. C'est une politique que les utilisateurs attendent de voir mémorisée par l'application comme tout le reste, et un travail d'impression qui atterrit sur le mauvais papier se lit comme un bug même quand chaque octet du PDF était correct

Gardez un seul moteur pour l'aperçu et pour l'impression

Une dernière décision gouverne discrètement la fidélité. La sélection du moteur de rendu s'applique aux destinations écran comme imprimante, d'où la tentation de prévisualiser avec un moteur rapide et d'imprimer avec un moteur précis. Résistez-y. Faire passer l'aperçu et le travail par des moteurs différents réintroduit exactement la dérive de fidélité qu'un aperçu fidèle à l'imprimante devait supprimer, et le fait d'une manière qui n'apparaît que sur le papier. Les compromis entre les moteurs intégré, Cairo et PDFium sont pesés dans le rendu PDF multi-moteur en Delphi ; choisissez-en un et utilisez-le des deux côtés

Les documents trop volumineux pour être chargés confortablement avant impression peuvent être ouverts par le chemin d'accès direct décrit dans la fusion, la découpe et l'accès direct des gros PDF, qui rend les pages sur un contexte de périphérique depuis un handle de fichier sans construire l'arbre du document. La référence complète de l'API d'impression est sur la page produit de losLab PDF Library for Delphi