Skip to main content
Table of Contents

Performance and large documents

A one-page report is fast whatever you do. A 400-page catalog with a photo on every page, or a nightly job that generates thousands of invoices, is where generation time, memory use and file size start to matter — and almost all of it is decided by a handful of choices: how images are loaded, sized and encoded, how many fonts the document uses, whether page streams can be compressed, and where the finished bytes go. This chapter explains what the library does with each of those at generation time, so you can make the choice that keeps a large document small and quick. None of it changes how you draw: the calls are the ones from Drawing graphics and Text and fonts. Reach for this chapter when a document is large, image-heavy or generated in bulk; for a short document, the defaults are the right answer.

Where these rules apply

The behaviour described here is that of the library's own PDF engine, which TTMSFNCPDFLib uses for VCL, for FMX on Windows and Linux, and for TMS WEB Core. FMX on macOS, iOS and Android hands generation to the platform's native PDF support by default, and there image encoding and font embedding are the platform's. The advice on loading and sizing images still pays off on every platform; the specifics of deduplication, JPEG encoding and stream compression below describe the library's engine.

How a document is held until EndDocument

It helps to know that nothing is written out while you draw. Every page, font and image is accumulated in memory, and the file is assembled only in EndDocument: fonts are written, images are encoded, placeholders such as the total page count are resolved, and the parts are concatenated into the final PDF. Two consequences follow:

  • Passing a file name does not reduce memory use. BeginDocument(AFileName) only decides where the finished bytes go; the whole document is still built in memory first. Peak memory is reached during EndDocument, while the final PDF is assembled.
  • EndDocument is where image-heavy documents spend their time. Image encoding is deferred until the document is finished, so a slow EndDocument on a photo catalog is expected, not a hang.

With no file name, EndDocument returns the finished PDF as a new TMemoryStream. It is a separate copy that you own — free it after handing it on. With a file name, EndDocument writes the file and returns nil.

Generate to a stream you ownPascal
uses
  System.Classes, FMX.TMSFNCPDFLib;

function TForm1.BuildInvoicePDF: TMemoryStream;
var
  p: TTMSFNCPDFLib;
begin
  p := TTMSFNCPDFLib.Create;
  try
    p.BeginDocument; { no file name: the bytes come back from EndDocument }
    p.NewPage;
    p.Graphics.DrawText('Invoice 2026-0412', PointF(40, 40));
    { The caller owns the returned stream and must free it. }
    Result := p.EndDocument;
  finally
    p.Free;
  end;
end;

procedure TForm1.SendInvoiceClick(Sender: TObject);
var
  ms: TMemoryStream;
begin
  ms := BuildInvoicePDF;
  try
    ms.Position := 0;
    ms.SaveToFile('invoice-2026-0412.pdf'); { or copy it into an HTTP response }
  finally
    ms.Free;
  end;
end;

When a single document would be too large to hold comfortably, generate several smaller documents instead of one — one per month, one per customer — each with its own BeginDocument/EndDocument pair. All per-document state, including the font cache described below, is released at the end of each pair.

Reusing images

Most size problems in generated PDFs are images, and the first thing to know is that the library already avoids duplicates for you. Every DrawImage, DrawImageFromFile and DrawImageWithName call serializes the bitmap and compares it with the images already in the document. When the pixels are identical and the call uses the same quality and background colour as the earlier one, the page simply references the image that is already embedded. A logo drawn on 500 pages is stored once.

That rule tells you what defeats it, and what it costs:

  • Keep the quality and background colour constant for an image you repeat. The same logo drawn at quality 0.85 on one page and 0.9 on the next is embedded twice.
  • Load a repeated image once. DrawImageFromFile reads and decodes the file on every call, and the comparison then has to rule the copy out again. Load the file into a TTMSFNCBitmap once and pass that bitmap to DrawImage on every page. DrawImageWithName with a BitmapContainer achieves the same by keeping the decoded bitmap in the container.
  • Expect each draw call to cost a comparison. The check runs against every image already embedded, so a document with many distinct images pays a little more per image as it grows. That is another reason to keep images small.
Load a repeated image once and draw it on every pagePascal
uses
  System.SysUtils, FMX.TMSFNCPDFLib, FMX.TMSFNCPDFCoreLibBase, FMX.TMSFNCTypes,
  FMX.TMSFNCGraphicsTypes;

procedure TForm1.GenerateLetterhead(const AFileName: string; APageCount: Integer);
var
  p: TTMSFNCPDFLib;
  Logo: TTMSFNCBitmap;
  I: Integer;
