Technical Article

How to Create a PDF in Delphi Without Picking the Wrong Route

Open a Delphi-generated invoice on a customer laptop and the totals render in a substitute font; send the file to an archive and the PDF/A check rejects it. Both failures start with how the PDF was created. For documents built from code, use a native library such as the HotPDF Delphi component; for existing PDFs, use PDFium Component

Ask how to create a PDF in Delphi and the answers split three ways: print to Microsoft Print to PDF, export from the report designer, or add a PDF library. Every one of them produces a convincing first page. That is why the wrong choice survives the prototype and fails later, when the roadmap asks for embedded fonts, PDF/A (ISO 19005), PAdES signatures, fillable forms or a Lazarus build for Linux. A route that never had the concept cannot add it afterwards; a printer driver has no idea what an AcroForm field is. If you have already settled on a library and only need to weigh the three losLab products, the feature-by-feature comparison of the three libraries goes further. The decision here comes one step earlier

Why does printing to Microsoft Print to PDF fall short?

A PDF printer is fine for one scenario: a user looks at a screen, clicks Print and chooses where the file goes. Whatever you already draw on TPrinter.Canvas becomes a PDF with no extra dependency, and Windows 10 and later normally include the Microsoft Print to PDF driver. Automation is where it breaks. The reason sits in Vcl.Printers: TPrinter.BeginDoc fills the Win32 DOCINFO record with only the document title before it calls StartDoc, so lpszOutput stays nil and the driver opens a Save dialog. You can call StartDoc yourself with lpszOutput set to a path. Then you own raw GDI printing code, written against a driver you do not control, on a Windows feature an administrator can switch off

The file itself is out of your hands too. The driver picks the PDF version, the font handling and the compression. GDI has no notion of hyperlinks, bookmarks, document metadata, form fields, encryption or an OutputIntent, so none of them can reach the PDF. Printing from a Windows service or a scheduled task adds the spooler and the printer list of that account to the risk, a shaky base for a nightly invoice batch. Keep this route for an interactive Save as PDF button. If the document will be archived, signed, filled in or parsed by another system, start with a library

Report designer export: the shortest path for existing layouts

When the documents already exist as report layouts, the designer's own PDF export is the quickest route and usually the right first step. Most Delphi report designers, FastReport and ReportBuilder among them, ship a PDF export, and a layout already tuned for page breaks, grouping and totals is worth more than a rewrite. Test the export in your edition against the requirements above before you commit: Unicode text in the fonts your customers use, fonts embedded instead of referenced by name, PDF/A output, signatures, and generation from a service with no form on screen. A missing item does not mean throwing the layout away. HotPDF ships optional PDF backends for FastReport, QuickReport, ReportBuilder and DevExpress ExpressPrinting, so existing layouts can emit through HotPDF while report authors keep their designer

Generating PDF from code with HotPDF

Invoices, delivery notes, certificates and labels that your application builds itself call for a PDF generation library, and HotPDF is built for that job. THotPDF compiles authoring, image codecs and the font engine into your executable, so nothing extra is installed on the customer machine. The model mirrors TCanvas: set document properties, call BeginDoc, draw on CurrentPage, add pages, finish with EndDoc. Coordinates count from the top-left with Y growing downward, although the file stores bottom-up coordinates as ISO 32000-1 §8.3.2.3 requires. Change two defaults in production code: AutoLaunch starts as True and opens a viewer after saving, and Compression starts as cmNone

uses
  System.SysUtils, Winapi.Windows, Vcl.Graphics, HPDFDoc;

