peptolab / php2pdf
Pure-PHP library for precision PDF layout: embedded fonts, templates, images and text that flows through chained boxes.
Requires
- php: ~8.3.0 || ~8.4.0 || ~8.5.0
- ext-mbstring: *
- ext-zlib: *
Requires (Dev)
- peptolab/peptolab-qa-tools: 0.1.x-dev
- phpunit/phpunit: ^11.5 || ^12.0
Suggests
- ext-gd: Lets images in formats other than JPEG and PNG (GIF, WebP, BMP) be placed by transcoding them.
- ext-imagick: Preferred over ext-gd for transcoding other image formats; also keeps ICC profiles.
- ext-intl: Normalises text and finds its script before shaping, as HarfBuzz does; needed for right-to-left text.
- ext-openssl: Imports PDFs encrypted with AES (AES-128 and AES-256).
Provides
None
Conflicts
None
Replaces
None
README
A pure-PHP library for precision PDF layout: embedded fonts, PDF/AI/SVG templates, images, and text that flows through chained boxes across pages. It keeps the parts of PDFlib that matter for design-led documents and leaves out the rest.
Requires PHP 8.3+ with ext-zlib and ext-mbstring. No other runtime dependencies.
Status
This is an early release (0.1): the features listed in the changelog work and are tested, but the API is not settled. Until 1.0, a minor version may change it.
The library is plain PHP, with no extensions beyond zlib and mbstring, so text takes longer than
in a C library. On a 2017 desktop (Core i7-7700K), flowing two columns of text takes about 2.5 ms a
page; justified text, set by the whole-paragraph composer, about 5 ms. A 2,000-row table takes
1 s. That is 1.7 to 2.2 times PDFlib's time for text, and with opcache and its JIT enabled
(opcache.jit_buffer_size) long documents take about a third less, 1.2 to 1.4 times PDFlib's.
Tables and images are faster than in PDFlib: the 2,000-row table takes a quarter of its time and a
third of its memory. Files are smaller than PDFlib's: 500 pages of text come to 570 KiB against
its 609 KiB. tools/bench runs the scenarios these numbers come from.
Known gaps are listed in Limitations.
Documentation
The guides in docs/ cover installation, documents and pages, the canvas,
fonts and text, images, the fit engine, templates and PDF import, SVG, Textflow, tables, links
and PDF/X-4, a cookbook, limitations and development. Moving from PDFlib? See
Coming from PDFlib.
Quick look
use Php2Pdf\Color\Rgb; use Php2Pdf\Document\Document; use Php2Pdf\Document\PageSize; use Php2Pdf\Geometry\Box; $doc = new Document('out.pdf'); $doc->info()->setTitle('Hello'); $page = $doc->addPage(PageSize::A4); $page->setTrimBox(Box::fromMm(10, 10, 190, 277)); $page->canvas() ->setFillColor(Rgb::fromHex('#0088b8')) ->roundedRect(72, 72, 200, 100, 12) ->fill(); $doc->close();
Units are points (1/72 in). Unit::mm(), Unit::in() and Box::fromMm() convert at the API
boundary. The origin is bottom-left as in PDF; pass topDown: true to addPage() (or set it in
DocumentOptions) for a top-left origin with y growing downwards, as in design tools.
SVG
$doc->svg('logo.svg') converts an SVG to reusable artwork in pure PHP: paths and shapes,
transforms, viewBox and nested viewports, fills and strokes with opacity, linear and radial
gradients, patterns, clip paths, masks, group opacity, style sheets with simple selectors,
<use>/<symbol>, markers, embedded PNG/JPEG images, and text set through the font engine so
it stays searchable. Filters are skipped and reported by NativeConverter::warnings(). When
full fidelity matters and rsvg-convert is installed, pass new RsvgConverter() instead: the
SVG is converted by librsvg and imported through the PDF importer.
$art = $doc->svg('artwork.svg', new NativeConverter(fallbackFont: $sans)); $page->place($art, Box::fromMm(20, 200, 80, 60), Fit::meet());
Tables
Tables flow through boxes like Textflows: columns are fixed, percentages or sized from their
content; cells hold text, markup, Textflows or anything placeable, with column and row spans,
padding, borders and backgrounds; header rows repeat in every box; grouped rows stay together;
and a row too tall for the space left continues its text in the next box. Document::flow()
drains any flowable across pages, making each page through a closure.
$table = $doc->table([Column::mm(55), Column::auto(), Column::mm(20, new CellStyle(alignment: Alignment::Right))], $base, 'body'); $table->addHeaderRow(['Reference', 'Project', 'Price'], new CellStyle(borderBottom: new Border(0.25))); foreach ($entries as $entry) { $table->addRow([$entry->reference, $entry->name, sprintf('$%0.2f', $entry->price)]); } $doc->flow($table, Box::fromMm(15, 48, 180, 176), function (Document $doc, int $n) use ($template): Page { $page = $doc->addPage(PageSize::A4); $page->place($template, $page->mediaBox(), Fit::entire()); return $page; });
Links, bookmarks, PDF/X-4 and PDF/A
$page->addLink($box, 'https://…') or ->addLink($box, Destination::page(3)) makes an area a
link; $doc->addDestination('chapter-2', Destination::top(5, 700)) names a place; and
$doc->outline()->add('Chapter 2', 5)->add('Section', Destination::at(5, 0, 400)) builds the
bookmarks panel.
new DocumentOptions(pdfx: PdfX::X4, rgbProfile: $srgb) writes PDF/X-4: PDF 1.6, trim boxes on
every page, a DefaultRGB profile so RGB colours and images are device-independent, output
intent and conformance declarations in the Info dictionary and XMP, and transparency groups in
the output intent's colour space. The document refuses device colours the output intent cannot reproduce, imported pages that draw
in them, links inside the printed area, badly nested page boxes, and closing without setOutputIntent() or a title. The
structure follows ISO 15930-7; check the output with Acrobat Preflight or callas pdfToolbox for a
formal check.
new DocumentOptions(pdfa: PdfA::A2b) writes PDF/A-2b, and PdfA::A3b PDF/A-3b, alone or with
PDF/X-4; the output passes veraPDF. PDF/A-3 documents take attachments, and attachFacturX()
makes a Factur-X (ZUGFeRD 2) e-invoice of one.
Design notes
- Deterministic output. Inject a
FixedClockthroughDocumentOptionsand identical input produces byte-identical PDFs, so tests compare against golden files. - Streaming. Each page is written as soon as the next one starts. Fonts are subset and written when the document closes.
- Validation. Integration tests run
qpdf --check, read geometry back withpdfinfo, and comparepdftoppmrenders against baselines.
Development
Install Mago once per machine, then:
composer install composer check # mago format --check, mago lint, mago analyze, unit tests composer test-integration # needs qpdf and poppler-utils
QA configuration follows peptolab/peptolab-qa-tools; see Development.
Licence
MIT.