Creating a PDF document
Every document made with TTMSFNCPDFLib follows the same skeleton: begin the
document, add at least one page, draw, end the document. Everything else on this
page — page size, metadata, headers, security — is configuration you apply around
that skeleton, and almost all of it must be set before the page it should
affect is created.
This chapter covers the document itself. Drawing on the page is covered in Drawing graphics and Text and fonts.
Starting a new document
Call BeginDocument to start a document, always paired with EndDocument. When
AFileName is empty, EndDocument returns the content as a TMemoryStream
instead of writing to disk — which is what you want when the PDF is going to a
database blob, an HTTP response, or an email attachment. Pass True as the second
parameter to EndDocument to open the generated file in the default PDF viewer.
procedure TForm1.GeneratePDF(AFileName: string);
var
p: TTMSFNCPDFLib;
begin
p := TTMSFNCPDFLib.Create;
try
p.BeginDocument(AFileName);
p.NewPage;
p.EndDocument;
finally
p.Free;
end;
end;
void __fastcall TForm1::GeneratePDF(String AFileName)
{
TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
try
{
p->BeginDocument(AFileName);
p->NewPage();
p->EndDocument();
}
__finally
{
delete p;
}
}
Add the appropriate unit to your uses clause:
uses
FMX.TMSFNCPDFLib; // FMX
VCL.TMSFNCPDFLib; // VCL
WEBLib.TMSFNCPDFLib; // WEB Core
#include <FMX.TMSFNCPDFLib.hpp> // FMX
Key class: TTMSFNCPDFLib
Adding pages
Call NewPage to start a new page. Each NewPage call clears the content buffer
but preserves appearance settings — fill, stroke, font — from the previous
page, so a document-wide look is set once rather than re-applied per page.
EndDocument raises an error if no page was added.
| Call | Purpose |
|---|---|
NewPage |
Appends a page and makes it current |
InsertPage(APageIndex) |
Inserts a page at a specific position |
GetPageCount |
Total number of pages |
GetPageIndex |
Zero-based index of the current page |
Page size and orientation
| Property | Description |
|---|---|
PageSize |
Standard size (A4, Letter, Legal, …) or psCustom |
PageOrientation |
poPortrait or poLandscape |
PageWidth / PageHeight |
Custom size when PageSize = psCustom |
Supported sizes include ISO A0–A8, B0–B10, C2–C6, SRA and RA series, US Letter, Legal, Ledger, Tabloid, Executive, ANSI C–E, and traditional British sizes.
Tip
psCustom is not only for exotic paper. Fitting the page to the content is the
simplest way to produce a PDF with no trailing empty space — a receipt, a label,
or a one-block report. The example on the chapter index uses
a 595 × 440 custom page for exactly that reason.
Metadata
Set Author, Creator, Title, Subject, and Keywords directly on the
TTMSFNCPDFLib instance before BeginDocument — the document information is
written when the document starts, so a value assigned afterwards does not reach
the file. These populate the document properties a PDF reader shows, and Title
is what most readers put in the window caption instead of the file name.
Headers, footers, and page numbering
A header or footer is drawn automatically on every page, so it is configured once on the document rather than redrawn per page.
| Property | Description |
|---|---|
Header / Footer |
Content text (supports HTML) |
HeaderSize / FooterSize |
Height of the area |
HeaderMargins / FooterMargins |
Margins around the area |
HeaderAlignment / FooterAlignment |
Horizontal text alignment |
HeaderFont / FooterFont |
Font for the text |
Use GetHeaderRect and GetFooterRect to retrieve the bounding rectangles — for
example to keep body content clear of them.
Page numbering is separate, because it needs to be resolved after the total page count is known:
| Property | Description |
|---|---|
PageNumber |
pnNone (default), pnHeader, or pnFooter |
PageNumberFormat |
Format string |
PageNumberSize / PageNumberMargins / PageNumberAlignment / PageNumberFont |
Layout and appearance |
GetPageNumberRect retrieves the page number bounding rectangle.
Note
Header and Footer default to a non-empty value. If a generated page shows an
unexpected caption at the top, set Header := '' and Footer := '' explicitly
before BeginDocument.
Security, archival output, and the generation engine
Three document-level choices decide what kind of file comes out of
EndDocument: whether it is password-protected, whether it claims PDF/A
archival conformance, and how its page boxes are laid out for print. All three
are captured when the document or page starts, and the first two depend on which
generation engine the platform uses — so it pays to know that before relying on
either.
| Setting | Type | Default | Applies to |
|---|---|---|---|
OwnerPassword |
String |
empty | Encryption and permission enforcement |
UserPassword |
String |
empty | Password prompt when opening |
AllowsPrinting |
Boolean |
True |
Permission to print |
AllowsCopying |
Boolean |
True |
Permission to copy content |
PDFStandard |
TTMSFNCPDFLibStandard |
pdfNone |
pdfNone for plain PDF, pdfA1 for PDF/A-1 |
These five properties are declared protected on TTMSFNCPDFLib, so
p.OwnerPassword := … on a plain TTMSFNCPDFLib variable does not compile
(E2362 Cannot access protected symbol). Declare a one-line descendant in your
own unit — TSecurePDFLib = class(TTMSFNCPDFLib); — and create that instead; it
behaves identically and makes the properties reachable. Both snippets below use
this pattern.
The component picks its engine per platform, and the engines do not honour these settings equally:
| Platform | Engine | Passwords and permissions | pdfA1 |
|---|---|---|---|
| FMX on Windows and Linux, VCL | General (built-in writer) | Not written — the file is unencrypted | Applied |
| TMS WEB Core | General (built-in writer) | Not written — the file is unencrypted | Raises an exception |
| FMX on macOS and iOS | Native (Core Graphics) | Applied | Ignored — plain PDF |
| FMX on Android | Native (Android) | Not written — the file is unencrypted | Ignored — plain PDF |
Warning
On every platform except macOS and iOS, the password and permission properties are stored but not written: the generated file opens without a prompt and allows printing and copying, and no error is raised. Do not rely on them to protect confidential content there — encrypt the finished file with a dedicated tool, or protect it at the transport or storage level.
Protecting a document with passwords
Protect a document when it leaves your control but its use should still be limited — a salary statement that may be printed but not copied into another file, or a quotation that should only open for the intended recipient.
A PDF distinguishes two passwords. The user password is the one a reader
must type to open the file; leave it empty and the document opens freely. The
owner password unlocks full control, and it is what switches encryption on:
permissions are stored inside the encrypted file, so without an owner password
the user password and both Allows… flags have nothing to attach to and are not
applied. Always set an owner password when you set permissions, and make it
different from the user password — a reader who opens the file with the owner
password bypasses every restriction.
Assign all security properties before BeginDocument; the native engine
hands them to the operating system's PDF writer when the document is created.
type
{ OwnerPassword, UserPassword, AllowsPrinting, AllowsCopying and PDFStandard
are protected on TTMSFNCPDFLib. A descendant declared in your own unit
makes them reachable without changing the library. }
TSecurePDFLib = class(TTMSFNCPDFLib);
procedure TForm1.GenerateProtectedPDF(const AFileName: string);
var
p: TSecurePDFLib;
begin
p := TSecurePDFLib.Create;
try
p.Header := '';
p.Footer := '';
p.Title := 'Salary statement - March 2026';
p.Author := 'Payroll department';
{ Security is written when the document starts, so everything below must
be assigned before BeginDocument. }
p.PDFStandard := pdfNone; { PDF/A forbids encryption }
p.OwnerPassword := 'Payroll#Owner-2026';
p.UserPassword := 'march2026'; { leave empty to open without a prompt }
p.AllowsPrinting := True;
p.AllowsCopying := False;
p.BeginDocument(AFileName);
try
p.NewPage;
p.Graphics.Font.Name := 'Segoe UI';
p.Graphics.Font.Size := 16;
p.Graphics.DrawText('Salary statement', PointF(40, 40));
p.Graphics.Font.Size := 10;
p.Graphics.DrawText('Printing is allowed; copying text is not.',
PointF(40, 70));
finally
p.EndDocument;
end;
finally
p.Free;
end;
end;
// OwnerPassword, UserPassword, AllowsPrinting, AllowsCopying and PDFStandard
// are protected on TTMSFNCPDFLib. A descendant declared in your own unit
// makes them reachable without changing the library.
class TSecurePDFLib : public TTMSFNCPDFLib
{
public:
void __fastcall Protect(String AOwnerPassword, String AUserPassword,
bool AAllowPrinting, bool AAllowCopying)
{
PDFStandard = TTMSFNCPDFLibStandard::pdfNone; // PDF/A forbids encryption
OwnerPassword = AOwnerPassword;
UserPassword = AUserPassword; // leave empty to open without a prompt
AllowsPrinting = AAllowPrinting;
AllowsCopying = AAllowCopying;
}
};
void __fastcall TForm1::GenerateProtectedPDF(const String AFileName)
{
TSecurePDFLib *p = new TSecurePDFLib();
try
{
p->Header = "";
p->Footer = "";
p->Title = "Salary statement - March 2026";
p->Author = "Payroll department";
// Security is written when the document starts, so everything below must
// be assigned before BeginDocument.
p->Protect("Payroll#Owner-2026", "march2026", true, false);
p->BeginDocument(AFileName);
try
{
p->NewPage();
p->Graphics()->Font->Name = "Segoe UI";
p->Graphics()->Font->Size = 16;
p->Graphics()->DrawText("Salary statement", PointF(40, 40));
p->Graphics()->Font->Size = 10;
p->Graphics()->DrawText("Printing is allowed; copying text is not.",
PointF(40, 70));
}
__finally
{
p->EndDocument();
}
}
__finally
{
delete p;
}
}
Permissions are honoured by the PDF reader, not by the file format itself. Mainstream readers respect them, but they restrict ordinary use rather than prevent a determined user from extracting content — treat them as a usage policy, and use the user password when the content itself must stay private.
Producing PDF/A archival documents
Choose PDF/A when the file has to render identically years from now, without the fonts, colour settings or software that produced it — invoices and contracts kept for statutory retention periods, records submitted to public authorities, or any document an archive will only accept as PDF/A.
Setting PDFStandard := pdfA1 makes the general engine write the parts PDF/A-1
requires on top of the normal output: a PDF 1.4 header, an sRGB output intent
with its embedded colour profile, an XMP metadata stream built from Title,
Author, Subject, Keywords and Creator, the marked-content flag, and a
document identifier in the trailer.
type
{ PDFStandard is protected on TTMSFNCPDFLib; a local descendant exposes it. }
TArchivalPDFLib = class(TTMSFNCPDFLib);
procedure TForm1.GenerateArchivalPDF(const AFileName: string);
var
p: TArchivalPDFLib;
begin
p := TArchivalPDFLib.Create;
try
p.Header := '';
p.Footer := '';
p.PageSize := psA4;
{ The archival identification and its metadata are written when the
document starts: select the standard and fill in the metadata first. }
p.PDFStandard := pdfA1;
p.EmbedFonts := True; { PDF/A requires every font embedded }
p.Title := 'Invoice 2026-0412';
p.Author := 'Accounts receivable';
p.Subject := 'Customer invoice, retained for ten years';
p.Keywords.Add('invoice');
p.Keywords.Add('archive');
{ No OwnerPassword or UserPassword: PDF/A does not allow encryption. }
p.BeginDocument(AFileName);
try
p.NewPage;
p.Graphics.Font.Name := 'Segoe UI';
p.Graphics.Font.Size := 18;
p.Graphics.DrawText('Invoice 2026-0412', PointF(40, 40));
p.Graphics.Font.Size := 10;
p.Graphics.DrawText('Total due: 1.250,00', PointF(40, 72));
finally
p.EndDocument;
end;
finally
p.Free;
end;
end;
// PDFStandard is protected on TTMSFNCPDFLib; a local descendant exposes it.
class TArchivalPDFLib : public TTMSFNCPDFLib
{
public:
void __fastcall SetStandard(TTMSFNCPDFLibStandard AStandard) { PDFStandard = AStandard; }
};
void __fastcall TForm1::GenerateArchivalPDF(const String AFileName)
{
TArchivalPDFLib *p = new TArchivalPDFLib();
try
{
p->Header = "";
p->Footer = "";
p->PageSize = TTMSFNCPDFLibPageSize::psA4;
// The archival identification and its metadata are written when the
// document starts: select the standard and fill in the metadata first.
p->SetStandard(TTMSFNCPDFLibStandard::pdfA1);
p->EmbedFonts = true; // PDF/A requires every font embedded
p->Title = "Invoice 2026-0412";
p->Author = "Accounts receivable";
p->Subject = "Customer invoice, retained for ten years";
p->Keywords->Add("invoice");
p->Keywords->Add("archive");
// No OwnerPassword or UserPassword: PDF/A does not allow encryption.
p->BeginDocument(AFileName);
try
{
p->NewPage();
p->Graphics()->Font->Name = "Segoe UI";
p->Graphics()->Font->Size = 18;
p->Graphics()->DrawText("Invoice 2026-0412", PointF(40, 40));
p->Graphics()->Font->Size = 10;
p->Graphics()->DrawText("Total due: 1.250,00", PointF(40, 72));
}
__finally
{
p->EndDocument();
}
}
__finally
{
delete p;
}
}
PDF/A trades flexibility for durability, and the restrictions come with it:
- No encryption. PDF/A forbids it, so leave
OwnerPasswordandUserPasswordempty. A document can be protected or archival, not both — if you need both, keep the PDF/A file as the archived master and hand out a protected copy. - Every font embedded.
EmbedFontsisTrueby default; keep it that way. The standard does not switch it back on for you, so turning it off produces a file that is labelled PDF/A but does not conform. - Metadata first. The XMP stream is written when the document starts, so set
the metadata properties before
BeginDocumentor the archival metadata is empty. - No validation. The library writes the PDF/A identification and structures; it does not inspect what you draw. When conformance is contractual, check the output with a PDF/A validator as part of your release process.
Page boxes for print production
A screen-only PDF needs nothing beyond the page size. A PDF headed for a commercial printer usually does: the printer needs to know where the sheet ends, where it will be cut, and how far artwork may run past the cut so no white edge shows if the blade is slightly off. The five page boxes carry exactly that information.
| Property | Describes |
|---|---|
MediaBox |
The full sheet, including bleed; also the drawing surface |
BleedBox |
How far artwork may extend past the cut |
TrimBox |
The finished size after cutting |
ArtBox |
The area that holds the meaningful content |
CropBox |
The region a viewer displays |
All five are TRectF values in points. Assigning MediaBox also resizes the
drawing surface to match, so graphics can reach into the bleed.
procedure TForm1.GeneratePrintReadyFlyer(const AFileName: string);
var
p: TTMSFNCPDFLib;
Bleed, Safe, W, H: Single;
begin
p := TTMSFNCPDFLib.Create;
try
p.Header := '';
p.Footer := '';
{ Assigning PageSize or PageOrientation resets every box to the media box,
so choose the trim size first and set the boxes afterwards. }
p.PageSize := psA5;
W := p.MediaBox.Width; { finished (trim) width in points }
H := p.MediaBox.Height;
Bleed := MillimeterToPixel(3);
Safe := MillimeterToPixel(8);
{ The sheet is the trim size plus the bleed on every side. }
p.MediaBox := RectF(0, 0, W + 2 * Bleed, H + 2 * Bleed);
p.BleedBox := p.MediaBox;
p.CropBox := p.MediaBox;
p.TrimBox := RectF(Bleed, Bleed, Bleed + W, Bleed + H);
p.ArtBox := RectF(Bleed + Safe, Bleed + Safe,
Bleed + W - Safe, Bleed + H - Safe);
p.BeginDocument(AFileName);
try
p.NewPage;
{ A background that runs off the trim edge into the bleed, so no white
sliver appears if the cut is slightly off. }
p.Graphics.Fill.Kind := gfkSolid;
p.Graphics.Fill.Color := gcSteelblue;
p.Graphics.Stroke.Kind := gskNone;
p.Graphics.DrawRectangle(p.MediaBox);
{ Text stays inside the art box, clear of the cut. }
p.Graphics.Font.Name := 'Segoe UI';
p.Graphics.Font.Color := gcWhite;
p.Graphics.Font.Size := 24;
p.Graphics.DrawText('Open day - 12 April', PointF(p.ArtBox.Left, p.ArtBox.Top));
finally
p.EndDocument;
end;
finally
p.Free;
end;
end;
void __fastcall TForm1::GeneratePrintReadyFlyer(const String AFileName)
{
TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
try
{
p->Header = "";
p->Footer = "";
// Assigning PageSize or PageOrientation resets every box to the media box,
// so choose the trim size first and set the boxes afterwards.
p->PageSize = TTMSFNCPDFLibPageSize::psA5;
float W = p->MediaBox.Width(); // finished (trim) width in points
float H = p->MediaBox.Height();
float Bleed = MillimeterToPixel(3);
float Safe = MillimeterToPixel(8);
// The sheet is the trim size plus the bleed on every side.
p->MediaBox = RectF(0, 0, W + 2 * Bleed, H + 2 * Bleed);
p->BleedBox = p->MediaBox;
p->CropBox = p->MediaBox;
p->TrimBox = RectF(Bleed, Bleed, Bleed + W, Bleed + H);
p->ArtBox = RectF(Bleed + Safe, Bleed + Safe,
Bleed + W - Safe, Bleed + H - Safe);
p->BeginDocument(AFileName);
try
{
p->NewPage();
// A background that runs off the trim edge into the bleed, so no white
// sliver appears if the cut is slightly off.
p->Graphics()->Fill->Kind = TTMSFNCGraphicsFillKind::gfkSolid;
p->Graphics()->Fill->Color = gcSteelblue;
p->Graphics()->Stroke->Kind = TTMSFNCGraphicsStrokeKind::gskNone;
p->Graphics()->DrawRectangle(p->MediaBox);
// Text stays inside the art box, clear of the cut.
p->Graphics()->Font->Name = "Segoe UI";
p->Graphics()->Font->Color = gcWhite;
p->Graphics()->Font->Size = 24;
p->Graphics()->DrawText("Open day - 12 April", PointF(p->ArtBox.Left, p->ArtBox.Top));
}
__finally
{
p->EndDocument();
}
}
__finally
{
delete p;
}
}
Three rules keep the boxes where you intend them:
- Set boxes after the page size. Assigning
PageSize,PageOrientation,PageWidthorPageHeightresets all four inner boxes to the media box. AssigningMediaBoxdoes not, so after enlarging it, set every other box explicitly — a staleCropBoxwould make viewers show only part of the sheet. - Set boxes before the page. Each
NewPagetakes the box values current at that moment, so change them before theNewPagethey should affect. - Keep insets symmetric. Box rectangles are written in PDF page space, whose origin is the bottom-left corner — unlike the top-left drawing coordinates. An inset that is the same on all sides, as in the snippet, lands in the same place in both systems. The general engine also rounds box edges to whole points.
Generation callbacks
The document raises events around the parts it draws for you, so you can replace
the default rendering rather than switch it off and rebuild it by hand. Set
ADefaultDraw := False in a Before… handler to suppress the built-in output and
draw your own.
| Event | Fires |
|---|---|
OnBeforeDrawHeader / OnAfterDrawHeader |
Around the header |
OnBeforeDrawFooter / OnAfterDrawFooter |
Around the footer |
OnBeforeDrawPageNumber / OnAfterDrawPageNumber |
Around the page number |
OnNewPageStarted |
After each NewPage; provides APageIndex |
procedure TForm1.PDFBeforeDrawHeader(Sender: TObject; APageIndex: Integer;
AHeader: UnicodeString; ARect: TRectF;
AGraphics: ITMSFNCCustomPDFGraphicsLib; var ADefaultDraw: Boolean);
begin
ADefaultDraw := False;
AGraphics.Font.Size := 9;
AGraphics.DrawText('Page ' + IntToStr(APageIndex + 1), ARect.TopLeft);
end;
void __fastcall TForm1::PDFBeforeDrawHeader(TObject *Sender, int APageIndex,
String AHeader, const TRectF &ARect,
_di_ITMSFNCCustomPDFGraphicsLib AGraphics, bool &ADefaultDraw)
{
ADefaultDraw = false;
AGraphics->Font->Size = 9;
AGraphics->DrawText("Page " + IntToStr(APageIndex + 1), ARect.TopLeft());
}
Working in millimetres or inches
PDF coordinates are points (1/72 inch) with the origin at the top-left. When your
measurements come from a print spec instead, convert rather than hand-computing.
All functions accept an optional ADPI parameter (default 72.0):
| Function | Description |
|---|---|
InchToPixel(AInch, ADPI) |
Inches → PDF points |
MillimeterToPixel(AMillimeter, ADPI) |
Millimetres → PDF points |
MillimeterRect(AX, AY, AWidth, AHeight, ADPI) |
TRectF from mm coordinates |
InchRect(AX, AY, AWidth, AHeight, ADPI) |
TRectF from inch coordinates |
p.Graphics.DrawRectangle(MillimeterRect(10, 10, 100, 150));
Common mistakes
- Ending a document with no page.
EndDocumentraises an error ifNewPagewas never called. - Setting page size after the page exists.
PageSize,PageWidthandPageHeightapply to pages created afterwards — set them beforeBeginDocumentor before theNewPagethey should affect. - Expecting appearance to reset per page. It does not: fill, stroke and font carry over from the previous page by design.
- Forgetting the default header. See the note above — an unexplained caption
at the top of the page is almost always the default
Header. - Configuring the document after it started. Metadata, passwords,
permissions and
PDFStandardare written byBeginDocument; assignments made afterwards are silently lost. - Assuming a password protects the file everywhere. Only the macOS and iOS engine encrypts. Elsewhere the file is written unprotected without an error — see the platform table above.
- Setting permissions without an owner password. The owner password is what
turns encryption on;
AllowsPrinting := Falseon its own restricts nothing. - Combining PDF/A with a password. PDF/A forbids encryption. Pick one per document.
Putting it together
The report on the chapter index is this chapter's skeleton
with content on top: a custom page fitted to its table, both default captions
cleared, and everything drawn between one BeginDocument/EndDocument pair. It
is the source of the screenshot there, so the code and the picture cannot drift
apart.
procedure TForm1.BuildReport(const AFileName: string);
const
Rows: array[0..7, 0..2] of string = (
('Northern Europe', 'Alice Moreau', '184.000'),
('Southern Europe', 'Bob Devlin', '72.500'),
('North America', 'Carol Yang', '412.800'),
('South America', 'Dan Pereira', '43.000'),
('Middle East', 'Eve Novak', '159.000'),
('South Asia', 'Frank Bauer', '91.000'),
('East Asia', 'Grace Lim', '213.000'),
('Oceania', 'Hugo Marsh', '66.500'));
var
p: TTMSFNCPDFLib;
i: Integer;
y: Single;
Euro: string;
begin
{ Build the euro sign from its code point so the source stays ASCII-safe. }
Euro := Char($20AC);
p := TTMSFNCPDFLib.Create;
try
{ Header/Footer default to a non-empty value - clear them before drawing
your own, or the built-in caption appears at the top of every page. }
p.Header := '';
p.Footer := '';
{ A custom page fitted to the content, so the PDF has no trailing empty
space below the table. }
p.PageSize := psCustom;
p.PageWidth := 595;
p.PageHeight := 440;
p.BeginDocument(AFileName);
try
p.NewPage;
{ Header band: a filled rectangle with no stroke, drawn before the text
that sits on top of it. }
p.Graphics.Fill.Color := gcSteelblue;
p.Graphics.Fill.Kind := gfkSolid;
p.Graphics.Stroke.Kind := gskNone;
p.Graphics.DrawRectangle(RectF(40, 40, 555, 92));
p.Graphics.Font.Color := gcWhite;
p.Graphics.Font.Name := 'Segoe UI';
p.Graphics.Font.Size := 18;
p.Graphics.DrawText('Regional sales report', PointF(54, 55));
p.Graphics.Font.Size := 10;
p.Graphics.DrawText('Fiscal year 2026 - all regions', PointF(54, 76));
{ Mini HTML for a paragraph that mixes weight and colour inline. }
p.Graphics.Font.Color := gcBlack;
p.Graphics.Font.Size := 10;
p.Graphics.DrawHTMLText(
'Revenue by region for the full year. Figures in <b>EUR</b>, ' +
'rounded to the nearest thousand. Regions above ' +
'<font color="#1F7A1F"><b>150.000</b></font> are on track.',
RectF(40, 108, 555, 148));
{ Table header. Alignment is canvas state, so it is set for the currency
column and restored immediately afterwards. }
y := 158;
p.Graphics.Fill.Color := gcGainsboro;
p.Graphics.Stroke.Kind := gskSolid;
p.Graphics.Stroke.Color := gcSilver;
p.Graphics.DrawRectangle(RectF(40, y, 555, y + 24));
p.Graphics.Font.Size := 10;
p.Graphics.DrawText('Region', PointF(50, y + 6));
p.Graphics.DrawText('Account manager', PointF(220, y + 6));
p.Graphics.Alignment := gtaTrailing;
p.Graphics.DrawText('Revenue', RectF(390, y + 6, 545, y + 22));
p.Graphics.Alignment := gtaLeading;
{ Rows, with alternating band fills. }
y := y + 24;
for i := 0 to High(Rows) do
begin
if i mod 2 = 1 then
p.Graphics.Fill.Color := gcWhitesmoke
else
p.Graphics.Fill.Color := gcWhite;
p.Graphics.DrawRectangle(RectF(40, y, 555, y + 22));
p.Graphics.DrawText(Rows[i][0], PointF(50, y + 5));
p.Graphics.DrawText(Rows[i][1], PointF(220, y + 5));
p.Graphics.Alignment := gtaTrailing;
p.Graphics.DrawText(Euro + ' ' + Rows[i][2],
RectF(390, y + 5, 545, y + 21));
p.Graphics.Alignment := gtaLeading;
y := y + 22;
end;
{ Totals row. }
p.Graphics.Fill.Color := gcLightsteelblue;
p.Graphics.DrawRectangle(RectF(40, y, 555, y + 24));
p.Graphics.DrawText('Total', PointF(50, y + 6));
p.Graphics.Alignment := gtaTrailing;
p.Graphics.DrawText(Euro + ' 1.241.800', RectF(390, y + 6, 555, y + 22));
p.Graphics.Alignment := gtaLeading;
{ Footer rule and caption, clear of the table. }
p.Graphics.Stroke.Color := gcSilver;
p.Graphics.DrawLine(PointF(40, 398), PointF(555, 398));
p.Graphics.Font.Size := 8;
p.Graphics.Font.Color := gcGray;
p.Graphics.DrawText('Generated with TMS FNC PDF Library', PointF(40, 406));
p.Graphics.Alignment := gtaTrailing;
p.Graphics.DrawText('Page 1 of 1', RectF(390, 406, 555, 420));
p.Graphics.Alignment := gtaLeading;
finally
p.EndDocument;
end;
finally
p.Free;
end;
end;
void __fastcall TForm1::BuildReport(const String AFileName)
{
const String Rows[8][3] = {
{"Northern Europe", "Alice Moreau", "184.000"},
{"Southern Europe", "Bob Devlin", "72.500"},
{"North America", "Carol Yang", "412.800"},
{"South America", "Dan Pereira", "43.000"},
{"Middle East", "Eve Novak", "159.000"},
{"South Asia", "Frank Bauer", "91.000"},
{"East Asia", "Grace Lim", "213.000"},
{"Oceania", "Hugo Marsh", "66.500"}};
// Build the euro sign from its code point so the source stays ASCII-safe.
String Euro = WideChar(0x20AC);
TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
try
{
// Header/Footer default to a non-empty value - clear them before drawing
// your own, or the built-in caption appears at the top of every page.
p->Header = "";
p->Footer = "";
// A custom page fitted to the content, so the PDF has no trailing empty
// space below the table.
p->PageSize = TTMSFNCPDFLibPageSize::psCustom;
p->PageWidth = 595;
p->PageHeight = 440;
p->BeginDocument(AFileName);
try
{
p->NewPage();
// Header band: a filled rectangle with no stroke, drawn before the text
// that sits on top of it.
p->Graphics()->Fill->Color = gcSteelblue;
p->Graphics()->Fill->Kind = TTMSFNCGraphicsFillKind::gfkSolid;
p->Graphics()->Stroke->Kind = TTMSFNCGraphicsStrokeKind::gskNone;
p->Graphics()->DrawRectangle(RectF(40, 40, 555, 92));
p->Graphics()->Font->Color = gcWhite;
p->Graphics()->Font->Name = "Segoe UI";
p->Graphics()->Font->Size = 18;
p->Graphics()->DrawText("Regional sales report", PointF(54, 55));
p->Graphics()->Font->Size = 10;
p->Graphics()->DrawText("Fiscal year 2026 - all regions", PointF(54, 76));
// Mini HTML for a paragraph that mixes weight and colour inline.
p->Graphics()->Font->Color = gcBlack;
p->Graphics()->Font->Size = 10;
p->Graphics()->DrawHTMLText(
"Revenue by region for the full year. Figures in <b>EUR</b>, "
"rounded to the nearest thousand. Regions above "
"<font color=\"#1F7A1F\"><b>150.000</b></font> are on track.",
RectF(40, 108, 555, 148));
// Table header. Alignment is canvas state, so it is set for the currency
// column and restored immediately afterwards.
float y = 158;
p->Graphics()->Fill->Color = gcGainsboro;
p->Graphics()->Stroke->Kind = TTMSFNCGraphicsStrokeKind::gskSolid;
p->Graphics()->Stroke->Color = gcSilver;
p->Graphics()->DrawRectangle(RectF(40, y, 555, y + 24));
p->Graphics()->Font->Size = 10;
p->Graphics()->DrawText("Region", PointF(50, y + 6));
p->Graphics()->DrawText("Account manager", PointF(220, y + 6));
p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaTrailing;
p->Graphics()->DrawText("Revenue", RectF(390, y + 6, 545, y + 22));
p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaLeading;
// Rows, with alternating band fills.
y = y + 24;
for (int i = 0; i < 8; i++)
{
if (i % 2 == 1)
p->Graphics()->Fill->Color = gcWhitesmoke;
else
p->Graphics()->Fill->Color = gcWhite;
p->Graphics()->DrawRectangle(RectF(40, y, 555, y + 22));
p->Graphics()->DrawText(Rows[i][0], PointF(50, y + 5));
p->Graphics()->DrawText(Rows[i][1], PointF(220, y + 5));
p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaTrailing;
p->Graphics()->DrawText(Euro + " " + Rows[i][2],
RectF(390, y + 5, 545, y + 21));
p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaLeading;
y = y + 22;
}
// Totals row.
p->Graphics()->Fill->Color = gcLightsteelblue;
p->Graphics()->DrawRectangle(RectF(40, y, 555, y + 24));
p->Graphics()->DrawText("Total", PointF(50, y + 6));
p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaTrailing;
p->Graphics()->DrawText(Euro + " 1.241.800", RectF(390, y + 6, 555, y + 22));
p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaLeading;
// Footer rule and caption, clear of the table.
p->Graphics()->Stroke->Color = gcSilver;
p->Graphics()->DrawLine(PointF(40, 398), PointF(555, 398));
p->Graphics()->Font->Size = 8;
p->Graphics()->Font->Color = gcGray;
p->Graphics()->DrawText("Generated with TMS FNC PDF Library", PointF(40, 406));
p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaTrailing;
p->Graphics()->DrawText("Page 1 of 1", RectF(390, 406, 555, 420));
p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaLeading;
}
__finally
{
p->EndDocument();
}
}
__finally
{
delete p;
}
}