跳到內容

轉換座標空間:旋轉、縮放、傾斜與鏡像

以選定的樞紐點為中心,轉換繪圖座標空間。這則 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 實際運用到的部分。