Technical Article

PDFlibPas Print Preview and Device-Context Output in Delphi

The ticket was three lines long: "Preview shows the form centered. The printed page is shifted up and left, and the border is clipped on two sides." Nothing was wrong with the rendering code. The preview drew the page relative to the sheet of paper, while the printer's device context origin sits at the corner of the printable area — and the laser printer in question could not reach the outer 4.2 mm of the sheet. Every Delphi print feature eventually collides with this mismatch, and the cheap time to handle it is before the first customer prints a bordered form. losLab PDF Library (PDFlibPas) covers the whole path with device-context rendering calls, a virtual-printer configuration layer, and preview bitmaps generated from the printer's own metrics.

Paper geometry is not printable geometry

Three rectangles describe any print target, and confusing them produces exactly the ticket above. The paper rectangle is the physical sheet. The printable rectangle is the region the print engine can reach. The offset between their origins is the hardware margin, and it differs per printer model and sometimes per tray. The library's printing layer models all three: the underlying TPLPrinter class exposes PageWidth and PageHeight for the printable area, FullPageWidth and FullPageHeight for the sheet, and PrintOffsetX and PrintOffsetY for the gap, all in device pixels at the resolution reported by GetDPI. An honest preview scales the same three rectangles down to screen resolution instead of painting the page into whatever rectangle the control happens to have.

Screen preview through RenderPageToDC

For an on-screen preview control, RenderPageToDC(DPI, Page, DC) draws a page of the loaded document straight onto any GDI device context — a TPaintBox canvas, an off-screen bitmap, or a metafile DC. The DPI argument sets the zoom: 96 approximates a 100% view on a classic display, and doubling it doubles the rendered size.

procedure TPreviewForm.PreviewBoxPaint(Sender: TObject);
begin
  // these three are sticky library state, not per-call parameters:
  FPdf.SetRenderDCOffset(FOffsetX, FOffsetY);
  FPdf.SetRenderDCErasePage(1);
  FPdf.SetRenderCropType(0);
  FPdf.RenderPageToDC(FPreviewDpi, FCurrentPage, PreviewBox.Canvas.Handle);
end;

The trap is that the DC render path is steered by sticky library state rather than per-call parameters. SetRenderDCOffset, SetRenderDCErasePage, and SetRenderCropType each persist until changed, so a thumbnail loop that runs after the user adjusted the zoomed view inherits whatever offset or crop the previous code path left behind — and the symptom is a preview that drifts only in specific navigation sequences, which is miserable to reproduce. Setting all relevant state at the top of the paint handler, as above, costs nothing and removes the whole bug class. A second multiplier hides nearby: the effective output resolution is the render scale times the DPI argument, and SetRenderScale defaults to 1.0 but persists once changed, so an export feature that adjusted it quietly rescales every later preview.

Scrolling viewers and partial repaints have a dedicated variant: RenderPageToDCClip takes a clip specification along with the device context, so invalidating one band of the window repaints only that band instead of re-rasterizing the full page. At high zoom levels on large-format pages, that difference is the line between a viewer that tracks the scrollbar and one that smears behind it.

A print job that matches the preview

The printing side works through a virtual printer: NewCustomPrinter clones a system printer into a library-private configuration, and SetupPrinter adjusts that clone — paper with setting 1 (a DMPAPER_* constant) and orientation with setting 11 — without touching the machine-wide DevMode. A service can print A4 labels while the host's default printer stays on Letter, and nothing needs restoring afterwards.

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);        // setting 1 = paper, DMPAPER_A4
    Pdf.SetupPrinter(Virt, 11, 1);       // setting 11 = orientation, 1 = portrait
    Opt := Pdf.PrintOptions(1, 1, 'Monthly Report');  // fit to paper, auto-rotate + center
    Pdf.PrintDocument(Virt, 1, Pdf.PageCount, Opt);
  finally
    Pdf.Free;
  end;
end;

PrintOptions deserves a careful read: it returns an options handle that must be passed to PrintDocument or PrintPages. It is not ambient state. Building options and forgetting to pass the handle is a silent failure — the job prints with defaults, and nobody notices until a fit-to-paper policy was expected and an oversized page came out cropped. The page-scaling argument carries the policy: no scaling preserves dimensional accuracy for forms that get measured against rulers, fit-to-paper rescales everything, and shrink-large-pages intervenes only when a page exceeds the printable area — usually the right default for mixed document sets. The auto-rotate-and-center flag handles landscape pages without a separate code path.

Applications that already manage a TPrinter through the VCL dialog flow can hand it over directly: PrintDocumentToPrinterObject and PrintPagesToPrinterObject accept the configured TPrinter instance, which keeps the standard print dialog as the user-facing configuration surface while the library handles page rendering. The two approaches do not mix well in one code path — pick the virtual-printer route for unattended services and the TPrinter route for interactive applications, and the geometry contract stays singular.

Unattended environments also get output without a physical device: the page-range printing calls have print-to-file variants, which is the practical answer for regression-testing print geometry on a build server with no driver queue. Render the same document through the same options into a file artifact on every build, and a geometry regression becomes a diff instead of a customer report.

Preview bitmaps with the printer's own metrics

A preview rendered at 96 DPI against an assumed page size answers the wrong question. GetPrintPreviewBitmapToString builds the preview using the same custom printer and the same options handle as the eventual job, so paper size, orientation, scaling policy, rotation, and the hardware offset all participate — what comes back is what the sheet will show.

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;

The MaxDimension argument caps the bitmap's long edge: 1200 pixels is comfortably sharp for a preview dialog and keeps memory modest even for E-size engineering drawings, where a full-resolution render at the printer's 600 DPI would run to gigabytes.

Remembering the user's printer choices

Print dialogs that forget their settings between sessions generate support tickets of their own. The DevMode pair — GetPrinterDevModeToString and SetPrinterDevModeFromString — serializes a printer's full driver configuration to an opaque string you can store in user preferences and restore next session, including driver-specific options no generic API models. Persist the printer by name from GetPrinterNames, never by list index: index order changes every time a printer is added or removed, and GetDefaultPrinterName covers the fallback when the remembered device has vanished.

Tray selection rounds out the persistence story: GetPrinterBins reports the paper sources a driver exposes, which matters for letterhead workflows where page one pulls from the letterhead tray and the rest from plain stock — a policy users expect the application to remember along with everything else.

Questions that come up in printing projects

Why is the printed page shifted relative to my preview?

Almost always the hardware margin: the printer DC origin is the printable-area corner, not the paper corner. Model the offset explicitly in the preview, or generate previews with GetPrintPreviewBitmapToString so the printer geometry is baked in.

How do I print a page selection like 2-5 and 12?

PrintPages accepts a range string — pass the virtual printer name, '2-5,12', and the options handle. The same range syntax drives the print-to-file variants.

Can the preview and the print job use different rendering engines?

They can, but they should not: the engine selection applies to both screen and printer destinations, and mixing engines reintroduces exactly the fidelity drift a printer-true preview eliminates. The trade-offs between the built-in, Cairo, and PDFium engines are weighed in multi-engine PDF rendering in Delphi.

Documents too large to load comfortably before printing can be opened through the direct-access path described in large PDF merge, split, and direct access, which renders pages to a device context from a file handle without building the document tree. The complete printing API reference is on the losLab PDF Library for Delphi product page.