procedure WriteDeliveryNote(const FileName, LogoFile: string);
var
  Pdf: THotPDF;
  Logo: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Document-wide settings must come before BeginDoc
    Pdf.FileName := FileName;
    Pdf.AutoLaunch := False;            // default True opens a viewer after EndDoc
    Pdf.Compression := cmFlateDecode;   // default cmNone writes raw content streams
    Pdf.EnableFontSubsetting := True;   // embed only the glyphs actually drawn
    Pdf.JpegQuality := 85;              // default 100 is large for photos
    Pdf.BeginDoc;                       // page 1 is A4, origin at the top-left

    Logo := Pdf.AddImage(LogoFile, icJpeg);
    Pdf.CurrentPage.ShowImage(Logo, 400, 40, 140, 60, 0);  // X, Y (top edge), W, H, angle

    Pdf.CurrentPage.SetFont('Arial', [fsBold], 18, DEFAULT_CHARSET);
    Pdf.CurrentPage.TextOut(50, 50, 0, 'Delivery note 2041');
    Pdf.CurrentPage.SetFont('Arial', [], 11, DEFAULT_CHARSET);
    Pdf.CurrentPage.TextOut(50, 120, 0, 'Ship to: Müller GmbH, Köln');
    Pdf.CurrentPage.TextOut(50, 138, 0, '3 × pallet, 1 × crate');

    Pdf.AddPage;                        // font state does not carry over
    Pdf.CurrentPage.SetFont('Arial', [], 11, DEFAULT_CHARSET);
    Pdf.CurrentPage.TextOut(50, 50, 0, 'Terms and conditions');
    Pdf.EndDoc;                         // writes FileName
  finally
    Pdf.Free;
  end;
end;

AddImage registers the logo once and returns an index; ShowImage places it, with Y marking the top edge of the image box just as it marks the top of a text line. Font state does not survive AddPage, so page two sets its font again or falls back to 12-point Arial. FontEmbedding is True by default, which is what keeps the substitute-font problem from the first paragraph away. The late requirements live in the same component: PDFACompliance with an ICC profile passed to AddPDFAOutputIntent for PDF/A, AddTextField and AddCheckBox for AcroForm fields, THotPDF.SignPDFWithPFX for signing, and AddFacturXAssociatedFile for Factur-X and ZUGFeRD invoices. The HotPDF Hello World sample for Delphi walks through a complete first program

What changes when the PDF already exists?

When the job starts from PDFs you did not create, such as showing them in a form, rendering thumbnails, filling their fields or stamping approval marks, PDFium Component is the natural fit. It pairs the PDFium engine, the engine family behind PDF display in Chromium, with an Object Pascal layer: TPdf is the non-visual document, TPdfView a scrolling viewer for your form, TPdfThumbnailView a page strip beside it. Its LCL package builds with Lazarus on Windows, on Linux with Qt6 and on macOS with Cocoa. Ship the PDFium runtime that matches the target platform and bitness. A text stamp on every page of an existing document:

uses
  System.SysUtils, Vcl.Graphics, PDFium;

// Stamps one line of text near the bottom-left corner of every page
// and saves the result as a new file
procedure StampAllPages(const InFile, OutFile, Stamp: string);
var
  Pdf: TPdf;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := InFile;
    Pdf.Active := True;                 // reports failure through Active, not an exception
    if not Pdf.Active then
      raise Exception.CreateFmt('Cannot open %s: %s',
        [InFile, Pdf.LastLoadReport.ErrorMessage]);

    for I := 1 to Pdf.PageCount do
    begin
      Pdf.PageNumber := I;              // 1-based; AddText draws on the current page
      // PDF user space: origin at the bottom-left, Y grows upward
      Pdf.AddText(Stamp, 'Arial', 9, 36, 24, clGray, $C0, 0);
    end;

    if not Pdf.SaveAs(OutFile) then
      raise Exception.Create('Save failed: ' + OutFile);
  finally
    Pdf.Free;
  end;
end;

Setting Active to True does not raise when the file is missing, damaged or locked by a password you did not supply. It leaves Active False and fills LastLoadReport, so the check right after the assignment is mandatory. AddText works in PDF user space, origin bottom-left and Y upward, the opposite of HotPDF; a project that generates with one library and stamps with the other wants one conversion helper (PDF Y equals PageHeight minus Y from the top) rather than flipped constants scattered through the code. Pages with a non-zero CropBox origin or a /Rotate entry (ISO 32000-1 §7.7.3.3) need PageBounds and PageRotation in that calculation. SaveAs writes to a temporary file and then replaces the target. Loading the DLL and a first viewer form are covered in getting started with PDFium Component in Delphi and Lazarus

Compliance pipelines: PDF/A, signatures and e-invoices

