Migrate from FPDF to NextPDF
At a glance
Section titled “At a glance”This guide helps you move an FPDF-based codebase to
NextPDF core. FPDF is one of the most widely deployed legacy PHP Portable
Document Format (PDF) libraries, and its drawing surface — AddPage, SetFont,
Cell, MultiCell, Write, Text, Image, Output driven by a manual x/y
cursor — maps cleanly onto NextPDF’s own cell/text API, because NextPDF’s
low-level drawing methods follow the same FPDF/TCPDF lineage. NextPDF is not
a drop-in FPDF clone, though: it is a modern PDF 2.0 engine with strict types,
font subsetting, signing, PDF/A, and accessibility (tagged PDF). The two real
shifts are the unit model (NextPDF works in PDF points; FPDF defaults to
millimetres) and the output verbs (a typed OutputDestination enum instead
of FPDF’s 'I'/'D'/'F'/'S' characters).
There is no FPDF class shim in core. Rewrite each call site using the
verb mapping. If you want the smallest initial change for a
TCPDF 6.x codebase instead, see the
TCPDF compatibility adapter, which
ships a near-source-compatible drop-in path; FPDF has no such adapter.
Install
Section titled “Install”composer require nextpdf/core:^3Keep setasign/fpdf (or your fpdf/fpdf) installed while you migrate. Remove it
after the final cutover (see safe migration sequence).
Conceptual overview
Section titled “Conceptual overview”FPDF and NextPDF share the same mental model: a document made of pages, a
cursor (the current x/y position), and verbs that draw at or advance that
cursor. SetXY, Cell, Ln, and MultiCell all read and mutate the cursor in
both libraries, so most procedural FPDF code translates line for line.
The differences are deliberate, not accidental:
- Units. FPDF’s constructor (
new FPDF($orientation, $unit, $size)) defaults to millimetres. NextPDF works in PDF points (1 pt = 1/72 in, ISO 32000-2 §7). There is no document-wide unit knob — convert mm to points once (pt = mm * 72 / 25.4). - Y direction stays the same for you. Like FPDF, NextPDF’s user coordinates
put
y = 0at the page top and increase downward, so cursor arithmetic ports directly. NextPDF converts to PDF-native bottom-left origin internally. - Construction is explicit. FPDF folds orientation, unit, and size into the
constructor; NextPDF takes an immutable
NextPDF\Core\Configvalue object (page size, margins, fonts directory) and an explicitaddPage(). - Always Unicode, always subset. FPDF’s core build is Latin-1 and needs the
tFPDF/UTF-8 variant for Unicode. NextPDF is UTF-8 throughout and always
embeds fonts as subset programs (ISO 32000-2 §9). FPDF’s
AddFont/font-metric files have no analogue; register a TrueType/OpenType font directory and select the family by name.
API surface
Section titled “API surface”The core entry points used below are Document::createStandalone(),
Document::addPage(), Document::setFont(), Document::cell(),
Document::multiCell(), Document::text(), Document::write(),
Document::ln(), Document::image(), the cursor accessors (setXY/setX/
setY/getX/getY), Document::output(?string, OutputDestination),
Document::save(string $path): void, Document::getPdfData(): string, and the
NextPDF\Core\Config value object. The full reference for these core drawing,
text, and output methods lives in the core modules
and the reference index, auto-generated from PHPDoc. The
Html module is related reading for HTML-to-PDF, not
the reference for the verbs on this page.
API verb mapping
Section titled “API verb mapping”FPDF’s public method names are long-standing and well known. The NextPDF column below is confirmed against core source signatures (see Evidence / traceability).
| FPDF | NextPDF | Notes |
|---|---|---|
new FPDF($orient, $unit, $size) | Document::createStandalone($config) | Orientation/unit/size constructor args become a NextPDF\Core\Config (pageSize, margins, fontsDirectory). No $unit — work in points. Default createStandalone() page is A4 portrait. |
$pdf->AddPage($orient, $size) | $doc->addPage($size, $orientation) | Direct map. $size is a PageSize value object; $orientation is the Orientation enum (Portrait/Landscape). |
$pdf->SetFont($family, $style, $size) | $doc->setFont($family, $style, $size) | Direct map. $style uses the same ''/'B'/'I'/'BI' (plus 'U' underline) codes. |
$pdf->Cell($w, $h, $txt, $border, $ln, $align, $fill) | $doc->cell($w, $h, $txt, $border, $newLine, $align, $fill) | Direct map. $align is the Alignment enum (Left/Center/Right/Justify); $border accepts bool or an 'LTRB' string; $ln becomes the bool $newLine. |
$pdf->MultiCell($w, $h, $txt, $border, $align, $fill) | $doc->multiCell($w, $h, $txt, $border, $align) | Word-wraps on actual font metrics. No $fill argument; paint a filled rect() first if you need a background. |
$pdf->Write($h, $txt, $link) | $doc->write($h, $txt, $link) | Flowing text from the cursor; $link attaches a URL link annotation. |
$pdf->Text($x, $y, $txt) | $doc->text($x, $y, $txt) | Absolute-position text. Direct map. |
$pdf->Ln($h) | $doc->ln($h) | Line break to the left margin; 0 = default line height. |
$pdf->Image($file, $x, $y, $w, $h) | $doc->image($file, $x, $y, $w, $h) | Direct map; $x/$y/$w/$h are nullable (null = current cursor / intrinsic size). |
$pdf->SetXY($x, $y) / SetX / SetY | $doc->setXY($x, $y) / setX / setY | Direct map. getX()/getY() read the cursor. |
$pdf->SetMargins($l, $t, $r) | $doc->setMargins(new Margin($t, $r, $bottom, $l)) | One Margin value object; constructor order is (top, right, bottom, left) — not FPDF’s (left, top, right). FPDF SetMargins has no bottom argument (its bottom margin comes from SetAutoPageBreak($auto, $margin)), so choose $bottom yourself — commonly equal to the top margin, or pass the auto-page-break margin. |
$pdf->SetAutoPageBreak($auto, $margin) | $doc->setAutoPageBreak($auto, $margin) | Direct map. |
$pdf->SetDrawColor / SetFillColor / SetTextColor | $doc->setDrawColor / setFillColor / setTextColor | RGB (r, g, b), or a single value for grayscale. |
$pdf->Line / Rect / SetLineWidth | $doc->line / rect / setLineWidth | Direct map. rect() takes a style string ('S'/'F'/'DF'). |
$pdf->SetTitle/SetAuthor/SetSubject/SetKeywords/SetCreator | $doc->setTitle/setAuthor/setSubject/setKeywords/setCreator | Direct map. Lands in the ISO 32000-2 §14 information dictionary / Extensible Metadata Platform (XMP). |
$pdf->Output($dest, $name) | $doc->output($name, OutputDestination::…) | FPDF destination chars (I/D/F/S) map to the OutputDestination enum; note the argument order swaps (name first in NextPDF). |
$pdf->Output('S') | $doc->getPdfData() | Returns the PDF bytes. |
$pdf->Output('F', $path) | $doc->save($path) | Writes to a file path. |
$pdf->GetStringWidth($s) | (no public method) | String width is computed internally during cell()/multiCell() wrapping; there is no public per-string measurement verb. Drive wrapping through multiCell() instead of measuring by hand. |
Code sample — Quick start
Section titled “Code sample — Quick start”<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Contracts\Alignment;use NextPDF\Core\Document;
// FPDF:// $pdf = new FPDF(); // mm, A4 portrait// $pdf->AddPage();// $pdf->SetFont('Arial', 'B', 16);// $pdf->Cell(40, 10, 'Invoice');// $pdf->Output('F', 'out.pdf');
// NextPDF — points, default page is A4 portrait:$doc = Document::createStandalone();$doc->setTitle('Invoice');$doc->addPage();$doc->setFont('Helvetica', 'B', 16.0);$doc->cell(113.4, 28.3, 'Invoice', false, true, Alignment::Left); // ~40mm x ~10mm in points$doc->save(__DIR__ . '/out.pdf');
echo "Wrote out.pdf\n";Code sample — Production
Section titled “Code sample — Production”This example aligns with examples/04-text-and-fonts.php. It uses an explicit
page size, margins, a registered fonts directory, and the cursor-driven cell
model that an FPDF codebase already uses.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Contracts\Alignment;use NextPDF\Contracts\OutputDestination;use NextPDF\Core\Config;use NextPDF\Core\Document;use NextPDF\ValueObjects\Margin;use NextPDF\ValueObjects\PageSize;
// Equivalent of: new FPDF('P', 'mm', 'A4') + SetMargins(20, 16, 20)// i.e. FPDF left=20mm, top=16mm, right=20mm. FPDF SetMargins has no bottom// argument, so we pick bottom = top = 16mm. Convert each mm to points// (pt = mm * 72 / 25.4): 16mm = 45.354pt, 20mm = 56.693pt.// Margin constructor order is (top, right, bottom, left) — NOT FPDF's (L, T, R).$config = new Config( pageSize: new PageSize(595.276, 841.890, 'A4'), margins: new Margin(45.354, 56.693, 45.354, 56.693), // top,right,bottom,left in points fontsDirectory: __DIR__ . '/fonts',);
$doc = Document::createStandalone($config);$doc->setTitle('Quarterly Report');$doc->setAuthor('Finance');$doc->addPage();
// SetFont + Cell, the FPDF way — but in points and with a real Unicode font.$doc->setFont('DejaVuSans', 'B', 18.0);$doc->setTextColor(30, 58, 138);$doc->cell(0, 24.0, 'Quarterly Report', false, true, Alignment::Left);
$doc->setFont('DejaVuSans', '', 11.0);$doc->setTextColor(0, 0, 0);$doc->multiCell(0, 16.0, "Body text wraps on real font metrics. Unicode is " . "native, so accented and non-Latin characters need no tFPDF variant — " . "register the family in the fonts directory and select it by name.");
// Equivalent of $pdf->Output('D', 'report.pdf'):$doc->output('report.pdf', OutputDestination::Download);Edge cases & gotchas
Section titled “Edge cases & gotchas”- Units. Every numeric coordinate, width, height, and margin you copy from
FPDF is in millimetres by default. Multiply by
72 / 25.4to get points, once, during the port. Mixing the two silently mis-sizes everything. Output()argument order. FPDF isOutput($dest, $name); NextPDF isoutput($name, $dest). The destination is theOutputDestinationenum, not a character. Prefersave()/getPdfData()for file / string output.SetMarginsorder. FPDF is(left, top, right); the NextPDFMarginvalue object is(top, right, bottom, left). Re-order, do not transcribe.- Fonts. FPDF’s
AddFont()+.phpmetric files have no equivalent. Put the TrueType/OpenType file in the fonts directory and callsetFont()with the family name. Core Base14 names (Helvetica,Times,Courier) resolve without a file; under PDF/A or tagged PDF they are auto-substituted with an embeddable font. GetStringWidth. There is no public string-measurement method. If your FPDF code measures strings to lay out columns by hand, switch that block tomultiCell()(which wraps on metrics) or fixed-widthcell()calls.
Performance
Section titled “Performance”NextPDF emits content in a single streaming pass (architecture decision record
ADR-001); peak memory tracks document size, not a retained object tree. The
budget for this guide’s example is wall_ms: 2000, peak_mb: 128. For long
documents, drive content across addPage() calls — the same loop shape an FPDF
report already uses.
Security notes
Section titled “Security notes”- Metadata.
SetTitle()/SetAuthor()map to typed setters writing the ISO 32000-2 §14 information dictionary / XMP. Never store secrets there. - Image paths.
image()rejects stream-wrapper schemes and embedded NUL bytes before reading. Pass application-controlled paths. - No in-document code. NextPDF executes no in-document scripts; nothing in FPDF changes that.
Conformance
Section titled “Conformance”| Statement | Spec | Clause |
|---|---|---|
| Page format/orientation map to the page boundary box. | ISO 32000-2 | §7 |
| Fonts are written as embedded/subset font programs. | ISO 32000-2 | §9 |
| Title / metadata land in the info dictionary / XMP. | ISO 32000-2 | §14 |
| Lines, rectangles, and images are content-stream painting. | ISO 32000-2 | §8 |
NextPDF produces ISO 32000-2 content; it does not assert visual identity with FPDF. Re-review output whenever you change renderer.
Commercial context
Section titled “Commercial context”Not applicable. NextPDF core covers the FPDF migration path described here.
See also
Section titled “See also”Migration detail (R6 required sections)
Section titled “Migration detail (R6 required sections)”Who this is for
Section titled “Who this is for”Teams running FPDF (or tFPDF) for server-side, procedural PDF generation. If your
code is a sequence of AddPage / SetFont / Cell / MultiCell / Image /
Output calls driven by SetXY and Ln, the verb mapping
covers your whole surface.
In scope: the FPDF drawing verbs, the cursor model, fonts, colors, lines and
rectangles, metadata, and output. Out of scope: FPDF’s AddFont metric-file
tooling and third-party FPDF script-extensions (barcodes, rotation, bookmarks)
— map those to the corresponding NextPDF modules (Barcode, Transforms,
Navigation), which are not covered here.
Compatibility map
Section titled “Compatibility map”Behavioral compatibility, not a drop-in shim: core provides no FPDF class
shim. Rewrite every call site. The verbs line up closely because NextPDF’s
cell/text API shares FPDF/TCPDF lineage, but the unit model, the Output
argument order, and the Margin/enum types differ — so a transcription is wrong,
a translation is right.
Unit and configuration map
Section titled “Unit and configuration map”| FPDF construct | NextPDF | Notes |
|---|---|---|
$unit ('mm' default) | (no equivalent) | Work in PDF points. Convert dimensions with pt = mm * 72 / 25.4 once during the port. |
$orientation ('P'/'L') | Orientation enum on addPage(), or swap PageSize width/height | Landscape = width > height. |
$size ('A4', [w,h]) | Config->pageSize (PageSize value object) | Named formats become explicit point dimensions; PageSize::A4()…A0() and Letter/Legal factories exist. |
SetMargins($l, $t, $r) | Config->margins (Margin VO) | Constructor order (top, right, bottom, left). |
AddFont($family, $style, $file) | fonts directory + setFont() by name | Drop the metric file; place the TTF/OTF in Config->fontsDirectory. |
Font-handling delta
Section titled “Font-handling delta”- Fonts directories. FPDF’s per-font
AddFontregistration collapses to a fonts directory plussetFont()family matching. Start withConfig->fontsDirectory(the default search path); register additional directories viaFontRegistry::addFontDirectory()orDocument::addFontDirectory()when fonts live in more than one place. - Always Unicode. No Latin-1 default and no separate tFPDF build; UTF-8 input is the norm.
- Always subset. NextPDF always subsets embedded fonts (ISO 32000-2 §9); FPDF’s font-embedding choices have no equivalent and are not needed.
- Re-baseline glyphs. Font matching and fallback are engine-specific; an FPDF font alias may need an exact family name. Substitution differences are expected, not defects.
Behavioral differences
Section titled “Behavioral differences”- Unit conversion (mm → pt) — the most common porting mistake; see above.
Outputargument order swaps and the destination becomes an enum.Margin/Alignment/Orientationare typed objects/enums, not characters or positional(l, t, r)triples.- No public
GetStringWidth— drive wrapping throughmultiCell(). - Independent rasterization — line wrap and pagination on dense content can differ; re-baseline visual diffs.
These are documented behavioral differences, not defects in either engine.
Unsupported / no direct equivalent
Section titled “Unsupported / no direct equivalent”- FPDF
$unitselector — not modeled (always points). AddFont()+.php/.zmetric files — replaced by a fonts directory.GetStringWidth()— no public string-measurement verb.- FPDF’s
'I'/'D'/'F'/'S'destination chars — replaced by theOutputDestinationenum +save()/getPdfData().
Code that depends on these does not “migrate” verbatim. Re-express it with the rows above.
Safe migration sequence
Section titled “Safe migration sequence”- Add
nextpdf/corealongside FPDF; keep FPDF installed for now. - Choose one low-risk document. Convert the constructor via the unit map, then port each verb with the verb map. Convert every mm coordinate to points.
- Place the document’s fonts in
Config->fontsDirectoryand select them by family name; drop theAddFontcalls. - Generate both PDFs for the same input and visually diff them. Differences (font substitution, line wrap) are expected for independent engines — accept them per document.
- Replace any
GetStringWidth-based hand-layout withmultiCell()or fixed-widthcell()calls. - Repeat per document, lowest risk first; keep FPDF installed until the last cutover.
- Remove FPDF from
composer.jsonafter the final cutover.
Testing the migration
Section titled “Testing the migration”- Snapshot FPDF output for representative documents before you change code (golden inputs; the bytes will differ).
- For each migrated document, assert acceptance with your own check (visual diff
- text-extraction). NextPDF’s cell/font behavior is exercised by
examples/04-text-and-fonts.phpplus the coretests/Font and text-output suites. Migration acceptance is document-specific and remains your responsibility.
- text-extraction). NextPDF’s cell/font behavior is exercised by
- Add a regression test per migrated document.
Evidence / traceability
Section titled “Evidence / traceability”Every NextPDF behavioral statement on this page is backed by an in-repo source
signature, example, or architecture decision record (ADR), or, for PDF-format
properties, by the ISO 32000-2 clauses in the frontmatter citations: and the
Conformance table. FPDF behavior is asserted only as
“independent engine — expect documented differences”; this page claims no parity
that an in-repo artifact does not prove.
| NextPDF behavioral claim | In-repo evidence (path) |
|---|---|
AddPage maps to addPage(?PageSize, Orientation): static. | src/Core/Concerns/HasPages.php (addPage()). |
SetFont($family, $style, $size) maps to setFont(string, string, float): static; ''/'B'/'I'/'BI'/'U' styles. | src/Core/Concerns/HasTypography.php (setFont()). |
Cell maps to cell($w, $h, $txt, $border, $newLine, $align, $fill): static. | src/Core/Concerns/HasTextOutput.php (cell()). |
MultiCell maps to multiCell($w, $h, $txt, $border, $align): static (metric-based wrap). | src/Core/Concerns/HasTextOutput.php (multiCell(), wrapText()). |
Write/Text/Ln map to write()/text()/ln(). | src/Core/Concerns/HasTextOutput.php (write(), text(), ln()). |
SetXY/SetX/SetY/GetX/GetY map directly; SetMargins takes a Margin VO. | src/Core/Concerns/HasPages.php (setXY(), getX(), setMargins()); src/ValueObjects/Margin.php ((top, right, bottom, left)). |
Image maps to image($file, ?$x, ?$y, ?$w, ?$h): static; rejects scheme/NUL paths. | src/Core/Concerns/HasImages.php (image(), assertImageFilePath()). |
Line/Rect/SetLineWidth/SetDrawColor/SetFillColor/SetTextColor map directly. | src/Core/Concerns/HasDrawing.php (line(), rect(), setLineWidth()); src/Core/Concerns/HasColors.php (setDrawColor(), setFillColor(), setTextColor()). |
createStandalone() default page is A4 portrait (595.276 × 841.890 pt). | src/Core/Document.php (createStandalone()); src/ValueObjects/PageSize.php (A4()). |
Output destination is the OutputDestination enum (Inline/Download/File/String); Output('S') → getPdfData(), Output('F', $p) → save($p). | src/Contracts/OutputDestination.php; src/Core/Concerns/HasOutput.php (output()). |
SetTitle/SetAuthor/… map to typed metadata setters; land in the info dictionary / XMP. | src/Core/Concerns/HasMetadata.php (setTitle(), setAuthor()); ISO 32000-2 §14 (frontmatter citations:). |
| Fonts are always embedded as subset programs. | src/Core/Concerns/HasTypography.php (buildFontData()); ISO 32000-2 §9 (frontmatter citations:). |
| Content is emitted single-pass. | docs/architecture/adr/ADR-001-stream-based-rendering-pipeline.md. |
Rollback
Section titled “Rollback”Both packages stay installed until the final cutover, so per-call-site rollback means reverting that call site to the FPDF path. After the final cutover, rollback means restoring FPDF and the prior code from version control. No data migration is involved.
Performance considerations
Section titled “Performance considerations”See Performance. The single-pass model removes any retained buffer cost. The new per-document cost is eager font resolution (step 3), which is cacheable through the fonts directory.
Common pitfalls
Section titled “Common pitfalls”- Transcribing millimetre coordinates as points without the
* 72 / 25.4conversion. - Leaving
Output()in FPDF’s($dest, $name)order, or passing a character instead of theOutputDestinationenum. - Transcribing
SetMargins($l, $t, $r)straight intoMargin(whose order istop, right, bottom, left). - Expecting
AddFontmetric files to port; place the TTF/OTF in the fonts directory instead. - Reaching for a
GetStringWidthequivalent; usemultiCell()for wrapping. - Expecting byte/pixel-identical output (independent engines — this guide never claims a drop-in or 100% compatibility).