begin
  Logo := TTMSFNCBitmap.Create;
  p := TTMSFNCPDFLib.Create;
  try
    { Decode the file once. DrawImageFromFile would read and decode it again on
      every call. }
    Logo.LoadFromFile('logo.png');

    p.BeginDocument(AFileName);
    try
      for I := 1 to APageCount do
      begin
        p.NewPage;
        { Same bitmap, same background, same quality on every call: the first
          call embeds the image, every later call references that copy. }
        p.Graphics.DrawImage(Logo, gcWhite, RectF(40, 30, 160, 70),
          True, True, itOriginal, 0.85);
        p.Graphics.DrawText(Format('Page %d', [I]), PointF(40, 90));
      end;
    finally
      p.EndDocument;
    end;
  finally
    p.Free;
    Logo.Free;
  end;
end;

Image size and encoding

A PDF stores an image at its own pixel size, not at the size of the rectangle you draw it into. Drawing a 12-megapixel camera photo into a 200 × 150 pt thumbnail embeds all 12 million pixels, and scaling happens only in the viewer. The fix belongs before the draw call: resize the bitmap to what the page can show. Page units are points (72 per inch), so 150 dpi — ample for print and screen — means width in points × 150 / 72 pixels.

The library's engine then stores every image as JPEG, using the Quality argument (0.0 to 1.0, default 1.0, the largest setting). Two practical rules follow:

  • Lower Quality for photographs. Values around 0.75–0.85 usually cut photo size substantially with no visible loss at print size. Keep 1.0 for screenshots, diagrams and text-heavy images, where JPEG artefacts show.
  • Transparency is flattened onto the background colour. The overloads without a background colour use white. For a transparent logo on a coloured band, pass the band's colour as ABackgroundColor — and keep it the same on every page so the logo is still stored once.
Downscale a photo to its printed size before drawing itPascal
uses
  FMX.Graphics, FMX.TMSFNCPDFLib, FMX.TMSFNCPDFCoreLibBase, FMX.TMSFNCGraphicsTypes;

procedure TForm1.DrawProductPhoto(APDF: TTMSFNCPDFLib; APhoto: TBitmap; const ARect: TRectF);
const
  TargetDPI = 150; { page units are points: 72 per inch }
var
  Thumb: TBitmap;
begin
  { The PDF stores an image at its own pixel size, whatever rectangle it is
    drawn into. A 4000 x 3000 photo drawn 200 x 150 pt still embeds all
    12 million pixels - shrink it to what the page can show. }
  Thumb := APhoto.CreateThumbnail(Round(ARect.Width * TargetDPI / 72),
    Round(ARect.Height * TargetDPI / 72));
  try
    { Photos tolerate JPEG compression well; 0.8 is a good size/quality balance. }
    APDF.Graphics.DrawImage(Thumb, gcWhite, ARect, True, True, itOriginal, 0.8);
  finally
    Thumb.Free;
  end;
end;

Fonts

Fonts are the second large contributor to file size, and the cost is per font face, not per use. The first time a document draws with a given font name and style, the library measures the font and registers it; later uses — on the same page or a hundred pages later — are served from a per-document cache, so switching between fonts while drawing is cheap. What adds size is the number of distinct faces: with embedding on (the default), every name-and-style combination the document uses adds its own embedded font program. A font registered but never used for a single character is left out of the file.

  • Keep the palette small. Two families with regular and bold is a typical report. Every extra family, and every bold/italic variant of it, is another embedded font.
  • Embed only when the PDF leaves your control. EmbedFonts := False gives the smallest file and the fastest generation, but the document then depends on the font being installed wherever it is opened. See Font embedding for the trade-off, and TMS WEB Core for loading fonts in the browser.
  • Control exports follow the same rule. TTMSFNCGraphicsPDFIO exposes the setting as Options.EmbedFonts.

Compression and the total page count

Page content — the text and drawing operators — is compressed with Flate before it is written, and so are embedded font programs; images are already JPEG. There is one exception worth knowing about. A page whose content contains the total page-count placeholder, PDFPageCountRef, is written uncompressed, because the placeholder can only be replaced with the real total once the last page exists. A "Page N of M" footer on every page therefore leaves every page stream uncompressed, and a text-heavy document comes out noticeably larger.

  • Use PDFPageCountRef where the reader needs the total, and a plain Format('Page %d', [...]) elsewhere.
  • In the browser, compression also requires the Pako library; without it the output is valid but uncompressed. See Compression.

Printing and control export