HotPDF, PDFium Component and losLab PDF Library all produce PDF/A and PAdES signatures; in each of them the long-term B-LT and B-LTA levels come with the Enterprise edition. The choice turns on where the PDF sits in your workflow. If your application creates the document, generate it compliant from the start with HotPDF. If the pipeline receives PDFs and has to convert, tag, sign, validate and deliver them, losLab PDF Library (the Delphi PDF Library, also known as PDFlibPas) is built for that role. Its Enterprise edition converts existing files to PDF/A with ConvertToPDFA and reports what changed, ConvertToPDFX and AutoTagPages prepare print and tagged documents, and the engine is callable through DLL, ActiveX and Dylib builds or headless on Linux and macOS. Supplier invoices are a typical case:

uses
  System.SysUtils, PDFlibrary;

// Returns an empty string when the PDF carries a well-formed
// Factur-X / ZUGFeRD container, otherwise a description of the problem
function CheckIncomingInvoice(const FileName: string): string;
var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(FileName, '') <> 1 then
      Exit('cannot load PDF, error ' + IntToStr(Lib.LastErrorCode));
    if Lib.DetectFacturXInvoice <> 1 then
      Exit('no embedded invoice XML');
    if Lib.ValidateFacturXInvoice = 1 then
      Result := ''
    else
      Result := string(Lib.GetFacturXValidationIssues(', '));
    // GetFacturXInvoiceInfo(5) returns the declared conformance level, e.g. EN 16931
  finally
    Lib.Free;
  end;
end;

Factur-X and ZUGFeRD are hybrid invoices: a readable PDF/A-3 document (ISO 19005-3) with EN 16931 XML attached as an associated file, where the AFRelationship value, the XMP metadata and the attachment have to agree. ValidateFacturXInvoice checks that container, from PDF/A-3 identification and the catalog /AF array to the declared file name and conformance level, and GetFacturXValidationIssues names each failure, such as NotPDFA3 or MissingInvoiceXML. A clean container says nothing about totals or tax lines. Those business rules are a second pass through ValidateInvoiceBusinessRules, available from the Small Business edition. Run both on a sample from every new supplier before automatic routing; container errors and rule errors come from different parts of the sender's toolchain. Producing such files is covered in Factur-X and ZUGFeRD hybrid invoices in Delphi

Which approach fits which requirement?

Pick by the hardest requirement you can already see. The first page never tells you anything:

RequirementStart withWhy
An interactive Save as PDF for what is on screenTPrinter and Microsoft Print to PDFNo dependency; the user picks the file name
Existing report layoutsThe designer's PDF export, or a HotPDF report backendKeeps tuned layouts; the HotPDF backend adds font and standards control
Invoices, reports and labels generated from codeHotPDFNative authoring, embedded and subset fonts, nothing extra to deploy
Embedded viewer, thumbnails, stamping or form filling on existing PDFsPDFium ComponentPDFium rendering plus TPdfView and editing calls on TPdf
PDF/A, PAdES signatures and AcroForm fields in documents you createHotPDFCompliance is part of generation, so the first file is already correct; B-LT and B-LTA with Enterprise
Converting, tagging or validating PDFs you receivelosLab PDF LibraryConvertToPDFA and compliance reports in the Enterprise edition, AutoTagPages for untagged pages
Factur-X, ZUGFeRD or XRechnungHotPDF for invoices you issue, losLab PDF Library for invoices you receiveBoth handle the PDF/A-3 container; EN 16931 rule validation in both comes with the Small Business edition
Lazarus projectsHotPDF or losLab PDF Library on Windows, PDFium Component on Linux and macOSHotPDF builds with Free Pascal 3.3.1 or later for Win32 and Win64, losLab PDF Library with Free Pascal 3.2.2 on Windows x64, and the PDFium LCL package targets Qt6 and Cocoa
Headless servers on Linux or macOS, or callers in C# and VBAlosLab PDF LibraryNative headless builds plus DLL, ActiveX and Dylib packaging

Before writing production code, run the demo that matches your row against one of your own documents, on a clean machine without your fonts installed. For generated reports, start with the DBTable demo in the HotPDF Delphi PDF component package. For existing files, try the WatermarkStamp and PDFViewer demos of PDFium Component for Delphi and Lazarus. For incoming invoices, use the E-Invoice scenario in ComplianceLab from the losLab Delphi PDF Library. If PDF/A is on the roadmap, put the output through veraPDF in the same session