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 duringEndDocument, while the final PDF is assembled. EndDocumentis where image-heavy documents spend their time. Image encoding is deferred until the document is finished, so a slowEndDocumenton 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.
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.85on one page and0.9on the next is embedded twice. - Load a repeated image once.
DrawImageFromFilereads and decodes the file on every call, and the comparison then has to rule the copy out again. Load the file into aTTMSFNCBitmaponce and pass that bitmap toDrawImageon every page.DrawImageWithNamewith aBitmapContainerachieves 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.
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
Qualityfor photographs. Values around0.75–0.85usually cut photo size substantially with no visible loss at print size. Keep1.0for 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.
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 := Falsegives 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.
TTMSFNCGraphicsPDFIOexposes the setting asOptions.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
PDFPageCountRefwhere the reader needs the total, and a plainFormat('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.
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.
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.
EndDocumentwithout 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
DrawImageFromFilein 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.