コンテンツにスキップ

PDF ファイルの読み取り

扱うこと: PDF ファイル(.pdf)から文字を読む範囲、読めない PDF の扱い、読み取りの上限、場所(ページ)の付け方、取り込みの流れ。扱わないこと: Office ファイルの取り込み(クロール対象フォルダと取り込み対象・Excel)、テキストファイルの取り込み(テキストファイルの読み取り)、細工した PDF から身を守る確認(安全性の確認の詳細)。先に読むページ: クロール対象フォルダと取り込み対象、取り込み対象の決定。

方針

PDF は PowerShell だけで読む(shared/core/pdf_file.ps1・暗号化の復号は shared/core/pdf_crypt.ps1)。外の部品・ライブラリ・Office・外部プロセスは使わない。Office は起動しないので、取り込みは Office を使わずに読む仕事(取り込みのワーカー)で並べて行う。

画面にもファイルにも触らない関数(readPdfPages。バイト列からページごとの文字列を作る)と、ファイルを開いて読む関数(readPdfFile)に分ける。取り込みは tebunko/indexer/extract_pdf.ps1 の extractPdfFile が行う。

取り込みの流れ

  1. 元のファイルの大きさ(メタデータ。元は開かない)が上限(100MB)を超えるなら、コピーの前に失敗にする。
  2. 元のファイルを読み取りだけ・共有モードで、作業領域の source.copy にコピーする(copyFileShared。元のファイルを書き込みの API に渡さない)。
  3. コピーを読む(readPdfFile)。読み終えたら、TSV を書く前にコピーを消す(作業領域の中身は本文インデックスに回るため)。消せなかったときは失敗にする。
  4. ページごとに場所 ページ001 の TSV を書く(addUnitLines・writeUnits。Word と同じ書き方)。文字の無いページは TSV を作らない。
  5. 本文インデックスには種類 PDF のファイルとして載る。画像だけの PDF(文字が 1 つも無い)は TSV が 0 個で、取り込み済み(済)になり、更新が無ければ取り込み直さない。

読む範囲

