0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

0
Posted at

image.png

はじめに

前の記事のデモは、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 になります:

  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 / https PDFFetchStream
それ以外で、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 をデモから深掘り 2: getDocument メソッドの全体像

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?