Aller au contenu

Transformer l'espace de coordonnées : rotation, mise à l'échelle, cisaillement et miroir

Transforme l’espace de coordonnées du dessin autour d’un pivot choisi. Ce recipe couvre la rotation, la mise à l’échelle, le cisaillement et le miroir. Chaque transformation est confinée dans un bloc d’état graphique sauvegardé, donc elle n’affecte pas le contenu qui suit. Il reprend examples/21-transforms.php.

Fenêtre de terminal
composer require nextpdf/core:^3

Tu n’as pas besoin d’un package Pro ou Enterprise. L’API de transformation est incluse dans le Core et fonctionne de PHP 8.1 à 8.4.

Le contenu PDF est dessiné dans l’espace utilisateur. Par défaut, l’espace utilisateur a son origine en bas à gauche de la page, et une unité vaut 1/72 de pouce (ISO 32000-2 §8.3.2). Une transformation multiplie la matrice de transformation courante (CTM) par une nouvelle matrice au moyen de l’opérateur cm (§8.3.4). Les transformations se composent par concaténation de matrices ; l’ordre compte donc.

NextPDF expose un système de coordonnées auteur avec origine en haut à gauche. En interne, il le convertit vers l’espace utilisateur natif (origine en bas à gauche) à l’aide de la projection toY() dans les méthodes de transformation. Les positions utilisent les unités de l’espace utilisateur, c’est-à-dire les points PDF, où 1 pt vaut 1/72 de pouce. Pour garder une transformation locale, encadre-la entre startTransform() et stopTransform(). Ces méthodes émettent les opérateurs d’état graphique q (sauvegarde) et Q (restauration) (§8.4.2). Tout ce qui est dessiné entre les deux hérite de la transformation. Tout ce qui vient après stopTransform() revient à la CTM précédente. Chaque appel rotate()/scale()/skewX()/mirrorH() prend un pivot explicite ; la transformation s’ancre donc là où tu l’attends plutôt qu’à l’origine de la page.

La surface de l’API est générée à partir de la PHPDoc. Les points d’entrée essentiels proviennent du trait \NextPDF\Core\Concerns\HasTransforms :

  • Document::startTransform(): static — émet q et ouvre un bloc d’état
  • Document::stopTransform(): static — émet Q et ferme le bloc
  • 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";

Voici le programme autonome que le harnais peut exécuter. Il reprend la section consacrée à la mise à l’échelle de examples/21-transforms.php. Chaque transformation est isolée dans un bloc d’état graphique sauvegardé avec un pivot explicite. L’état de couleur et de trait est réinitialisé à la fin, donc rien ne se propage vers une page suivante.

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

Sortie STDOUT attendue :

Wrote transforms.pdf

L’exemple complet couvre les quatre familles de transformations : rotation, mise à l’échelle, cisaillement et miroir. Lance-le avec php examples/21-transforms.php ; il écrit examples/output/21-transforms.pdf.

  • Apparie toujours le bloc. Chaque startTransform() doit avoir un stopTransform() correspondant. Un nombre déséquilibré de q/Q corrompt l’état graphique pour le reste de la page (ISO 32000-2 §8.4.2). NextPDF suit la profondeur, mais, dans ce recipe, le contrat reste de un pour un.
  • L’ordre n’est pas commutatif. Les transformations se composent par concaténation de matrices, donc rotate() puis scale() n’est pas la même chose que scale() puis rotate(). Applique-les dans un seul bloc, dans l’ordre souhaité.
  • Le pivot par défaut est l’origine. Si tu omets le pivot, la transformation pivote autour de l’origine de la page, pas autour de la forme. Ce n’est généralement pas ce que tu veux ; passe donc le pivot explicitement.
  • L’axe Y est en espace auteur. Le pivot y est la distance depuis le coin supérieur gauche côté auteur, que NextPDF projette vers l’espace utilisateur natif. Mélanger des coordonnées PDF brutes avec l’API auteur produit un résultat inversé.
  • Fuite d’état. La couleur, la police et la largeur de trait définies dans un bloc de transformation persistent après stopTransform(), car, dans cette surface d’API, Q ne restaure que la CTM. Réinitialise ces valeurs explicitement si une section ultérieure ne doit pas en hériter, comme le fait l’exemple de production.

Une transformation représente un seul opérateur cm plus la paire q/Q. Chaque partie n’occupe que quelques octets et n’ajoute aucun coût d’exécution mesurable, donc le recipe reste confortablement dans le budget de 1500 ms / 96 Mo. Le profil de reproductibilité est structurel. La sortie contient un tableau /ID dans le trailer et des métadonnées de création qui ne sont pas stables d’une exécution à l’autre ; tu dois donc les normaliser avant la comparaison. Le flux de transformation lui-même est déterministe.

  • Résidence des données & mesures d’atténuation des PII. Sans objet. Ce recipe dessine des primitives géométriques et de courts libellés. Il ne traite ni donnée externe ni donnée personnelle.
  • Télémétrie sûre & nettoyage des logs. Le recipe écrit une seule ligne de progression fixe. Aucun contenu de document n’est journalisé.
  • Modèle de menace. Sans objet. Il n’y a ni analyse d’entrée, ni cryptographie, ni frontière de confiance. Une transformation se limite à émettre un flux de contenu.
  • Comportement en mode FIPS. Sans objet. Il n’y a aucune opération cryptographique.
ÉnoncéSpécificationClausereference_id
Une transformation concatène une matrice sur la CTM via l’opérateur cm.ISO 32000-2§8.3.4
Les transformations se composent par concaténation de matrices ; l’ordre est significatif.ISO 32000-2§8.3.4
q sauvegarde et Q restaure l’état graphique, ce qui limite la portée de la transformation.ISO 32000-2§8.4.2
L’origine de l’espace utilisateur par défaut est en bas à gauche ; une unité vaut 1/72 de pouce.ISO 32000-2§8.3.2

Ce recipe met en œuvre les clauses citées de l’ISO 32000-2 sur l’état graphique et les transformations. Il n’affirme pas une conformité globale à l’ISO 32000-2 ; les clauses citées sont uniquement celles que ce recipe exerce.