Technical Article

PDFium in Delphi and Lazarus: Your First PDFium Component App

A new PDFium project in Delphi often stalls before a single page appears: Active := True leaves the document closed because pdfium.dll is missing or built for the other bitness. Put the Win32 or Win64 pdfium.dll that matches your executable where PDFium Component searches, and TPdf and TPdfView open, render and display PDFs the same way in Delphi and Lazarus

Where does PDFium Component look for pdfium.dll?

PDFium Component binds pdfium.dll lazily, on the first document load, and checks a fixed list of places before handing the bare file name to Windows. The order comes straight from the loader in PDFium.pas, and it turns most "module not found" reports into a quick fix:

  1. If the global LibraryName variable contains a path, that exact file is loaded and nothing else is tried, so a wrong path stays a visible error instead of silently picking up some other copy
  2. DLLs\Win32\pdfium.dll or DLLs\Win64\pdfium.dll under the executable's folder, then the same DLLs subfolder under each parent folder, up to eight levels up
  3. A Win32 or Win64 subfolder directly beside the executable
  4. pdfium.dll in the executable's own folder
  5. The system folder for the process bitness: System32 for a 64-bit process, SysWOW64 and then System32 for a 32-bit one
  6. Failing all of that, the plain file name, resolved by the standard Windows DLL search order

The installer places prebuilt binaries compiled from the open-source PDFium project in DLLs\Win32 and DLLs\Win64. Each folder holds the plain pdfium.dll next to pdfium.v8.dll, the build that carries the V8 engine for XFA forms and form JavaScript and is selected by the global EnableV8Engine flag. Bitness follows the process, not Windows: a Win32 executable on 64-bit Windows still needs the 32-bit DLL. Get it wrong and Windows reports ERROR_BAD_EXE_FORMAT (193), to which the component appends a note that the DLL was found but its architecture does not match the host process. The file is there. Only its bitness is wrong

The upward walk in step 2 explains why the bundled demos start without complaint while a fresh project under C:\Projects does not: the demo executables sit inside the installation folder, below the shipped DLLs tree, and the new project has no such ancestor. Give each project its own copy. Running DLLs\CopyDLLs.bat <YourProject>\DLLs creates Win32 and Win64 subfolders there, and an executable built into <YourProject>\Win32\Debug finds them by walking up. Started without arguments, the same script offers to copy into System32 and SysWOW64 instead; avoid that on customer machines, because a system folder holds a single pdfium.dll for every program on the computer, so the plain build, the V8 build and differing versions overwrite one another. Missing exports, version skew and threaded loading are covered in deploying the PDFium DLL and diagnosing load failures

Render a PDF page to PNG in Delphi

The smallest useful TPdf program opens a file, reads the page count and turns one page into an image. TPdf is a non-visual TComponent, so the function below needs no form and runs from a button handler, a service or a command-line tool alike. It reports load failures with the real reason, and the notes after the code explain the lines that most often trip people up:

uses
  System.SysUtils, Vcl.Graphics, Vcl.Imaging.pngimage, PDFium;

// Renders page PageNo (1-based) of PdfFile to PngFile at Dpi and
// returns the document's page count
function ExportPageAsPng(const PdfFile, PngFile: string;
  PageNo, Dpi: Integer): Integer;
var
  Pdf: TPdf;
  Bmp: TBitmap;
  Png: TPngImage;
  W, H: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := PdfFile;
    Pdf.Active := True;                 // never raises, so check the result
    if not Pdf.Active then
      raise Exception.CreateFmt('Cannot open %s: %s',
        [PdfFile, Pdf.LastLoadReport.ErrorMessage]);
    Result := Pdf.PageCount;

    Pdf.PageNumber := PageNo;           // default 0 means no page is loaded
    W := Round(PointsToPixels(Pdf.PageWidth, Dpi));   // 1 point = 1/72 inch
    H := Round(PointsToPixels(Pdf.PageHeight, Dpi));

    Bmp := Pdf.RenderPage(0, 0, W, H, ro0, [reAnnotations], clWhite);
    try
      Png := TPngImage.Create;
      try
        Png.Assign(Bmp);
        Png.SaveToFile(PngFile);
      finally
        Png.Free;
      end;
    finally
      Bmp.Free;                         // the function form returns a new bitmap
    end;
  finally
    Pdf.Free;
  end;
