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 が行う。
取り込みの流れ¶
- 元のファイルの大きさ(メタデータ。元は開かない)が上限(100MB)を超えるなら、コピーの前に失敗にする。
- 元のファイルを読み取りだけ・共有モードで、作業領域の
source.copyにコピーする(copyFileShared。元のファイルを書き込みの API に渡さない)。 - コピーを読む(
readPdfFile)。読み終えたら、TSV を書く前にコピーを消す(作業領域の中身は本文インデックスに回るため)。消せなかったときは失敗にする。 - ページごとに場所
ページ001の TSV を書く(addUnitLines・writeUnits。Word と同じ書き方)。文字の無いページは TSV を作らない。 - 本文インデックスには種類
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 に戻り、TdTDで足し、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 は、テストの中で自作する(作成者などのメタデータを入れない)。