跳转到内容

变换坐标空间:旋转、缩放、倾斜与镜像

围绕选定的枢轴点变换绘图坐标空间。这则 recipe(示例)涵盖旋转、缩放、倾斜与镜像。每个变换都隔离在已保存的图形状态区块中,因此不会影响后续内容。它遵循 examples/21-transforms.php 的做法。

Terminal window
composer require nextpdf/core:^3

你不需要安装 Pro 或 Enterprise 软件包。变换 API 随 Core 一起提供,可在 PHP 8.1 到 8.4 上运行。

PDF 内容绘制在 用户空间(user space) 中。默认情况下,用户空间的原点位于页面左下角,一个单位等于 1/72 英寸(ISO 32000-2 §8.3.2)。变换会通过 cm 运算符,将当前变换矩阵(CTM)与一个新矩阵相乘(§8.3.4)。多个变换以矩阵串联的方式组合,因此顺序很重要。

NextPDF 对外提供一套以左上角为原点的作者坐标系统。在内部,它会在各个变换方法中通过 toY() 投影,将其换算为原生的左下角用户空间。位置使用用户空间单位,也就是 PDF 点(point),其中 1 pt 等于 1/72 英寸。要把变换限制在局部范围内,请将其包在 startTransform()stopTransform() 之间。这些方法会发出 q(保存)与 Q(还原)两个图形状态运算符(§8.4.2)。在这两者之间绘制的所有内容都会继承该变换。stopTransform() 之后的所有内容都会回到先前的 CTM。每次调用 rotate()/scale()/skewX()/mirrorH() 时,都要传入明确的枢轴点,这样变换才会锚定在你预期的位置,而不是页面原点。

这份 API 接口由 PHPDoc 生成。承载核心功能的入口点来自 \NextPDF\Core\Concerns\HasTransforms trait:

  • Document::startTransform(): static — 发出 q,开启一个状态区块
  • Document::stopTransform(): static — 发出 Q,关闭该区块
  • 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";

这是一段可独立运行、能纳入测试框架执行的程序。它对应 示例 examples/21-transforms.php 的缩放章节。每个变换都隔离在带有明确枢轴点的已保存图形状态区块中。颜色与线条状态会在结尾重置,因此不会有任何状态泄漏到后续页面。

<?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:

Wrote transforms.pdf

完整示例涵盖全部四个变换家族:旋转、缩放、倾斜与镜像。使用 php examples/21-transforms.php 运行它,它会生成 examples/output/21-transforms.pdf

  • 务必成对使用区块。 每个 startTransform() 都需要一个对应的 stopTransform()。不平衡的 q/Q 计数会损坏该页后续的图形状态(ISO 32000-2 §8.4.2)。NextPDF 会追踪嵌套深度,但 recipe 层面的约定是一一配对。
  • 顺序不可交换。 多个变换以矩阵串联的方式组合,因此先 rotate()scale(),与先 scale()rotate() 并不相同。请在同一个区块内,按照你想要的顺序应用它们。
  • 枢轴点默认为原点。 若省略枢轴点,变换就会围绕页面原点旋转,而不是围绕该形状。这通常不是你想要的结果,因此请明确传入枢轴点。
  • Y 轴采用作者空间。 枢轴点的 y 是相对于作者坐标左上角的距离,NextPDF 会将它投影到原生用户空间。混用原始 PDF 坐标与作者 API,会得到镜像翻转的结果。
  • 状态泄漏。 在变换区块内设置的颜色、字体与线宽,在 stopTransform() 之后仍会保留,因为在这份 API 接口中,Q 只会还原 CTM。如果后续章节不能继承这些值,请像生产版示例那样明确重置它们。

一个变换就是单个 cm 运算符,再加上一对 q/Q。每个部分都只有几个字节,不会带来任何可测量的运行时成本,因此这则 recipe 可以轻松保持在 1500 ms / 96 MB 的预算之内。可重现性配置文件为 结构性(structural)。输出中包含一个 trailer 的 /ID 数组以及创建时的元数据,这些内容并非每次运行都稳定,因此在比对前必须先将它们规范化。变换流本身是确定性的。

  • 数据驻留与 PII 缓解措施。 不适用。这则 recipe 绘制的是几何基本图形与简短标签。它不处理任何外部数据或个人数据。
  • 安全遥测与日志清理。 这则 recipe 只输出单行固定的进度信息。不会记录任何文档内容。
  • 威胁模型。 不适用。没有任何输入解析、没有密码学运算,也没有信任边界。变换纯粹是内容流输出。
  • FIPS 模式行为。 不适用。没有任何密码学运算。
陈述规范条款参考 ID
变换会通过 cm 运算符,把一个矩阵串联到 CTM 上。ISO 32000-2§8.3.4
多个变换是以矩阵串联的方式组合;顺序具有重要影响。ISO 32000-2§8.3.4
q 保存、Q 还原图形状态,借此限定变换的作用范围。ISO 32000-2§8.4.2
默认用户空间原点位于左下角;一个单位等于 1/72 英寸。ISO 32000-2§8.3.2

这则 recipe 实现了所引用的 ISO 32000-2 图形状态与变换条款。它并不主张全面符合 ISO 32000-2;所引用的条款仅限于这则 recipe 实际使用到的部分。