end;
  • Pdf.Active := True never raises. The setter catches every load error and leaves Active at False, so a try/except around it stays quiet and the next line fails instead, with PageCount reporting "Cannot perform this operation on an inactive Pdf1 component" for a component dropped on a form as Pdf1; LastLoadReport.ErrorMessage holds the actual cause
  • TPdf.PageNumber starts at 0, which means no page is loaded, so calling RenderPage straight after opening raises "Cannot perform this operation on an inactive page". Valid values run from 1 to PageCount, and anything else raises "Incorrect page number" right away
  • PageWidth and PageHeight are in PDF points of 1/72 inch, and PointsToPixels converts them for the target resolution: an A4 page (595.28 by 841.89 points) at 150 DPI becomes a 1240 by 1754 pixel bitmap
  • An empty options set leaves out annotation appearances, so highlights, stamps and sticky-note icons vanish from the export. reAnnotations maps to PDFium's FPDF_ANNOT flag and is the option worth turning on for any image a person will look at
  • The function form of RenderPage allocates the TBitmap and the caller frees it, while the overload that takes a TBitmap parameter renders into one you already own. JPEG output, quality settings and whole-document batches are covered in rendering PDF pages to JPEG with PDFium Component

Build a Delphi PDF viewer form with TPdfView

A working Delphi PDF viewer needs one TPdf, one TPdfView linked to it, and a few commands on top. The form below creates everything in code, so it pastes into a new VCL Forms Application without touching the designer; assign FormCreate to the form's OnCreate event and it runs. Dropping both components from the losLab palette works just as well, with the same property assignments made in the Object Inspector:

unit ViewerMain;

interface

uses
  System.SysUtils, System.Classes, Vcl.Controls, Vcl.Forms, Vcl.StdCtrls,
  Vcl.ExtCtrls, Vcl.Dialogs, PDFium, FPdfView;

type
  TFormViewer = class(TForm)
    procedure FormCreate(Sender: TObject);
  private
    FPdf: TPdf;
    FView: TPdfView;
    FBar: TPanel;
    procedure AddButton(const ACaption: string; AOnClick: TNotifyEvent);
    procedure OpenClick(Sender: TObject);
    procedure FitWidthClick(Sender: TObject);
    procedure ZoomInClick(Sender: TObject);
    procedure ZoomOutClick(Sender: TObject);
    procedure ViewPageChange(Sender: TObject);
    procedure OpenPdf(const FileName: string);
  end;

var
  FormViewer: TFormViewer;

implementation

{$R *.dfm}

procedure TFormViewer.FormCreate(Sender: TObject);
begin
  FBar := TPanel.Create(Self);
  FBar.Parent := Self;
  FBar.Align := alTop;
  AddButton('Open...', OpenClick);
  AddButton('Fit width', FitWidthClick);
  AddButton('Zoom +', ZoomInClick);
  AddButton('Zoom -', ZoomOutClick);

  FPdf := TPdf.Create(Self);
  FView := TPdfView.Create(Self);
  FView.Parent := Self;
  FView.Align := alClient;
  FView.Pdf := FPdf;                    // allowed only while the view is inactive
  FView.OnPageChange := ViewPageChange;
end;

procedure TFormViewer.AddButton(const ACaption: string; AOnClick: TNotifyEvent);
var
  Button: TButton;
begin
  Button := TButton.Create(Self);
  Button.Parent := FBar;
  Button.Left := FBar.ControlCount * 100; // keep creation order left to right
  Button.Align := alLeft;
  Button.Caption := ACaption;
  Button.OnClick := AOnClick;
end;

procedure TFormViewer.OpenPdf(const FileName: string);
var
  Password: string;