読む 読まない(パスワードが要る暗号化・未対応の暗号化の方式・FlateDecode 以外の圧縮・ToUnicode の無い複合フォント・壊れた相互参照は読めない PDF として失敗にする。そのほかは失敗にせず読み飛ばす)
相互参照の表・相互参照のストリーム(PNG の予測つきを含む)・オブジェクトのストリーム 開くのにパスワードが要る PDF
空の利用者パスワードで開ける AES-256 の暗号化された PDF(下の「暗号化された PDF」) AES-256 以外の暗号化(RC4 40・128 ビット、AES-128 など)
圧縮なし、FlateDecode FlateDecode 以外の圧縮
文字の命令 Tj TJ ' "、行送り Td TD T* Tm、フォントの指定 Tf 壊れた相互参照の修復(修復しない)
フォントの ToUnicode(bfchar・bfrange・配列の形・codespacerange。サロゲートペアを含む) ToUnicode の無い複合フォント(CID)を使う文字
ToUnicode の無い単純フォント(WinAnsi 相当の 1 バイト文字) OCR(画像の文字は読まない)
XRefStm(表で解放済みとされた番号を補う)・Prev Do で呼ぶ Form XObject の中の文字
注釈・フォームの欄・しおり(アウトライン)の文字
単純フォントの /Encoding と /Differences(ToUnicode が無ければ、常に Windows-1252 で読む)
  • 場所: ページの順に ページ001 ページ002 …(ページ{0:D3})。場所の名前は Word のページと同じ形。
  • 行: 文字を出すとき、縦の位置(BT で 0 に戻り、Td TD で足し、Tm で決め、T* ' " では行送り TL の分だけ引く)が、前に出した文字と 1 ポイントより大きく違えば行を分ける。T* ' " は、必ず行を分ける。ET では分けない(Word や Chromium が 1 行を複数の BT〜ET に分けても、同じ縦位置なら同じ行になる)。横だけ動く Td(後ろ向き、または今のフォントの大きさ(1 em)以上の動き)と、Tm で位置を決め直すときは、半角の文字どうしなら空白でつなぐ。直前の Td 以降に出した文字が 1 字形(フォントの 1 コード)以下で、1 em より小さい前向きの Td は、1 文字ずつ並べる PDF(Chromium など)の語を切らないよう、空白を入れない(2 文字以上の語の後ろは空白でつなぐ)。文字の幅は見ないので、1 文字の短い語(a など)の後ろの空白が消えることと、1 em 以上の幅の文字(@ など)の後ろに空白が入ることがある。縦の動きには Tm の拡大(縦横の成分 c・d から求めた倍率)を掛ける。cm による位置の移動は見ないので、cm で位置を決める PDF は、縦の位置が変わらず 1 行になることがある。TJ の詰めの数値が -250 以下で、前後の文字がどちらも半角のときだけ半角の空白を 1 つ入れる。
  • 画像のインラインデータ(BI … ID … EI)は読み飛ばす。
  • 文字の位置・向き・列の並び(段組み)は見ない。ページの中の文字は、ファイルに書かれた順に並ぶ。

暗号化された PDF

所有者のパスワードだけが設定され、利用者のパスワードが空の PDF は、ブラウザーと同じようにパスワード無しで開ける。このうち AES-256 のものを復号して読む(pdf_crypt.ps1)。読む方式は標準のセキュリティハンドラー(/Filter /Standard)の次の 1 つだけ。

方式 /V /R 復号
AES-256 5・5 と 5・6 AES-CBC(ファイルの鍵をそのまま使う。R6 はアルゴリズム 2.B)
  • 空のパスワードから /U と照らして鍵を作る。合わなければ(開くのにパスワードが要る)PDFにパスワードが必要なため読めません。。パスワードを探す・試す処理は無い。
  • RC4 40・128 ビットと AES-128(/V が 4 以下)は読まない。これらは鍵の導出に MD5 が要り、MD5 は FIPS 準拠を強制した機械で .NET の型が例外になる上、自前でも書かない方針のため、空のパスワードで開けるかも判断できない。そのため、パスワードの有無にかかわらず常に PDFの暗号化の方式に対応していないため読めません。 で失敗にする。
  • 独自のフィルター・知らない /V /R・知らない暗号方式(/CFM)も同じ PDFの暗号化の方式に対応していないため読めません。。/U /UE が短いなどの壊れた辞書は PDFの暗号化の情報が壊れているため読めません。。暗号化されたストリームの長さや詰め物が壊れているときは PDFの暗号化されたデータが壊れているため読めません。。どれもそのファイルだけの失敗(暗号化まわりの文言は 4 つ)。
  • 復号するのはストリームだけ(文字列は読む内容がストリームの中にあるため、復号しない)。相互参照のストリームは暗号化されないので復号しない。/StmF /Identity・/CFM /None・ストリームごとの /Crypt フィルターの /Identity も復号しない。
  • R6 の鍵の計算の繰り返しは上限(300 回。仕様どおりなら最大 288 回で終わる)で止める。鍵・途中の値は、例外の文にも記録にも出さない。
  • SHA-2 と AES は FIPS でも使える *CryptoServiceProvider を使う。

読み取りの上限

細工した PDF で固まったり、メモリを使い切ったりしないよう、すべて上限を設ける。超えたら失敗にする(PDF で始まるメッセージの例外)。

上限 値 超えたとき
元のファイルの大きさ 100MB(コピーの前に確かめる) ファイルサイズが大きすぎるため更新できません。
取り出したストリームの大きさの合計(取り出す前の大きさと、圧縮を解いた後の大きさ。参照されるたびに数える) 100MB PDFの解凍後の大きさが上限を超えるため読めません。
オブジェクトの数 1,000,000 PDFのオブジェクトの数が上限を超えるため読めません。
入れ子の深さ(配列・辞書・ページツリー) 64 PDFの入れ子が深すぎるため読めません。 など
読み込み中の間接参照の重なり(/Length が別のストリームを指す連なりなど) 64 PDFの参照が深すぎるため読めません。
文字の対応表(ToUnicode)の項目の合計(同じ表を使うフォントは 1 度だけ数える) 1,000,000 PDFの文字の対応表が大きすぎるため読めません。
配列の要素・辞書の項目の数(1 つあたり) 1,000,000(オブジェクトの数と同じ) PDFの配列の要素の数が上限を超えるため読めません。 PDFの辞書の項目の数が上限を超えるため読めません。
読んだ相互参照の行の総数 1,000,000(オブジェクトの数と同じ) PDFのオブジェクトの数が上限を超えるため読めません。
ストリームの終わりを探して読み進めた文字数の合計 ファイルの大きさの 4 倍 PDFのストリームの終わりを探す量が上限を超えるため読めません。
たどる相互参照(Prev・XRefStm)の数 1,000 PDFの相互参照の数が上限を超えるため読めません。
ページ数 10,000 PDFのページ数が上限を超えるため読めません。

参照の循環(ページツリー・間接参照・古い相互参照のつながり)と、ファイルの外を指す相互参照の位置は、例外にする。Prev は呼び出しの入れ子にせず、順に読む。/Length が合わないストリームの終わりの探し直しは、直前に見つけた位置を使い回し、手前から探すときは手前の分だけ探す(探した文字数の合計は数え、上限を超えたら断る。並び順や入れ子の細工で同じ範囲を何度も探させられても、探す量はファイルの大きさの 4 倍で止まる)。オブジェクトのストリームの /N は、中身の大きさ(1 件に 4 文字以上)から見て多すぎれば、確保する前に断る。

読めない PDF

読めないときは、そのファイルだけが取り込みの失敗になり、インデックス作成は続く。画面・取り込み一覧のメッセージは PDF で始まる短い文言(エラーメッセージ一覧)。readPdfPages は、PDF で始まらない例外を PDFが壊れているため読めません。 に言い換える(内部の例外の文言を出さない)。

検索・開く

  • 種類のチップ「PDF」(setting.config の fileKinds の値 pdf)で、PDF だけを絞れる。前の版の設定は、fileKinds のキーが無い・空ならそのまま PDF も含むすべてになり、一部の種類だけを保存していたときは PDF が選ばれていない状態から始まる(設定ファイルの形は変えない。前の版の見本 settings/v0.3.1 の期待する値は、PDF を足したぶん変えた)。
  • 結果の見出しのアイコンは PDF(getContentIndexFileKind)。場所は ページN。
  • 検索結果から開くと、既定のアプリで開く(ページへは移動しない)。ステータスは 開きました:<パス>(検索結果からファイルを開く)。

テスト

  • tests/shared/core/pdf_file.Tests.ps1 … 読み取り(自作した PDF のバイト列。圧縮・相互参照のストリーム・フォント・上限・壊れた入力・AES-256 の暗号化 PDF・パスワードが要る PDF・未対応や壊れた暗号化の辞書)。
  • tests/shared/core/pdf_crypt.Tests.ps1 … 暗号の部品(SHA の公開されている正解の値、R5・R6 のハッシュと鍵の導出を別に書いた計算と照らす、オブジェクトのストリームごと暗号化した PDF を読む、ストリームごとの復号の方式の選び方)。暗号化 PDF を組み立てる道具(tests/helpers/pdf.ps1 の newEncryptedPdf)は、読む側の部品を使わず、.NET の型で別に書く。
  • tests/tebunko/indexer/extract_pdf.Tests.ps1 … コピーの扱い・TSV の書き出し・失敗の扱い。
  • tests/testdata/compat/index/v0.4.0+pdf/ … 前の版の見本(取り込み済みのインデックスを読み、検索できること)。
  • テストに使う PDF は、テストの中で自作する(作成者などのメタデータを入れない)。