自炊にちょうどいいブックリーダーアプリを開発する話 #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にしていて、オライリー本がコケたので・・・ 完成するのが楽しみになってきましたね。

人気の投稿

このブログを検索

ごあいさつ

このWebサイトは、独自思考で我が道を行くユゲタの少し尖った思考のTechブログです。 毎日興味がどんどん切り替わるので、テーマはマルチになっています。 もしかしたらアイデアに困っている人の助けになるかもしれません。

ブログ アーカイブ