begin
  FView.Active := False;
  FPdf.Active := False;
  FPdf.FileName := FileName;
  FPdf.Password := '';
  FView.PageNumber := 1;
  FView.Active := True;                 // opens FPdf as well and never raises
  if (not FView.Active) and
     (FPdf.LastLoadReport.NativeErrorCode = FPDF_ERR_PASSWORD) and
     InputQuery('Protected PDF', 'Open password:', Password) then
  begin
    FPdf.Password := Password;          // must be set while FPdf is inactive
    FView.Active := True;
  end;
  if not FView.Active then
    raise Exception.CreateFmt('Cannot open %s: %s',
      [FileName, FPdf.LastLoadReport.ErrorMessage]);
  FView.FitMode := pfmFitWidth;
  FView.SetFocus;                       // PgUp, PgDn, Ctrl+Home, Ctrl+End
end;

procedure TFormViewer.OpenClick(Sender: TObject);
var
  Dialog: TOpenDialog;
begin
  Dialog := TOpenDialog.Create(nil);
  try
    Dialog.Filter := 'PDF files (*.pdf)|*.pdf';
    if Dialog.Execute then
      OpenPdf(Dialog.FileName);
  finally
    Dialog.Free;
  end;
end;

procedure TFormViewer.FitWidthClick(Sender: TObject);
begin
  FView.FitMode := pfmFitWidth;         // re-applied on resize and page change
end;

procedure TFormViewer.ZoomInClick(Sender: TObject);
begin
  if FView.Active then
    FView.Zoom := FView.Zoom * 1.25;    // 1.0 = 100%; resets FitMode to pfmNone
end;

procedure TFormViewer.ZoomOutClick(Sender: TObject);
begin
  if FView.Active then
    FView.Zoom := FView.Zoom / 1.25;
end;

procedure TFormViewer.ViewPageChange(Sender: TObject);
begin
  Caption := Format('Page %d of %d', [FView.PageNumber, FPdf.PageCount]);
end;

end.

Activation goes through the view. TPdfView has its own Active property and paints a page only once that property is True; setting it opens the linked TPdf too, so OpenPdf activates the view, and code that opens only the document is left with an empty control. The Pdf link can be assigned only while the view is inactive, which is why the form sets it once in FormCreate and closes the view before switching files. OnPageChange fires each time the view loads a page, including the first page at activation, so the caption always shows the current position

Password handling hinges on one field. After a failed activation, LastLoadReport.NativeErrorCode equals FPDF_ERR_PASSWORD from the FPdfView unit only when the file is encrypted with an open password the component did not receive. That lets the viewer prompt on a protected file and report a damaged one straight away, rather than asking for a password that does not exist. Password must be assigned while the document is closed, because its setter refuses changes to an open TPdf

Navigation and zoom live on the view. TPdf.PageNumber and TPdfView.PageNumber are independent properties, a viewer drives the view's, and out-of-range values assigned to it are ignored. With the view focused, Page Up and Page Down step through pages and Ctrl+Home and Ctrl+End jump to the ends, governed by AllowUserPageChange, which defaults to True. Zoom is a scale factor, so 1.5 means 150%. Assigning it cancels the active fit mode, the convention desktop readers follow when you type a zoom value, while an active FitMode re-applies itself whenever the window is resized or the page changes

Lazarus and Free Pascal: package, mode and bitness

On Lazarus the components and their API are the same, and the work shifts to package setup and project options. Tick Lazarus on the installer's optional-content page, open Lib\FPC\PDFiumLaz.lpk in the IDE, compile it, then install it and let Lazarus rebuild itself; TPdf, TPdfView and TPdfThumbnailView then appear on a losLab palette page, exactly as in Delphi. The package depends on LCL and IDEIntf, compiles its units into Lib\FPC\i386-win32 or Lib\FPC\x86_64-win64 depending on the target, and on Windows uses the same DLLs\Win32 and DLLs\Win64 search as Delphi. The project file below adds a habit that pays off in both IDEs, pinning the engine and loading it at startup so a deployment mistake surfaces as a clear message before the user opens anything, and the remaining differences follow it:

program PdfViewerLaz;

{$MODE Delphi}

uses
  Interfaces, Forms, Dialogs, SysUtils, PDFium, ViewerMain;

{$R *.res}

