自炊にちょうどいいブックリーダーアプリを開発する話 #02 PDFを分解する仕様検討

2024年1月5日

アプリケーション テクノロジー

eyecatch 作りたいものが決まったら、すぐに開発に着手します。 今回のアプリの仕様は、自炊(スキャン)したファイルを適切なフォーマットにコンバートして使う事を前提に考えたいと思います。 もちろん、PDFファイルをサーバーに置いといてそのまま単一ページのみを取得するやり方でもいいんですが、どちらにしてもコンバートをしたデータを扱うようにした方がアプリの挙動が保てるので、まずはコンバート方式を作っていきたいと思います。

仕様検討

スキャンデータは、ほぼPDFにするのが一般的です。 自炊後にOCRを実行する場合、読み取ったテキストデータを保持するためには、JPEGやPNGフォーマットでは、難しいですからね。 もちろん、今回のシステム構築としては、そうしたJPEGやPNGファイルをまとめたZIP圧縮データも対象にしようと思うのですが、それらは後回しで開発を進めていきたいと思います。

システムフロー

  1. ユーザーが手元にあるPDFファイルをシステムのサーバー(今回開発するアプリケーション)にアップロードする。
  2. PDFデータを受け取ったサーバー側で、PDFファイルを1ページずつ分解してPNG画像に出力する。
  3. PNGファイルのサイズを最大サイズに整える(小さい画像の場合はそのまま)。
  4. 画像を1枚ずつWebpフォーマットに変換。
  5. Webp画像データをBase64(文字列)データに変換。
  6. サーバー側で取得できたファイルの情報と一緒に、全ての変換した文字列データをJSONフォーマットでテキストファイルとして保存。

技術仕様

最終的にパソコンやスマホアプリに変換しようと思うので、Nodejsで作ったほうがいいと思ったんですが、とりあえずwebサーバー版で安定稼働させたかったので、上記フローはPHPで構築していきたいと思います。 ※完成後に、Nodejs移植を考えますね。 PHPはPDFを言語ネイティブでは扱いきれないので、ライブラリをインストールする必要があります。 ここで、PHPのPDFライブラリがたくさんあって、どれが良いかを選別しなければいけなくなりました。
  • TCPDF
  • mPDF
  • FPDF
  • html2pdf
  • wkhtmltopdf(+snappy)
TCPDFが良いというネット記事をたくさん見ましたが、個人的には、これらのPHPライブラリを使わずに、LinuxのPDFtoPPMコマンドを使う方法にしました。 理由としては、PHPライブラリのPDFツールは、テキストやHTMLからPDFファイルを作成する事を前提にしているため、PDFを1ページずつ変換するという扱いが便利にできるのが無かったのです。 PDFtoPPMは、PDFを画像にコマンド一発で変換してくれるので、今回の仕様にもってこいという理由ですね。 あと、サーバーコマンドであれば、Dockerでインストールをカッチリ設定しておけば、どのサーバーでもDocker上で安定した動きが保証されるというメリットもあるからですね。

リサイズ処理はGDライブラリを使用

PDFtoPPMでPNGファイルの出力ができたら、次は、画像サイズを調整する処理ですが、これはImageMagicとGDライブラリの2大巨頭のどちらを使うのが良いか考えどころですね。 画像クオリティが良いのは、ImageMagicなんですが、スピードが遅いというのと、バージョンアップが不安定、バグがある・・・などのライブラリ的な問題が混在するようなので、GDライブラリを使うことにしました。

ソースコード

完成後にGithubにアップすて公開する予定なのですが、部分的なソースコードを紹介しておきます。

PDFファイル情報の取得

function pdf($path=null){ $cmd = "pdfinfo {$path}"; $datas = [ "book" => $path, ]; exec($cmd , $res); for($i=0; $i<count($res); $i++){ if(!$res[$i]){continue;} $sp = explode(":", $res[$i]); $key = trim($sp[0]); $val = trim($sp[1]); // blank if($val === ""){ $datas[$key] = null; } // number else if(preg_match("/^\d+?$/" , $val)){ $datas[$key] = (int)$val; } // string else{ $datas[$key] = $val; } } return [ "book" => $path, "ext" => "pdf", "pages" => $datas["Pages"], ]; }
$path : サーバー側でファイルを受け取った、$_FILE["file"]["tmp_path"]を送るだけです。

PDF変換処理

function pdf2png($pdf_file=null, $out_path=null , $page=1){ $cmd = "pdftoppm -png {$pdf_file} {$out_path} -f {$page} -l {$page} -cropbox -singlefile"; exec($cmd); $num = sprintf("%03d" , $page); return "{$out_path}-{$num}.png"; } クラス内でPDF変換に使う関数です。
$pdf_file : 変換前のPDFファイル。 $out_path : 出力するファイル名を記載します。(***.png)という感じですね。 $page : 1ページ毎に処理するため、ページ番号を送ります。

PNGをWebpに変換

function png2webp($png_path=null, $webp_path=null, $quality=null){ if(!is_file($png_path)){return;} $quality = $quality ? $quality : $this->quality; $png_image = imagecreatefrompng($png_path); $png_image = $this->resize_image($png_image); imagewebp($png_image , $webp_path, $quality); } function resize_image($image=null){ $max_size = 1000; $x1 = imagesx($image); $y1 = imagesy($image); $x2 = $y2 = $max_size; // landscape if($x1 > $y1){ if($x1 < $max_size){ return $image; } $rate = $x1 / $max_size; $x2 = $max_size; $y2 = floor($y1 / $rate); } // horizontal else{ if($y1 < $max_size){ return $image; } $rate = $y1 / $max_size; $y2 = $max_size; $x2 = floor($x1 / $rate); } $image2 = imagecreatetruecolor($x2, $y2); imagecopyresampled($image2, $image, 0, 0, 0, 0, $x2, $y2, $x1, $y1); return $image2; }
$png_path : 出力されたPNGファイルのパス $webp_path : アウトプットするwebpのファイルパス $quality : Webp変換のクオリティ値(0 ~ 100)※50ぐらいで十分です。

あとがき

多くの書籍は、200ページぐらいなんですが、辞書とかみたいに膨大なページ数がある場合を考えて、オライリーのJavascriptの書籍をスキャンしたので変換してみたいと思います。 ページ数は、359ページあり、ファイルサイズが620MBほどあります。 返還時間は、MacBookAir(M2)で、10分ほどかかってしまいました。 ファイルサイズは、なんと、18.3MBです。圧縮率ハンパないですね。 ちなみに、サーバーにはファイルアップロードの上限を設けないととんでもないことになるので、1000MB(1GB)にすることにしました。 ※500MBにしていて、オライリー本がコケたので・・・ 完成するのが楽しみになってきましたね。