tecnickcom/tc-lib-pdf-page
Manages page sizes, orientation, and box definitions (MediaBox, CropBox, TrimBox) for PDF documents. Part of the tc-lib-pdf/TCPDF ecosystem, it provides deterministic page geometry and unit conversion helpers for multi-page layouts.
tc-lib-pdf-page is a geometry model, not a renderer. It answers one question well: "what rectangle is this page, and what are its boxes?" Reach for it directly only when you are writing a composition layer or doing layout math; the other ~95% of the time you will meet it transitively through tecnickcom/tc-lib-pdf, which owns a Page instance internally and calls it for you.
Prerequisites — PHP 8.2+, ext-zlib, Composer:
composer require tecnickcom/tc-lib-pdf-page
If you are using the full stack, install tecnickcom/tc-lib-pdf instead and treat this as an implementation detail you read but rarely instantiate.
Where to look first, in this order:
vendor/tecnickcom/tc-lib-pdf-page/src/ — the source is small; read Page.php top to bottom once. It is faster than any docs.vendor/tecnickcom/tc-lib-pdf/src/ — grep for ->setBox( and Page( to see how the parent library actually drives this class. That is your real usage manual.First use case — a custom label/receipt format with a print bleed, which is exactly the sort of thing standard A4/Letter defaults cannot express:
<?php
require __DIR__ . '/vendor/autoload.php';
use Com\Tecnick\Pdf\Page\Page;
use Com\Tecnick\Color\Pdf as ColorPdf;
use Com\Tecnick\Pdf\Encrypt\Encrypt;
// unit, color, encrypt, pdfa, cache — all five are required
$page = new Page('mm', new ColorPdf(), new Encrypt(false), false, false);
// 100x150 mm label
$boxes = $page->setBox([], 'MediaBox', 0, 0, 100, 150);
// 3 mm bleed on a 94x144 mm trim
$boxes = $page->setBox($boxes, 'BleedBox', 0, 0, 100, 150);
$boxes = $page->setBox($boxes, 'TrimBox', 3, 3, 94, 144);
$boxes = $page->setBox($boxes, 'CropBox', 3, 3, 94, 144);
var_dump($boxes['TrimBox']);
Note two things immediately: boxes are threaded through an array you pass in and get back, and the constructor demands collaborators you may never use. Both are covered under Gotchas.
The most common failure mode in PDF code is geometry constants duplicated in three places: the template, the renderer, and the test. Wrap the page model in your own small service and make everything else ask it:
final class DocumentGeometry
{
public function __construct(private Page $page, private string $unit = 'mm') {}
/** @return array<string, array{x: float, y: float, w: float, h: float}> */
public function boxesFor(string $format, string $orientation = 'P'): array
{
[$w, $h] = $this->dimensions($format, $orientation);
$boxes = $this->page->setBox([], 'MediaBox', 0, 0, $w, $h);
return $this->page->setBox($boxes, 'CropBox', 0, 0, $w, $h);
}
}
Downstream code (image scaling, footer placement, column math) then consumes boxesFor() rather than hardcoding 210/297.
For anything going to a printer, set boxes in nesting order — outer to inner — and keep them consistent:
| Box | Purpose | Typical value |
|---|---|---|
MediaBox |
Physical medium | trim + 2×bleed |
BleedBox |
Area that may be trimmed off | = MediaBox or trim + bleed |
TrimBox |
Finished cut size | the actual product |
CropBox |
What a viewer displays | usually = TrimBox |
ArtBox |
Intended content area | inside the trim |
A TrimBox larger than the MediaBox, or a negative origin, is a you bug — the library will happily store it.
Fold-outs and inserts (an A3 landscape plan stapled into an A4 portrait report) are the reason this package exists as a separate unit. Set the format per page at the document layer; do not attempt to mutate one page object back and forth mid-render — you will lose the geometry of pages already emitted.
$pdf->setPageFormat('A4', 'P');
$pdf->addPage();
// ...
$pdf->setPageFormat('A3', 'L'); // fold-out
$pdf->addPage();
$pdf->setPageFormat('A4', 'P'); // back to normal
Pick one internal unit (mm is the sane default for print) and convert exactly once, on the way into the model. Mixed mm/pt in one document produces layouts that are subtly wrong rather than obviously broken — the worst kind of bug. Do all arithmetic in your chosen unit; let the library convert to PDF points when the page object is serialised (1 pt = 25.4/72 mm).
This is the underrated use: because the class is pure geometry, you can assert page boxes in a fast unit test with no PDF output, no font loading, no temp files:
$boxes = $geometry->boxesFor('custom-100x150', 'L');
self::assertSame(150.0, $boxes['MediaBox']['w']);
self::assertSame(100.0, $boxes['MediaBox']['h']);
Pair with an integration test that renders once and checks the page count and /MediaBox with qpdf --show-pages or pdfinfo.
If you are already on tc-lib-pdf, do not create a parallel Page instance for the same document. Two models, two truths, and the renderer will win. Instead, configure geometry via the parent library's setPageFormat() / page-box setters, and use this package only in code paths that never touch rendering.
The array API is functional, not mutating. setBox() takes the existing boxes array as its first argument and returns the updated set. If you write $page->setBox([], 'TrimBox', ...); and then read $page->getBoxes(), you may get a stale or empty result. Always chain the return value:
$boxes = $page->setBox($boxes, 'TrimBox', 3, 3, 94, 144); // correct
$page->setBox($boxes, 'TrimBox', 3, 3, 94, 144); // discards the result
The constructor's collaborators are mandatory. Color\Pdf and Encrypt are required even for pure-geometry use, and Encrypt(false) means "encryption disabled". Stub them (new Encrypt(false)) and move on — but be aware this makes the class awkward to instantiate in isolation, and it is a genuine argument for wrapping it.
Verify signatures with Reflection before trusting a blog post or an LLM. The package is pre-1.0-flavoured SemVer with frequent patch releases (4.16.x ships regularly), and the README shows a single call, not a full signature. One line beats a debugging session:
php -r 'require "vendor/autoload.php";
(new ReflectionMethod(Com\Tecnick\Pdf\Page\Page::class, "setBox"))->getParameters()
|> array_map(fn($p) => print($p->getName()." "), ...);'
Or just open src/Page.php — it is a few hundred lines.
PDF origin is bottom-left, y grows upward. Every layout bug where a footer lands at the top or a stamp is mirrored traces back to this. Your mental model is almost certainly top-left (CSS); the model's is not. Decide once where the flip happens and document it.
Landscape is not a width/height swap you do yourself. Orientation is a property of the page, not of your arithmetic. Swapping $w and $h manually and setting 'L' gives you a double rotation and a page twice as wide as it should be.
Errors are typed exceptions. Invalid units, non-positive dimensions, and unknown box names throw rather than silently coercing. Catch the package's own exception classes (under \Com\Tecnick\Pdf\Page\Exception\) rather than \Throwable, so you can distinguish "bad input" from "bug in my code". Validate user-supplied paper sizes at the boundary and convert to a friendly error there.
Licensing needs a look before you ship. The README states GNU LGPL v3, while composer.json reports NOASSERTION — the metadata and the licence file disagree. LGPL has real obligations for static linking and for modifications to the library itself. Wrapping it (see pattern 1) keeps your code cleanly separated, which is the right architecture and the right licensing posture. Get it reviewed if you are shipping closed-source.
Ecosystem signal, not package popularity. Seven stars and zero direct dependents understate it: this is a component of the tc-lib-pdf/TCPDF stack (100M+ installs), so bugs you find are likely already found and fixed upstream. Pin the version, follow the release notes, and treat "is this maintained?" as answered — the question is "is this documented?", and the answer is no.
Debugging geometry: diff against a reference PDF. When a printer rejects a file, dump every box and compare with qpdf --show-object=N file.pdf or pdfinfo -box. Nine times out of ten the TrimBox/BleedBox are inverted or missing, not the page size.
Extension points: wrap, don't subclass. Page is a concrete class with a heavy constructor and no interface. A decorator around it gives you the testability the class itself lacks; subclassing chains you to its internals across patch releases.
Scope discipline. This package does not do margins, headers, content areas, page numbers, or rotation of content. If you find yourself looking for those here, you want tc-lib-pdf (or tc-lib-pdf-graph), not this.
How can I help you explore Laravel packages today?