begin
  Application.Initialize;
  // Optional: pin an explicit engine location. A value with a path is
  // loaded as-is in either engine mode and disables the DLLs-tree search
  {$IFDEF WIN64}
  LibraryName := ExtractFilePath(ParamStr(0)) + 'engine\x64\pdfium.dll';
  {$ELSE}
  LibraryName := ExtractFilePath(ParamStr(0)) + 'engine\x86\pdfium.dll';
  {$ENDIF}
  try
    PDFium.LoadLibrary;                 // qualified: Windows has a LoadLibrary too
  except
    on E: EPdfError do
    begin
      ShowMessage('PDF engine did not start: ' + E.Message);
      Exit;
    end;
  end;
  Application.CreateForm(TFormViewer, FormViewer);
  Application.Run;
end.
  • Keep {$MODE Delphi} in units that use the component. The package compiles in Delphi syntax mode, and the viewer unit above passes method pointers Delphi-style (AddButton('Open...', OpenClick)), which under {$MODE ObjFPC} must be written @OpenClick
  • Unit and resource names change: SysUtils, Forms and ExtCtrls drop the System. and Vcl. prefixes, the form resource becomes {$R *.lfm}, and LCL's TPortableNetworkGraphic replaces TPngImage in the exporter
  • Text APIs return WString, which is string on Delphi and WideString on Free Pascal, so extracted text bound for an LCL control goes through the usual UTF-16 to UTF-8 conversion
  • A 64-bit Lazarus install builds x86_64-win64 executables unless the project's target CPU says otherwise, so those need the Win64 DLL; in Delphi, the Target Platforms node in the Projects window shows which platform the build uses
  • A pinned LibraryName is used exactly as given, so point it at pdfium.v8.dll when the application needs XFA
  • Outside Windows, set LibraryName to the libpdfium.so or libpdfium.dylib built for that target. String handling, conditional code and other Free Pascal differences are covered in building a PDF viewer for Lazarus and Free Pascal with PDFium

Which Delphi versions does PDFium Component support?

PDFium Component supports Delphi and C++Builder from RAD Studio XE5 through 13.2 Florence, with Win32 and Win64 packages for each release in folders named by the IDE's internal BDS number, from Lib\12.0 (XE5) to Lib\37.0 (13), plus the Lazarus packages under Lib\FPC. People regularly search for a Delphi 2007 build, and the answer follows from the source as much as from packaging: on Delphi the component's text type WString is the Unicode string that arrived in Delphi 2009, and the cancellable async helpers are generic classes such as TPdfFuture<T>, a language feature from the same release. Delphi 2007 (BDS 5.0) predates both, and the installer ships no package folder for it. A Delphi 2007 application that needs PDFium rendering is better served by moving that project to XE5 or later, or by building the PDF part with Lazarus, which compiles the same TPdf and TPdfView source

Why does a page render blank or a file refuse to open?

Almost every open or render failure at this stage traces back to the environment or to the input file, and LastLoadReport.ErrorMessage names which one after any failed Active := True. The messages and symptoms map to causes like this:

  • Windows' "module could not be found" text followed by a DLL path: none of the search locations held the file, so copy the DLLs tree beside the executable
  • The same text plus the note that the DLL architecture does not match the host process: the file exists but has the other bitness, typically a Win64 build target sitting next to the 32-bit DLL
  • "Password required or incorrect password": the PDF is encrypted with an open password, and Password has to be set before activation
  • "File not in PDF format or corrupted": usually not a PDF at all, such as an HTML error page saved under a .pdf name or a truncated download; compare the first bytes of the file with %PDF-
  • The document opens but the viewer stays empty: the view was never activated, or the code activated only the TPdf
  • An exported image is missing highlights, stamps or comment icons: the render call omitted reAnnotations
  • An XFA form shows only a "Please wait" placeholder page: the document needs the V8 build. The component switches to pdfium.v8.dll by itself when it detects XFA markers before the engine is loaded, but a process that already loaded the plain DLL fires OnXfaRuntimeMissing instead, so applications that expect XFA should set EnableV8Engine := True at startup

Run the exporter against a handful of your own PDFs before wiring up the viewer. Real files surface a misplaced DLL, an encrypted document or a mislabeled download much sooner than the bundled samples do. The trial installer includes TPdfView, so you can build this viewer before buying a license, and it is available from the PDFium Component product page