
作りたいものが決まったら、すぐに開発に着手します。
今回のアプリの仕様は、自炊(スキャン)したファイルを適切なフォーマットにコンバートして使う事を前提に考えたいと思います。
もちろん、PDFファイルをサーバーに置いといてそのまま単一ページのみを取得するやり方でもいいんですが、どちらにしてもコンバートをしたデータを扱うようにした方がアプリの挙動が保てるので、まずはコンバート方式を作っていきたいと思います。
仕様検討
スキャンデータは、ほぼPDFにするのが一般的です。
自炊後にOCRを実行する場合、読み取ったテキストデータを保持するためには、JPEGやPNGフォーマットでは、難しいですからね。
もちろん、今回のシステム構築としては、そうしたJPEGやPNGファイルをまとめたZIP圧縮データも対象にしようと思うのですが、それらは後回しで開発を進めていきたいと思います。
システムフロー
  - ユーザーが手元にあるPDFファイルをシステムのサーバー(今回開発するアプリケーション)にアップロードする。
- PDFデータを受け取ったサーバー側で、PDFファイルを1ページずつ分解してPNG画像に出力する。
- PNGファイルのサイズを最大サイズに整える(小さい画像の場合はそのまま)。
- 画像を1枚ずつWebpフォーマットに変換。
- Webp画像データをBase64(文字列)データに変換。
- サーバー側で取得できたファイルの情報と一緒に、全ての変換した文字列データを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にしていて、オライリー本がコケたので・・・
完成するのが楽しみになってきましたね。
 
0 件のコメント:
コメントを投稿