Every PDF rasterizer has a file that humiliates it. In one support case it was a utility-bill run where soft-masked logos came out as black rectangles — but only at the customer site, because their build rendered through a different engine than the test bench. Bugs like that are rarely fixable in application code; the page is valid, the engine just disagrees with it. What is fixable is the architecture: make the rendering engine a runtime decision, verify which engines the deployed binary actually contains, and record which engine produced every image. PDFlibPas, losLab's Delphi and C++Builder PDF library, is built for exactly that pattern — one rendering call surface, three selectable engines.
Three rasterizers behind one call surface
The library numbers its engines: 1 is the built-in renderer (the default, with GDI+ smoothing options on Windows), 2 is Cairo, and 3 is PDFium, selected at runtime with SelectRenderer. The external engines load from DLLs whose paths you supply through SetCairoFileName and SetPDFiumFileName. Whatever the engine, the same calls do the work — RenderPageToFile, RenderPageToStream, RenderDocumentToFile — so a fallback strategy changes one integer, not the rendering code.
Under the hood the destination model is broader than bitmaps: the renderer class supports metafiles (WMF, EMF, EMF+), EPS, direct device contexts, printers, and HTML5 output, with Cairo and PDFium appearing as additional destinations when compiled in. For this article the focus is raster output, where engine differences are most visible.
Never assume an engine exists: probe at startup
Cairo and PDFium support are conditional compilation features. A binary built without them will not throw when you select engine 2 or 3 — SelectRenderer simply returns something other than the requested ID, and if you ignore the return value you keep rendering with whatever was active before. The reliable pattern is a startup probe:
function ProbeEngines(PDF: TPDFlib): string;
begin
Result := 'built-in'; // engine 1 is always present
if (PDF.SetCairoFileName('cairo.dll') = 1) and (PDF.SelectRenderer(2) = 2) then
Result := Result + ', cairo';
if (PDF.SetPDFiumFileName('pdfium.dll') = 1) and (PDF.SelectRenderer(3) = 3) then
Result := Result + ', pdfium';
PDF.SelectRenderer(1); // restore the default before real work
end;
Log the probe result with every job. When a customer reports a rendering difference, the first diagnostic question is "which engines does your installation have," and a one-line answer in the log beats a remote-desktop session.
Ten output formats behind one Options integer
The Options parameter of the render calls selects the output encoding: 0 is BMP, 1 JPEG, 2 WMF, 3 EMF, 4 EPS, 5 PNG, 6 GIF, 7 TIFF, 8 EMF+, and 9 HTML5. PNG (5) is the sensible default for previews and archival page images; JPEG (1) paired with SetJPEGQuality wins for photographic scans where size matters.
One format hides a stream requirement. The BMP path patches the resolution fields of the header after the image data is written, seeking back to offset 0x26 in the output. Render BMP into a forward-only stream — a compression wrapper, a network stream — and the call fails in a way that looks engine-related but is not. If a non-seekable target is unavoidable, render PNG instead, or stage BMP output through a memory stream.
The DPI you pass is not the DPI you get
All render calls take a DPI argument, but the effective resolution is that value multiplied by the global render scale. SetRenderScale defaults to 1.0; once changed, it silently applies to every subsequent render on that instance:
PDF.SetRenderScale(2.0); // every later render is doubled
PDF.RenderPageToFile(150, 1, 5, 'p1.png'); // effectively 300 DPI
PDF.SetRenderScale(1.0); // reset, or your thumbnails arrive huge
The same persistence applies to SetRenderCropType and the JPEG quality setting. In a service that renders thumbnails, previews, and print-resolution images from one shared instance, these lingering settings are the root cause behind "thumbnails are suddenly 40 MB" tickets. Either reset state at the top of every operation or dedicate an instance per output profile.
Tuning the default engine before reaching for another
A surprising share of "we need a different engine" requests are really quality-settings requests in disguise. The built-in renderer exposes its smoothing behavior through SetGDIPlusOptions and the broader SetRenderOptions family, and pointing the engine at a specific GDI+ runtime is possible through SetGDIPlusFileName when a deployment environment ships an unusual one. Jagged line art at low DPI, fuzzy text in thumbnails, and banding in gradients all respond to these knobs — and adjusting them costs nothing at deployment time, whereas adding Cairo or PDFium to an installer adds DLLs, bitness variants, and an update obligation.
The practical sequence for a quality complaint is therefore: first reproduce at the customer's exact DPI and scale settings, then try smoothing options on the built-in engine, and only then A/B the page across engines with everything else held constant. Render the same page to PNG through engines 1, 2, and 3 with identical DPI, and attach all three images to the issue. Most of the time two of the three agree, which tells you whether the outlier is the document's interpretation or the baseline expectation — evidence that settles "renders wrong" disputes far faster than adjective-based bug reports.
A fallback chain that explains itself
With probing and state discipline in place, the fallback chain itself is short. Failure detection uses LastRenderError, which carries the engine's message text for the most recent render:
procedure RenderPageWithFallback(PDF: TPDFlib; Page: Integer; const OutFile: string);
begin
PDF.SelectRenderer(1); // built-in first
PDF.RenderPageToFile(200, Page, 5, OutFile); // 5 = PNG
if PDF.LastRenderError = '' then Exit;
LogEngineFailure('built-in', Page, PDF.LastRenderError);
if PDF.SelectRenderer(3) = 3 then // PDFium as the heavy fallback
begin
PDF.RenderPageToFile(200, Page, 5, OutFile);
if PDF.LastRenderError = '' then Exit;
LogEngineFailure('pdfium', Page, PDF.LastRenderError);
end;
raise Exception.CreateFmt('Page %d failed on all available engines', [Page]);
end;
Two design points are deliberate. The chain logs the reason for each switch, because "this page fell back to PDFium since release 3.7" is a regression signal you want trending in monitoring, not buried. And the fallback order is a policy you should choose per workload: the built-in engine deploys with no extra DLLs, which makes it the right first try in most installations, while documents heavy with transparency groups or unusual shading patterns are the classic reason teams wire in an alternative engine at all. Measure on your own corpus; the corpus always wins the argument.
Past single pages: TIFF batches and live device contexts
Two neighbors of the per-page calls round out the toolbox. RenderAsMultipageTIFFToFile renders a page-range expression straight into a multi-page TIFF — the natural shape for archival hand-offs to document management systems that predate PDF. And RenderPageToDC paints directly onto a Windows device context for preview controls, governed by its own trio of sticky settings (SetRenderDCOffset, SetRenderDCErasePage, plus the crop type) that deserve the same reset discipline as the scale factor. Screen preview and print-path rendering have enough of their own traps that they get a dedicated article, linked below.
Rendering questions, answered
Why does SelectRenderer(3) return 0 on my machine? Either the deployed binary was compiled without PDFium support, or SetPDFiumFileName did not resolve to a loadable DLL — wrong path, wrong bitness, or missing dependencies. The probe-at-startup pattern distinguishes the two: if the filename call already returned 0, the DLL is the problem.
Which engine is fastest? There is no stable answer across document types, which is precisely why the library lets you choose per call. Benchmark each engine against a sample of your real documents at your real DPI, and revisit when either the engine DLLs or the document mix changes.
Can different pages of one document use different engines? Yes. SelectRenderer takes effect for subsequent calls on the instance, so a fallback chain can retry a single stubborn page on another engine while the rest of the document stays on the default.
Keep exploring
For preview painting, printer selection, and DevMode handling, continue with the print preview and device context article. If your renders feed a high-volume pipeline over very large files, the handle-based approach in the direct-access guide pairs well with per-page rendering via DARenderPageToFile.
Engine packaging, supported formats, and trial builds are detailed on the PDFlibPas product page.