The print components draw straight onto the printer canvas, so the PDF-side rules above do not apply to them; what matters is how much they have to draw. TTMSFNCRichEditorPrintIO copies every picture in the editor into a temporary bitmap container for each print — set Options.ExportImages := False for fast, text-only drafts. A grid printed through TTMSFNCGridPrintIO with Options.FitToPage is scaled to the page width, font sizes included, so a very wide grid can come out unreadably small; see Printing for the options that split it over pages instead. For quick internal drafts, the export side has the matching switch: TTMSFNCGraphicsPDFIO skips font embedding with Options.EmbedFonts := False.

Fast draft output from the print and export componentsPascal
uses
  FMX.TMSFNCRichEditorPrintIO, FMX.TMSFNCGraphicsPDFEngine;

procedure TForm1.PrintDraftClick(Sender: TObject);
begin
  { Skips copying every editor picture into a temporary container. }
  TMSFNCRichEditorPrintIO1.RichEditor := TMSFNCRichEditor1;
  TMSFNCRichEditorPrintIO1.Options.ExportImages := False;
  TMSFNCRichEditorPrintIO1.Print;
end;

procedure TForm1.ExportDraftClick(Sender: TObject);
begin
  { Smallest and fastest PDF - only for readers that have the fonts installed. }
  TMSFNCGraphicsPDFIO1.Options.EmbedFonts := False;
  TMSFNCGraphicsPDFIO1.ExportObject := TMSFNCChart1;
  TMSFNCGraphicsPDFIO1.Save('chart-draft.pdf');
end;

Putting it together: a lean image-heavy catalog

The catalog below applies every rule in this chapter at once: the logo is decoded once and drawn identically on every page, each product photo is shrunk to 150 dpi at its printed size and stored at JPEG quality 0.8, the text uses a single family in two styles, the page number avoids the total so every page stream stays compressed, and the result is returned as a stream for the caller to store or send.

A lean image-heavy catalogPascal
uses
  System.SysUtils, System.Classes, System.UITypes, FMX.Graphics, FMX.TMSFNCPDFLib, FMX.TMSFNCPDFCoreLibBase,
  FMX.TMSFNCTypes, FMX.TMSFNCGraphicsTypes;

function TForm1.BuildCatalog(APhotos: TArray<TBitmap>; ANames: TArray<string>): TMemoryStream;
const
  TargetDPI = 150;
var
  p: TTMSFNCPDFLib;
  Logo: TTMSFNCBitmap;
  Thumb: TBitmap;
  I: Integer;
  R: TRectF;
begin
  Logo := TTMSFNCBitmap.Create;
  p := TTMSFNCPDFLib.Create;
  try
    Logo.LoadFromFile('logo.png');   { decoded once, embedded once }
    p.BeginDocument;                 { stream output }

    for I := 0 to High(APhotos) do
    begin
      p.NewPage;

      { Same logo, background and quality on every page: one embedded image. }
      p.Graphics.DrawImage(Logo, gcWhite, RectF(40, 30, 160, 70), True, True, itOriginal, 0.9);

      { Photo shrunk to 150 dpi at its printed size, stored at JPEG quality 0.8. }
      R := RectF(40, 100, 340, 325);
      Thumb := APhotos[I].CreateThumbnail(Round(R.Width * TargetDPI / 72),
        Round(R.Height * TargetDPI / 72));
      try
        p.Graphics.DrawImage(Thumb, gcWhite, R, True, True, itOriginal, 0.8);
      finally
        Thumb.Free;
      end;

      { Two faces for the whole document: each extra family or style would add
        another embedded font. }
      p.Graphics.Font.Name := 'Segoe UI';
      p.Graphics.Font.Style := [TFontStyle.fsBold];
      p.Graphics.Font.Size := 16;
      p.Graphics.DrawText(ANames[I], PointF(40, 345));

      { A page number without the total keeps this page's content stream
        compressed; PDFPageCountRef would leave it uncompressed. }
      p.Graphics.Font.Style := [];
      p.Graphics.Font.Size := 9;
      p.Graphics.DrawText(Format('Page %d', [I + 1]), PointF(40, 800));
    end;

    Result := p.EndDocument;         { caller frees }
  finally
    p.Free;
    Logo.Free;
  end;
end;

Common pitfalls

  • Expecting a file name to save memory. The document is built in memory either way; split very large output into several documents instead.
  • Leaking the result stream. EndDocument without a file name returns a stream you must free.
  • Drawing full-resolution photos into small rectangles. The file stores every pixel; downscale first.
  • Varying quality or background for a repeated image. Each combination is a separate embedded copy.
  • Calling DrawImageFromFile in a page loop. It decodes the file on every call; load a bitmap once.
  • "Page N of M" everywhere by habit. Every page that contains the total is written uncompressed.
  • A font per heading level. Distinct faces, not font sizes, drive the embedded font size — vary size and colour, not the family.

See also