Transform the coordinate space: rotate, scale, skew and mirror
At a glance
Section titled “At a glance”Transform the drawing coordinate space around a pivot you choose. This recipe
covers rotation, scaling, skew, and mirroring. Each transform stays isolated
in a saved graphics-state block, so it does not affect later content. It
follows examples/21-transforms.php.
Install
Section titled “Install”composer require nextpdf/core:^3You do not need a Pro or Enterprise package. The transform application programming interface (API) ships with Core and runs on PHP 8.1 through 8.4.
Conceptual overview
Section titled “Conceptual overview”Portable Document Format (PDF) content is drawn in user space. By
default, user space has its origin at the lower-left of the page, and one
unit equals 1/72 inch (ISO 32000-2 §8.3.2). A transform multiplies the
current transformation matrix (CTM) by a new matrix through the cm
operator (§8.3.4). Transforms compose by matrix concatenation, so order
matters.
NextPDF lets you work in a top-left author coordinate system. Internally, it
converts that system to native lower-left user space through the toY()
projection in the transform methods. Positions use user-space units: PDF
points, where 1 pt equals 1/72 in. To keep a transform local, wrap it
between startTransform() and stopTransform(). These methods emit the
q (save) and Q (restore) graphics-state operators (§8.4.2). Everything
drawn between them inherits the transform. Everything after
stopTransform() returns to the prior CTM. Each
rotate()/scale()/skewX()/mirrorH() call takes an explicit pivot, so
the transform anchors where you expect instead of at the page origin.
API surface
Section titled “API surface”The API surface is generated from PHPDoc. The main entry points
come from the \NextPDF\Core\Concerns\HasTransforms trait:
Document::startTransform(): static— emitsqand opens a state blockDocument::stopTransform(): static— emitsQand closes the blockDocument::rotate(float $angle, float $x = 0, float $y = 0): staticDocument::scale(float $sx, float $sy, float $x = 0, float $y = 0): staticDocument::skewX(float $angle, float $x = 0, float $y = 0): static/skewY(...)Document::mirrorH(float $x = 0): static/mirrorV(float $y = 0): staticDocument::translateCtm(float $dx, float $dy): static
Code sample — Quick start
Section titled “Code sample — Quick start”<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();$doc->setTitle('Coordinate Transforms');$doc->addPage();
$cx = 60.0;$cy = 60.0;
// Rotate 30° around (cx, cy). The transform is scoped to this block.$doc->startTransform();$doc->rotate(30, $cx, $cy);$doc->setFont('helvetica', '', 14);$doc->text($cx, $cy, 'Rotated 30 degrees');$doc->stopTransform();
// Back to the untransformed CTM — this text is upright.$doc->setFont('helvetica', '', 10);$doc->text($cx, $cy + 20, 'Not rotated');
$doc->save(__DIR__ . '/transforms.pdf');
echo "Created: transforms.pdf\n";Code sample — Production
Section titled “Code sample — Production”This self-contained program runs under the cookbook harness. It mirrors the
scaling section of examples/21-transforms.php.
Each transform stays in a saved graphics-state block with an explicit pivot.
Color and line state are reset at the end, so nothing leaks into a later
page.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();$doc->setTitle('Coordinate Transforms');$doc->addPage();
$doc->setFont('helvetica', 'B', 13);$doc->cell(0, 8, 'Scaling a reference square at 0.5x, 1.0x, 1.5x, 2.0x', newLine: true);$doc->ln(6);
$scaleBaseY = $doc->getY();$scaleFactors = [0.5, 1.0, 1.5, 2.0];
$doc->setDrawColor(30, 58, 138);$doc->setLineWidth(0.4);
foreach ($scaleFactors as $idx => $factor) { $cx = 25.0 + $idx * 45; $cy = $scaleBaseY + 5;
$doc->startTransform(); $doc->scale($factor, $factor, $cx, $cy); // scale about (cx, cy)
$doc->setFillColor(220, 230, 241); $doc->rect($cx, $cy, 15, 15, 'DF'); $doc->line($cx, $cy, $cx + 15, $cy + 15);
$doc->stopTransform(); // CTM restored here
// Drawn AFTER the block — at the original scale, untransformed. $doc->setFont('helvetica', '', 8); $doc->setTextColor(0); $doc->text($cx, $scaleBaseY + 38, sprintf('%.1fx', $factor));}
// Explicit state reset so nothing carries into the next section.$doc->setTextColor(0);$doc->setFillColor(255);$doc->setDrawColor(0);
// The harness sets NEXTPDF_COOKBOOK_OUTPUT and runs this script twice under// the structural profile (the transform stream itself is deterministic).$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');$doc->save($out !== false && $out !== '' ? $out : __DIR__ . '/transforms.pdf');
echo "Wrote transforms.pdf\n";Expected STDOUT:
Wrote transforms.pdfThe full example covers all four transform families: rotation, scaling,
skew, and mirror. Run it with php examples/21-transforms.php; it writes
examples/output/21-transforms.pdf.
Edge cases & gotchas
Section titled “Edge cases & gotchas”- Always pair the block. Every
startTransform()must have a matchingstopTransform(). An unbalancedq/Qcount corrupts the graphics state for the rest of the page (ISO 32000-2 §8.4.2). NextPDF tracks the depth, but the recipe-level contract remains one-to-one. - Order is not commutative. Transforms compose by matrix
concatenation, so
rotate()thenscale()is not the same asscale()thenrotate(). Apply them inside one block in the order you intend. - Pivot defaults to the origin. If you omit the pivot, the transform pivots around the page origin, not the shape. That is usually not what you want, so pass the pivot explicitly.
- Y axis is author-space. The pivot
yis the distance from the author top-left, and NextPDF projects it to native user space. Mixing raw PDF coordinates with the author API produces a mirrored result. - State leak. Color, font, and line width set inside a transform block
persist after
stopTransform(), because in this API surfaceQrestores only the CTM. Reset these values explicitly if a later section must not inherit them, as the production sample does.
Performance
Section titled “Performance”A transform emits one cm operator plus the q/Q pair. Each part is only
a few bytes and adds no measurable runtime cost, so the recipe stays within
the 1500 ms / 96 MB budget. The reproducibility profile is structural.
The output contains a trailer /ID array and creation metadata that are not
stable across runs, so you must normalize them before comparison. The
transform stream itself is deterministic.
Security notes
Section titled “Security notes”- Data Residency & Personally Identifiable Information (PII) Mitigations. Not applicable. This recipe draws geometric primitives and short labels. It processes no external or personal data.
- Safe Telemetry & Log Scrubbing. The recipe writes one fixed progress line. It logs no document content.
- Threat model. Not applicable. There is no input parsing, no cryptography, and no trust boundary. A transform is a pure content-stream emission.
- Federal Information Processing Standards (FIPS)-mode behavior. Not applicable. There is no cryptographic operation.
Conformance
Section titled “Conformance”| Statement | Spec | Clause | reference_id |
|---|---|---|---|
A transform concatenates a matrix onto the CTM with the cm operator. | ISO 32000-2 | §8.3.4 | |
| Transforms compose by matrix concatenation, and order is significant. | ISO 32000-2 | §8.3.4 | |
q saves and Q restores the graphics state, which scopes the transform. | ISO 32000-2 | §8.4.2 | |
| Default user space origin is lower-left; one unit is 1/72 inch. | ISO 32000-2 | §8.3.2 |
This recipe follows the cited ISO 32000-2 graphics-state and transformation clauses. It does not assert blanket ISO 32000-2 conformance; the cited clauses are the only ones this recipe exercises.