Ir al contenido

Transformar el espacio de coordenadas: rotar, escalar, sesgar y reflejar

Esta receta muestra cómo transformar el espacio de coordenadas de dibujo en torno a un pivote elegido. Cubre la rotación, el escalado, el sesgo y el reflejo. Cada transformación queda aislada en un bloque de estado gráfico guardado, así que no afecta al contenido posterior. Sigue a examples/21-transforms.php.

Ventana de terminal
composer require nextpdf/core:^3

No se necesita un paquete Pro ni Enterprise. La API de transformación viene con Core y funciona de PHP 8.1 a 8.4.

El contenido PDF se dibuja en el espacio de usuario. De forma predeterminada, el espacio de usuario tiene su origen en la esquina inferior izquierda de la página, y una unidad equivale a 1/72 de pulgada (ISO 32000-2 §8.3.2). Una transformación multiplica la matriz de transformación actual (CTM) por una matriz nueva mediante el operador cm (§8.3.4). Las transformaciones se componen por concatenación de matrices, por lo que el orden importa.

NextPDF expone un sistema de coordenadas de autor con origen en la esquina superior izquierda. Internamente, lo convierte al espacio de usuario nativo, con origen en la esquina inferior izquierda, mediante la proyección toY() en los métodos de transformación. Las posiciones se expresan en unidades del espacio de usuario, es decir, en puntos PDF, donde 1 pt equivale a 1/72 de pulgada. Para mantener una transformación local, hay que envolverla entre startTransform() y stopTransform(). Estos métodos emiten los operadores de estado gráfico q (guardar) y Q (restaurar) (§8.4.2). Todo lo que se dibuja entre ellos hereda la transformación. Todo lo que viene después de stopTransform() vuelve a la CTM anterior. Cada llamada a rotate()/scale()/skewX()/mirrorH() toma un pivote explícito, de modo que la transformación se ancla donde se espera, no en el origen de la página.

La superficie de la API se genera a partir de PHPDoc. Los puntos de entrada principales provienen del trait \NextPDF\Core\Concerns\HasTransforms:

  • Document::startTransform(): static — emite q, abre un bloque de estado
  • Document::stopTransform(): static — emite Q, cierra el bloque
  • Document::rotate(float $angle, float $x = 0, float $y = 0): static
  • Document::scale(float $sx, float $sy, float $x = 0, float $y = 0): static
  • Document::skewX(float $angle, float $x = 0, float $y = 0): static / skewY(...)
  • Document::mirrorH(float $x = 0): static / mirrorV(float $y = 0): static
  • Document::translateCtm(float $dx, float $dy): static
<?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";

Este programa es autónomo y ejecutable por el harness. Refleja la sección de escalado de examples/21-transforms.php. Cada transformación queda aislada en un bloque de estado gráfico guardado con un pivote explícito. El color y el estado de línea se restablecen al final, así que nada se filtra a una página posterior.

<?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";

STDOUT esperado:

Wrote transforms.pdf

El ejemplo completo cubre las cuatro familias de transformación: rotación, escalado, sesgo y reflejo. Se ejecuta con php examples/21-transforms.php, que escribe examples/output/21-transforms.pdf.

  • Emparejar siempre el bloque. Cada startTransform() necesita un stopTransform() que lo acompañe. Un recuento desequilibrado de q/Q corrompe el estado gráfico durante el resto de la página (ISO 32000-2 §8.4.2). NextPDF lleva el seguimiento de la profundidad, pero el contrato de la receta es uno a uno.
  • El orden no es conmutativo. Las transformaciones se componen por concatenación de matrices, así que rotate() seguido de scale() no es lo mismo que scale() seguido de rotate(). Deben aplicarse dentro de un mismo bloque en el orden previsto.
  • El pivote es el origen de forma predeterminada. Si se omite el pivote, la transformación gira en torno al origen de la página, no a la figura. Eso normalmente no es lo deseado, así que conviene pasar el pivote de forma explícita.
  • El eje Y pertenece al espacio de autor. El pivote y es la distancia desde la esquina superior izquierda de autor, que NextPDF proyecta al espacio de usuario nativo. Mezclar coordenadas PDF en crudo con la API de autor produce un resultado reflejado.
  • Fuga de estado. El color, la fuente y el grosor de línea definidos dentro de un bloque de transformación persisten después de stopTransform(), porque en esta superficie de API Q restaura solo la CTM. Hay que restablecer estos valores de forma explícita si una sección posterior no debe heredarlos, como hace el ejemplo de producción.

Una transformación es un único operador cm más el par q/Q. Cada parte ocupa unos pocos bytes y no añade ningún costo de ejecución medible, así que la receta se mantiene cómodamente dentro del presupuesto de 1500 ms / 96 MB. El perfil de reproducibilidad es estructural. La salida contiene un arreglo /ID en el tráiler y metadatos de creación que no son estables entre ejecuciones, así que hay que normalizarlos antes de comparar. El flujo de transformación en sí es determinista.

  • Residencia de datos y mitigaciones de PII. No aplica. Esta receta dibuja primitivas geométricas y etiquetas cortas. No procesa ningún dato externo ni personal.
  • Telemetría segura y depuración de registros. La receta escribe una única línea de progreso fija. No se registra ningún contenido del documento.
  • Modelo de amenazas. No aplica. No hay análisis de entrada, ni criptografía, ni límite de confianza. Una transformación es una emisión directa al flujo de contenido.
  • Comportamiento en modo FIPS. No aplica. No hay ninguna operación criptográfica.
DeclaraciónEspecificaciónCláusulareference_id
Una transformación concatena una matriz sobre la CTM mediante el operador cm.ISO 32000-2§8.3.4
Las transformaciones se componen por concatenación de matrices; el orden es significativo.ISO 32000-2§8.3.4
q guarda y Q restaura el estado gráfico, acotando la transformación.ISO 32000-2§8.4.2
El origen predeterminado del espacio de usuario está en la esquina inferior izquierda; una unidad es 1/72 de pulgada.ISO 32000-2§8.3.2

Esta receta implementa las cláusulas de estado gráfico y transformación de ISO 32000-2 que se citan. No afirma conformidad general con ISO 32000-2; las cláusulas citadas son solo las que esta receta ejercita.