StreamPDFStreamPDF
ブログに戻る

PDF.js をデモから深掘り 2: getDocument メソッドの全体像

2026年10月27日 · StreamPDF Team

PDF.js をデモから深掘り 2: getDocument メソッドの全体像

はじめに

前の記事のデモは、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;

残りのパラメータはすべて未指定です。ソースコードの順に、計算結果は次のようになります:

ローカル変数計算結果値の取り方
datanull渡された場合は getDataProp で一律 Uint8Array に変換
httpHeadersnullそのまま透過、未指定なら null
withCredentialsfalse=== true のみ受け付ける
passwordnull?? null
rangeTransportnullPDFDataRangeTransport インスタンスのみ受け付ける
rangeChunkSize2 ** 16 (64KB)正整数のときだけ渡された値を受け付ける
workernullPDFWorker インスタンスのみ受け付ける
verbosityundefinedsrc.verbosity をそのまま取る
docBaseUrlnulldata: 以外のプロトコルの string のみ受け付ける
cMapUrlnullgetFactoryUrlProp: string 以外は null、かつ / で終わる必要あり
cMapPackedtrue!== false
iccUrlnullcMapUrl と同じ
standardFontDataUrlnullcMapUrl と同じ
wasmUrlnullcMapUrl と同じ
ignoreErrorstruesrc.stopAtErrors !== true、後述
maxImageSize-1 (無制限)整数かつ > -1 のときだけ受け付ける
isOffscreenCanvasSupportedtrue未指定なら !isNodeJS
isImageDecoderSupportedtrue未指定なら !isNodeJS
canvasMaxAreaInBytes-1 (無制限)整数のみ受け付ける
disableFontFacefalse未指定なら isNodeJS
fontExtraPropertiesfalse=== true のみ受け付ける
enableXfafalse=== true のみ受け付ける
ownerDocumentglobalThis.document未指定ならグローバルの document
disableRangefalse=== true のみ受け付ける
disableStreamfalse=== true のみ受け付ける
disableAutoFetchfalse=== true のみ受け付ける
pdfBugfalse=== true のみ受け付ける
CanvasFactoryDOMCanvasFactory環境による分岐、後述
FilterFactoryDOMFilterFactory同上
BinaryDataFactoryDOMBinaryDataFactory同上
enableHWAfalse=== true のみ受け付ける
enableWebGPUfalse=== true のみ受け付ける
gpuPromisePromise.resolve(false)WebGPU 無効のため GPU 初期化は行わない
useWasmtrue!== false
pagesMappernew PagesMapper()未指定なら新規作成
useSystemFontstrue連鎖的なデフォルト、後述
useWorkerFetchfalse複合判定、後述の Q&A を参照
styleElementnullTESTING ビルドのみ渡された値を保持、実運用では常に 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 になります:

  1. カスタムの BinaryDataFactory がない —— useWorkerFetch が true のときこのパラメータは直接無視される (後段の transportFactory で binaryDataFactory が null に置かれる) ため、ユーザー定義のファクトリは尊重されるべきで、デフォルトを true にはできない;
  2. cMapUrl / standardFontDataUrl / wasmUrl がすべて提供され、かつ cMapPacked が true —— URL がなければ fetch する対象がない;
  3. この 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) がどちらも完了すると、次の順に処理が進みます:

  1. hasGPU = false を evaluatorOptions に埋め戻す;
  2. worker へ GetDocRequest メッセージを送る。ペイロードは docParams。data は null なので、所有権を移す必要のあるバッファはない;
  3. ネットワークストリームを選択する: 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 / httpsPDFFetchStream
それ以外で、GENERIC ビルドかつ Node 環境PDFNodeStream
その他の場合PDFNetworkStream (XHR 実装)

デモの url は https://... なので、実際にインスタンス化されるのは PDFFetchStream です。他の 2 種類の入力にもそれぞれ専用のストリームがあります: rangeTransport を渡した場合は PDFDataTransportStream、data を渡した場合は PDF 全体がすでにメモリ上にあるためストリームを一切作りません。

まとめると: const NetworkStream = PDFFetchStream.

続けて実行されます:

  1. worker からの GetDocRequest への応答 (workerId) を受け取る: イベントを見る。ここで受け取る戻り値は d0_worker で、変数 workerId に代入される
  2. MessageHandler を生成し、パラメータ { docId: 'd0', workerId: 'd0_worker', port: Worker } を渡す: 呼び出しを見る、変数 messageHandler に代入
  3. WorkerTransport を生成し、パラメータ { messageHandler, task, networkStream, transportParams, transportFactory, pagesMapper } を渡す: 呼び出しを見る、task._transport に取り付ける
  4. worker へ Ready メッセージを送り、メインスレッド側が準備完了を宣言する: イベントを見る

チェーンの最後で task._setupCapability.resolve が呼ばれ、task の非同期処理が完了します。

474〜475 行目: 戻り値

上記の .then チェーンはすべてバックグラウンドで非同期に進み、getDocument 自体は同期的に task を返します。チェーンのどこかでエラーが起きると、task 内部の capability が reject されます。それがデモの await loadingTask.promise が待っている promise です。

これで getDocument の実行は完了です。

まとめ

本節ではデモに沿って getDocument の呼び出しを最後まで追いました:

  1. PDFDocumentLoadingTask を生成し、docId = 'd0' を取り出す;
  2. src を 35 以上のローカル変数に展開する。デモは url しか渡していないため、残りはすべてデフォルト値になる;
  3. 3 つの DOM ファクトリをインスタンス化し、transportFactory としてまとめる;
  4. PDFWorker.create が worker を生成し、ハンドシェイクを開始する;
  5. docParams (worker へ送る) と transportParams (メインスレッドに残す) を組み立てる;
  6. worker 準備完了後に GetDocRequest を送り、ネットワークストリームを確立し、MessageHandler と WorkerTransport を生成し、最後に Ready を送る;
  7. 同期的に task を返し、デモは await loadingTask.promise でドキュメントの準備完了を待つ。

第 1 記事のデモに戻ると、4〜7 行目で呼ばれているのが、まさに本記事で分解した getDocument です。

上位の呼び出しチェーンに沿うと、次の呼び出し地点は: getPage


私について: StreamPDF の創作者で、AI と PDF 領域の研究開発に従事しています。

PDF.js をデモから深掘りの続き