はじめに
前の記事のデモは、getDocument を呼ぶときに { url: '...' } だけを渡しています。本記事ではこの呼び出しから出発し、getDocument の実行チェーンを最後まで追いかけます。
getDocument 呼び出し
ソースコード上の仮引数名は src で、デモが実際に渡すキーは 1 つだけです:
src のキー |
値 |
|---|---|
url |
'https://assets-blog.streampdf.com/PDF32000_2008.pdf' |
display/api.js における getDocument の定義に基づくと、コードの実行フローは次のとおりです。
235〜236 行目: loading タスクの初期化
PDFDocumentLoadingTask クラスをインスタンス化し: ソースを見る、戻り値を task 変数に代入します。
task から docId 変数を取り出します。この時点で docId は d0 です。
238〜335 行目: パラメータの検証と処理
この区間は、ユーザーが渡した src オブジェクトを 35 以上のローカル変数に展開します: 渡された値は検証と型変換を行い、渡されていない値はデフォルト値を取ります。デモは url しか渡していないため、ほとんどのパラメータはデフォルト分岐を通ります。
唯一渡された url は、getUrlProp によって URL オブジェクトに変換されます:
const url = URL.parse('https://assets-blog.streampdf.com/PDF32000_2008.pdf', window.location);
// const url = src.url ? getUrlProp(src.url) : null;
残りのパラメータはすべて未指定です。ソースコードの順に、計算結果は次のようになります:
| ローカル変数 | 計算結果 | 値の取り方 |
|---|---|---|
data |
null |
渡された場合は getDataProp で一律 Uint8Array に変換 |
httpHeaders |
null |
そのまま透過、未指定なら null
|
withCredentials |
false |
=== true のみ受け付ける |
password |
null |
?? null |
rangeTransport |
null |
PDFDataRangeTransport インスタンスのみ受け付ける |
rangeChunkSize |
2 ** 16 (64KB) |
正整数のときだけ渡された値を受け付ける |
worker |
null |
PDFWorker インスタンスのみ受け付ける |
verbosity |
undefined |
src.verbosity をそのまま取る |
docBaseUrl |
null |
data: 以外のプロトコルの string のみ受け付ける |
cMapUrl |
null |
getFactoryUrlProp: string 以外は null、かつ / で終わる必要あり |
cMapPacked |
true |
!== false |
iccUrl |
null |
cMapUrl と同じ |
standardFontDataUrl |
null |
cMapUrl と同じ |
wasmUrl |
null |
cMapUrl と同じ |
ignoreErrors |
true |
src.stopAtErrors !== true、後述 |
maxImageSize |
-1 (無制限) |
整数かつ > -1 のときだけ受け付ける |
isOffscreenCanvasSupported |
true |
未指定なら !isNodeJS
|
isImageDecoderSupported |
true |
未指定なら !isNodeJS
|
canvasMaxAreaInBytes |
-1 (無制限) |
整数のみ受け付ける |
disableFontFace |
false |
未指定なら isNodeJS
|
fontExtraProperties |
false |
=== true のみ受け付ける |
enableXfa |
false |
=== true のみ受け付ける |
ownerDocument |
globalThis.document |
未指定ならグローバルの document
|
disableRange |
false |
=== true のみ受け付ける |
disableStream |
false |
=== true のみ受け付ける |
disableAutoFetch |
false |
=== true のみ受け付ける |
pdfBug |
false |
=== true のみ受け付ける |
CanvasFactory |
DOMCanvasFactory |
環境による分岐、後述 |
FilterFactory |
DOMFilterFactory |
同上 |
BinaryDataFactory |
DOMBinaryDataFactory |
同上 |
enableHWA |
false |
=== true のみ受け付ける |
enableWebGPU |
false |
=== true のみ受け付ける |
gpuPromise |
Promise.resolve(false) |
WebGPU 無効のため GPU 初期化は行わない |
useWasm |
true |
!== false |
pagesMapper |
new PagesMapper() |
未指定なら新規作成 |
useSystemFonts |
true |
連鎖的なデフォルト、後述 |
useWorkerFetch |
false |
複合判定、後述の Q&A を参照 |
styleElement |
null |
TESTING ビルドのみ渡された値を保持、実運用では常に null
|
以上がこの区間で生成されるすべてのローカル変数です。多くの判定は「未指定ならデフォルト」というありきたりなものですが、いくつか掘り下げる価値があります。
ignoreErrors: 意味が反転した内部変数
ユーザー向けのパラメータは stopAtErrors ですが、内部変数は ignoreErrors という名前で、値の取り方は src.stopAtErrors !== true です。つまりデフォルトの挙動はエラーを無視することで、解析エラー時に getOperatorList / getTextContent / RenderTask などの promise を reject するのではなく、可能な限り状態を復元しようとします。ソースを読むとき、ここは名前と値を取り違えやすい箇所です。
rangeChunkSize: 64KB の伏筆
rangeChunkSize のデフォルトは 64KB です。disableRange = false であるため、後段で transport を作るとき PDF に対して range リクエストを送り、1 回ごとに取得するチャンクのサイズがこの値になります。覚えておいてください。
docBaseUrl: なぜ data: URL を除外するのか
ソースコードのコメントが理由を示しています: data: URL は絶対パスの復元に使えず、しかもPDF ドキュメント全体を内包していて長さに上限がないため、worker スレッドへ送らないのです。
gpuPromise: Worker 起動と並行する設計
const gpuPromise = Promise.resolve(false); // enableWebGPU ? initGPU() : Promise.resolve(false);
WebGPU を有効にしなくても、ここでは即座に resolved な promise を返します。判定を使用時まで先送りするのではなく、GPU 初期化を worker の起動と並行して実行し、解決された真偽値を後で worker に転送する、という意図がコメントに記されています。
3 つの Factory: 環境による分岐
const CanvasFactory = DOMCanvasFactory;
// src.CanvasFactory || (GENERIC ビルド && isNodeJS ? NodeCanvasFactory : DOMCanvasFactory);
FilterFactory, BinaryDataFactory も同様です。判定は 3 段階です: ユーザー定義 > Node 環境の実装 > DOM 実装。デモはブラウザで動作し、カスタムもしていないため、3 つのファクトリはすべて DOM 版になります。それぞれの役割:
-
CanvasFactory: レンダリング用の canvas を生成する -
FilterFactory: 画像フィルタ (SVG フィルタなど) を生成する -
BinaryDataFactory: 組み込みの静的リソースをダウンロードする。後述のuseWorkerFetchと連動する
useSystemFonts: デフォルト値が他のパラメータに依存する
const useSystemFonts = true;
// typeof src.useSystemFonts === "boolean" ? src.useSystemFonts : !isNodeJS && !disableFontFace;
デフォルト値は、直前に解析された disableFontFace に依存します。なぜか? PDF に埋め込まれていないフォントは FontFace として読み込まれますが、disableFontFace が true だとこの読み込み経路が無効になり、その状態でシステムフォントに頼るとグリフが完全に崩れます。そこでこのケースではデフォルト値も false にして、完全に壊れたフォントをレンダリングしてしまう事態を避けます。
useWorkerFetch: 複合判定
const useWorkerFetch = false;
// typeof src.useWorkerFetch === "boolean"
// ? src.useWorkerFetch
// : MOZCENTRAL ||
// !!(
// BinaryDataFactory === DOMBinaryDataFactory &&
// cMapUrl && cMapPacked && standardFontDataUrl && wasmUrl &&
// isValidFetchUrl(cMapUrl, document.baseURI) &&
// isValidFetchUrl(standardFontDataUrl, document.baseURI) &&
// isValidFetchUrl(wasmUrl, document.baseURI)
// );
Q: この判定は何をしているのか?
A: 「組み込みの静的リソース」(CMap ファイル、標準フォントファイル、wasm ファイル) を誰がダウンロードするかを決めています: worker スレッドが Fetch API で自分でダウンロードする (true)、それともメインスレッドがダウンロードして worker に転送する (false)、のどちらかです。ユーザーが明示的に boolean を渡した場合は、その値をそのまま使います。そうでなければデフォルト値を計算します: MOZCENTRAL (Firefox 組み込み viewer) は常に
true、通常のビルドでは次の 3 条件を同時に満たす場合のみtrueになります:
- カスタムの
BinaryDataFactoryがない ——useWorkerFetchがtrueのときこのパラメータは直接無視される (後段のtransportFactoryでbinaryDataFactoryがnullに置かれる) ため、ユーザー定義のファクトリは尊重されるべきで、デフォルトをtrueにはできない;cMapUrl/standardFontDataUrl/wasmUrlがすべて提供され、かつcMapPackedがtrue—— URL がなければ fetch する対象がない;- この 3 つの URL が
document.baseURIを基準に解決した結果が http/https である —— Fetch API は http/https しかサポートせず、file/ftp は不可。本記事のデモは
urlだけを渡し、cMapUrlはnullなので条件 2 を満たさず、useWorkerFetch = falseになります: メインスレッド経路をたどり、DOMBinaryDataFactoryがメインスレッドでダウンロードし、worker は必要に応じてFetchBinaryDataメッセージでメインスレッドから取得します。
338 行目: メインスレッドのログレベル設定
setVerbosityLevel(verbosity); // verbosity は undefined
verbosity は undefined なので、メインスレッドのログレベルはデフォルトのままです。これはメインスレッドにのみ影響することに注意してください。worker スレッドのログレベルは worker 生成時に別途渡されます。
342〜350 行目: 3 つのファクトリのインスタンス化
const transportFactory = {
canvasFactory: new DOMCanvasFactory({ ownerDocument: document, enableHWA: false }),
filterFactory: new DOMFilterFactory({ docId: 'd0', ownerDocument: document }),
binaryDataFactory: new DOMBinaryDataFactory({
cMapUrl: null,
standardFontDataUrl: null,
wasmUrl: null,
}),
};
// binaryDataFactory: MOZCENTRAL または useWorkerFetch が true のときは null
先ほど選んだ 3 つの Factory クラスをインスタンス化し、transportFactory としてまとめて、その後の transport に渡します。useWorkerFetch = false なので binaryDataFactory は null ではありません —— 前節の Q&A と呼応しています: 組み込みの静的リソースはメインスレッドがダウンロードします。
DOMCanvasFactory のインスタンス化: ソースを見る
DOMFilterFactory のインスタンス化: ソースを見る
DOMBinaryDataFactory のインスタンス化: ソースを見る
352〜360 行目: Worker の生成
worker = PDFWorker.create({ verbosity: undefined, port: undefined });
// port は GlobalWorkerOptions.workerPort から取得、デモでは未設定
task._worker = worker;
デモは worker を渡していないため、PDF.js 自身が 1 つ生成して所有します。PDFWorker.create は PDFWorker インスタンスを同期的に返し、そのコンストラクタは GlobalWorkerOptions.workerSrc (第 1 記事で設定した worker ファイルのアドレス) を読み取り、Worker を生成して ready / test メッセージで起動ハンドシェイクを完了します。
PDFWorker.create は同期呼び出しです: 呼び出しを見る
ここでは次の 1 点だけ覚えておけば十分です: worker.promise は Promise<void> で、test の応答が成功した後に resolve され (値は undefined)、起動失敗や破棄時には reject されます。
362〜402 行目: 2 つのパラメータパックの組み立て
1 つ目は docParams で、後にスレッドを越えて worker へ送られます:
const docParams = {
docId: 'd0',
apiVersion: '6.3.289', // ビルドのバージョン番号
data: null,
password: null,
disableAutoFetch: false,
rangeChunkSize: 65536,
docBaseUrl: null,
enableXfa: false,
evaluatorOptions: {
maxImageSize: -1,
disableFontFace: false,
ignoreErrors: true,
isOffscreenCanvasSupported: true,
isImageDecoderSupported: true,
canvasMaxAreaInBytes: -1,
fontExtraProperties: false,
useSystemFonts: true,
useWasm: true,
useWorkerFetch: false,
cMapUrl: null,
cMapPacked: true,
iccUrl: null,
standardFontDataUrl: null,
wasmUrl: null,
hasGPU: false, // プレースホルダー、後で埋め戻す
},
};
2 つ目は transportParams で、メインスレッドに残します:
const transportParams = {
ownerDocument: document,
pdfBug: false,
styleElement: null,
enableHWA: false,
loadingParams: { disableAutoFetch: false, enableXfa: false },
};
一言で区別すると、docParams は worker に送る「作業指示」、transportParams はメインスレッドが自分で保持する「ローカル設定」です。
404〜473 行目: worker 準備完了後のハンドシェイク
Promise.all([worker.promise, gpuPromise]).then(...)
worker の準備完了と GPU 検出 (resolve 値は false) がどちらも完了すると、次の順に処理が進みます:
-
hasGPU = falseをevaluatorOptionsに埋め戻す; - worker へ
GetDocRequestメッセージを送る。ペイロードはdocParams。dataはnullなので、所有権を移す必要のあるバッファはない; - ネットワークストリームを選択する:
dataがnullかつrangeTransportもnullなのでurl分岐をたどり、ネットワークストリームをインスタンス化する:
networkStream = new NetworkStream({
url,
httpHeaders: null,
withCredentials: false,
rangeChunkSize: 65536,
disableRange: false,
disableStream: false,
});
ここの NetworkStream は import された固定クラスではなく、この分岐内で実行環境に応じて動的に選ばれます:
const NetworkStream = getNetworkStream(url); // api.js#L431
getNetworkStream は 3 択を 1 回だけ行います:
function getNetworkStream(url) {
// eslint-disable-next-line no-nested-ternary
return isValidFetchUrl(url)
? PDFFetchStream
: typeof PDFJSDev !== 'undefined' && PDFJSDev.test('GENERIC') && isNodeJS
? PDFNodeStream
: PDFNetworkStream;
}
| 判定 | 返されるストリーム実装 |
|---|---|
isValidFetchUrl(url): プロトコルが http / https
|
PDFFetchStream |
| それ以外で、GENERIC ビルドかつ Node 環境 | PDFNodeStream |
| その他の場合 |
PDFNetworkStream (XHR 実装) |
デモの url は https://... なので、実際にインスタンス化されるのは PDFFetchStream です。他の 2 種類の入力にもそれぞれ専用のストリームがあります: rangeTransport を渡した場合は PDFDataTransportStream、data を渡した場合は PDF 全体がすでにメモリ上にあるためストリームを一切作りません。
まとめると: const NetworkStream = PDFFetchStream.
続けて実行されます:
- worker からの
GetDocRequestへの応答 (workerId) を受け取る: イベントを見る。ここで受け取る戻り値はd0_workerで、変数workerIdに代入される - MessageHandler を生成し、パラメータ
{ docId: 'd0', workerId: 'd0_worker', port: Worker }を渡す: 呼び出しを見る、変数messageHandlerに代入 - WorkerTransport を生成し、パラメータ
{ messageHandler, task, networkStream, transportParams, transportFactory, pagesMapper }を渡す: 呼び出しを見る、task._transportに取り付ける - worker へ
Readyメッセージを送り、メインスレッド側が準備完了を宣言する: イベントを見る
チェーンの最後で task._setupCapability.resolve が呼ばれ、task の非同期処理が完了します。
474〜475 行目: 戻り値
上記の .then チェーンはすべてバックグラウンドで非同期に進み、getDocument 自体は同期的に task を返します。チェーンのどこかでエラーが起きると、task 内部の capability が reject されます。それがデモの await loadingTask.promise が待っている promise です。
これで getDocument の実行は完了です。
まとめ
本節ではデモに沿って getDocument の呼び出しを最後まで追いました:
-
PDFDocumentLoadingTaskを生成し、docId = 'd0'を取り出す; -
srcを 35 以上のローカル変数に展開する。デモはurlしか渡していないため、残りはすべてデフォルト値になる; - 3 つの DOM ファクトリをインスタンス化し、
transportFactoryとしてまとめる; -
PDFWorker.createが worker を生成し、ハンドシェイクを開始する; -
docParams(worker へ送る) とtransportParams(メインスレッドに残す) を組み立てる; - worker 準備完了後に
GetDocRequestを送り、ネットワークストリームを確立し、MessageHandlerとWorkerTransportを生成し、最後にReadyを送る; - 同期的に
taskを返し、デモはawait loadingTask.promiseでドキュメントの準備完了を待つ。
第 1 記事のデモに戻ると、4〜7 行目で呼ばれているのが、まさに本記事で分解した getDocument です。
上位の呼び出しチェーンに沿うと、次の呼び出し地点は: getPage
私について: StreamPDF の創作者で、AI と PDF 領域の研究開発に従事しています。
