はじめに
前の記事では、await pdf.getPage(1) で page 変数を取得しました。これは PDFPageProxy インスタンスで、_pageInfo にページのメタデータが保存されています: rotate: 0, userUnit: 1, view: [0, 0, 595, 842].
第 1 記事のデモに戻ると、10〜11 行目:
const viewport = page.getViewport({ scale: 0.5 });
getViewport 呼び出し
object パラメータを渡します:
| パラメータ | 値 | 意味 |
|---|---|---|
scale |
0.5 |
ズーム倍率 |
getPage と異なり、getViewport は同期呼び出しです: worker に触れず、IO も行わず、純粋な数学計算を 1 回行うだけです。本記事ではこの呼び出しから出発し、getViewport の完全な実行チェーンをたどります。
api.js における PDFPageProxy の定義に基づくと、コードの実行フローは次のとおりです。
1423〜1439 行目: パラメータの分割代入
getViewport({
scale, // 0.5, 唯一渡されたパラメータ
rotation = this.rotate, // this._pageInfo.rotate = 0
offsetX = 0,
offsetY = 0,
dontFlip = false,
} = {}) {
return new PageViewport({
viewBox: this.view, // [0, 0, 595, 842]
userUnit: this.userUnit, // 1
scale,
rotation,
offsetX,
offsetY,
dontFlip,
});
}
rotation のデフォルト値はページ自身から取られることに注意してください: PDF ページ固有の /Rotate 属性が自動的に適用されるため、呼び出し側が気にする必要はありません。this.view と this.userUnit は前の記事の GetPage メッセージが返した pageInfo から来ています。
パラメータの収集が完了すると、すべてが PageViewport コンストラクタ (display/page_viewport.js) に渡されます: ソースを見る. 以降の計算はすべてその中で行われます。
Q:
offsetX/offsetYは何に使うのか?A: レンダリング結果をキャンバス上で平行移動します。例えば複数ページを 1 つの大きなキャンバスに並べたり、ページの周囲に余白を残したりします。デモでは渡していないため、デフォルト値の
0になります。
70 行目: scale が userUnit を吸収 (page_viewport.js)
scale *= userUnit; // 0.5 * 1 = 0.5
Q:
userUnitとは何か?A: PDF 1.6 で導入された単位換算係数です: PDF ユーザー空間の 1 単位 =
userUnit/ 72 インチで、デフォルト値は1です。これをscaleに掛け込むことで、以降の計算で「ユーザー空間の単位」と「実際のサイズ」を区別する必要がなくなります。
74〜75 行目: ページの中心点を計算 (page_viewport.js)
const centerX = (viewBox[2] + viewBox[0]) / 2; // (595 + 0) / 2 = 297.5
const centerY = (viewBox[3] + viewBox[1]) / 2; // (842 + 0) / 2 = 421
回転はページの中心を軸に行われるため、先に中心点を計算しておきます。
78〜116 行目: 回転の正規化と回転ベクトル (page_viewport.js)
rotation %= 360; // 0
if (rotation < 0) {
rotation += 360;
}
switch (rotation) {
case 0:
rotateA = 1;
rotateB = 0;
rotateC = 0;
rotateD = -1;
break;
// case 90 / 180 / 270 は省略
default:
throw new Error('PageViewport: Invalid rotation, must be a multiple of 90 degrees.');
}
回転は [0, 360) の区間に正規化され、90 の倍数のみ許可されます。それ以外の値は直接エラーを投げます — これは PDF 仕様の制限です。
デモの rotation は 0 で、case 0 にヒットします: rotateA = 1, rotateB = 0, rotateC = 0, rotateD = -1.
Q: なぜ rotation が 0 のとき rotateD が -1 なのか?
A: これは y 軸の反転です。PDF 座標系では y 軸は上向き (数学座標系、原点は左下)、canvas 座標系では y 軸は下向き (スクリーン座標系、原点は左上) です。回転しなくても、両者を揃えるために 1 回の反転が必要で、rotateD = -1がその反転です。対応して、
dontFlip = trueはrotateC,rotateDを反転させ、この反転をキャンセルします — ターゲットの座標系も y 軸が上向きの場面に適しています。
120〜130 行目: 幅・高さとキャンバスオフセットの計算 (page_viewport.js)
if (rotateA === 0) {
// rotation が 90/270 のときの分岐: 幅と高さが入れ替わる
offsetCanvasX = Math.abs(centerY - viewBox[1]) * scale + offsetX;
offsetCanvasY = Math.abs(centerX - viewBox[0]) * scale + offsetY;
width = (viewBox[3] - viewBox[1]) * scale;
height = (viewBox[2] - viewBox[0]) * scale;
} else {
offsetCanvasX = Math.abs(centerX - viewBox[0]) * scale + offsetX; // 297.5 * 0.5 + 0 = 148.75
offsetCanvasY = Math.abs(centerY - viewBox[1]) * scale + offsetY; // 421 * 0.5 + 0 = 210.5
width = (viewBox[2] - viewBox[0]) * scale; // 595 * 0.5 = 297.5
height = (viewBox[3] - viewBox[1]) * scale; // 842 * 0.5 = 421
}
rotateA === 0 の分岐は 90/270 度の回転に対応します: ページが横倒しになり、width と height の計算が入れ替わります。デモの回転は 0 なので、else 分岐を通ります。
134〜141 行目: transform 行列の組み立て (page_viewport.js)
this.transform = [
rotateA * scale, // 0.5
rotateB * scale, // 0
rotateC * scale, // 0
rotateD * scale, // -0.5
offsetCanvasX - rotateA * scale * centerX - rotateC * scale * centerY, // 148.75 - 148.75 - 0 = 0
offsetCanvasY - rotateB * scale * centerX - rotateD * scale * centerY, // 210.5 - 0 + 210.5 = 421
];
// transform = [0.5, 0, 0, -0.5, 0, 421]
ソースコードのコメントが、この 6 つの成分が対応する複合変換を説明しています:
translate(-centerX, -centerY), rotate and flip vertically, scale, and translate(offsetCanvasX, offsetCanvasY)
つまり: ページの中心を原点に移動 → 回転して垂直反転 → スケール → キャンバス上の目標位置へ平行移動。これは canvas の setTransform(a, b, c, d, e, f) が必要とする 6 つの引数そのもので、レンダリング時に直接適用されます (次の記事を参照)。
行列の正しさを検証してみましょう: PDF 座標系でのページの左上隅 (0, 842) (y 軸は上向きなので、「上」は大きい値) を取り、行列で変換します:
x' = 0.5 * 0 + 0 * 842 + 0 = 0y' = 0 * 0 + (-0.5) * 842 + 421 = 0
ちょうど canvas 座標の (0, 0)、つまりキャンバスの左上隅にマッピングされます。✓
6 番目の成分 421 はちょうど height に等しくなります: y 軸反転後、ページ全体がキャンバスの上側 (y が負) に移動するため、ページ 1 枚分の高さだけ下に平行移動してキャンバス内に戻す必要があります。
143〜144 行目: 幅と高さを保存 (page_viewport.js)
this.width = width; // 297.5
this.height = height; // 421
この 2 つの値が第 1 記事のデモの canvas.width = Math.floor(viewport.width) の出所です: Math.floor(297.5) は 297 で、canvas の実際のサイズは 297 x 421 になります。
これで getViewport の計算はすべて完了し、返された PageViewport インスタンスがデモの viewport 変数に代入されます。
まとめ
本節ではデモに沿って getViewport({ scale: 0.5 }) の完全な計算をたどりました:
- パラメータの分割代入:
scale = 0.5、rotationはデフォルトでページの/Rotate値0、残りは0/false; -
scaleがuserUnitを吸収し、0.5のまま; - ページの中心点
(297.5, 421)を計算; - 回転を正規化した後、回転ベクトル
(1, 0, 0, -1)を取得。rotateD = -1が y 軸反転を担う; - 幅と高さ
297.5 x 421とキャンバスオフセット(148.75, 210.5)を計算; -
transform = [0.5, 0, 0, -0.5, 0, 421]を組み立て、「PDF 座標 → canvas 座標」のマッピングを完成。
viewport には他にも convertToViewportPoint / convertToPdfPoint (座標の相互変換。テキスト選択や注釈の位置決めに使われる: ソースを見る)、clone (新しい viewport を派生: ソースを見る) などのメソッドがぶら下がっていますが、デモでは使われていないため、展開しません。
viewport を取得した後、デモはそれを canvasContext と一緒にパックして page.render(renderContext) に渡します — transform 行列はそこで canvas コンテキストに適用されます。次の記事で展開します。
私について: StreamPDF の創作者で、AI と PDF 領域の研究開発に従事しています。
