自炊にちょうどいいブックリーダーアプリを開発する話 #04 zipデータのコンバート処理

2024年1月7日

PHP アプリケーション

eyecatch 自炊・・・それは自宅にある書籍を極限までデジタル化する行為。 知識の泉として購入した書籍で自分の生活空間を圧迫されている人が、快適な空間を取り戻す為の行為でもある。 今回は自炊をした後のデータはpdfデータのフォーマットが多いと思いますが、たまにzipファイルにしているケースもあります。 ページを一枚ずつJPEGフォーマットやPNGフォーマットにして、それを1つのファイルにするためにzipファイルにするケースです。 さらなる圧縮率を求めてrarやz7などの、かつてのアングラで良く使われたフォーマットに変換するツワモノもいますが、 とりあえずzipデータに対しても今回のブックリーダーの対象にしておく必要を感じたので、zipデータの変換処理を公開しておきます。

ソースコード

zip.php

<?php require_once dirname(__FILE__). "/common.php"; class Zip{ public static $info = []; var $temp_dir = "data/tmp/"; var $setting = []; var $quality = 50; var $max_size = 1000; function __construct($setting=null){ if(!$setting){return;} $this->setting = $setting; } function convert(){ $zip = new ZipArchive; $dir = $this->setting["tmp_dir"]; $file = $dir .DIRECTORY_SEPARATOR. $this->setting["origin_file"]; $zip->open($file); $jsons = []; $names = []; for($i=0; $i<$this->setting["page_count"]; $i++){ $name = $zip->getNameIndex($i); $name = trim($name); // progress $progress_data = [ "page_count" => $this->setting["page_count"], "current" => $i+1, ]; Common::progress_save($this->setting["uuid"] , $progress_data); // フォルダの場合は処理を飛ばす if(!$name || preg_match("/\/$/", $name)){continue;} // 画像ファイルのみをたいしょうにする。 $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); if(!$this->is_target_ext($ext)){continue;} $data = $zip->getFromIndex($i); $image = imagecreatefromstring($data); $image = $this->resize_image($image, $ext); $num = sprintf("%05d" , $i); $webp_path = "{$dir}/out-{$num}.webp"; echo $webp_path.PHP_EOL; imagewebp($image , $webp_path , $this->quality); imagedestroy($image); $base64 = base64_encode(file_get_contents($webp_path)); $jsons[] = $base64; } $zip->close(); $json_path = $this->setting["tmp_dir"].".". Common::$output_ext; $this->setting["page_count"] = count($jsons); $datas = [ "setting" => $this->setting, "datas" => $jsons, ]; $json = json_encode($datas , JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); file_put_contents($json_path, $json); $this->delete_tmp_dir(); } function is_target_ext($ext=null){ switch($ext){ case "jpg": case "jpeg": case "png": case "gif": case "webp": return true; default: return false; } } function resize_image($image=null, $ext=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; } function delete_tmp_dir(){ if(!is_dir($this->setting["tmp_dir"])){return;} exec("rm -rf ".$this->setting["tmp_dir"] , $res); return $res; } }

common.php

<?php class Common{ public static $shelf = "data/shelf/"; public static $temp = "data/tmp/"; public static $origin = "original"; public static $setting_file = "setting.json"; public static $progress_file = "progress.json"; public static $output_ext = "yomii"; public static function get_uuid_dir($uuid=null){ if(!$uuid){return;} $dir = dirname(__FILE__). "/../../../". Common::$temp . $uuid; return Common::normalize_path($dir); } public static function get_original_file($ext=null){ $original_file_name = Common::$origin; return "{$original_file_name}.{$ext}"; } public static function normalize_path($path=null){ $normalize = realpath($path); return $normalize ? $normalize : $path; } public static function get_path($uuid=null, $ext=null){ $dir = rtrim(Common::get_uuid_dir($uuid) , DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; $file = Common::get_original_file($ext); return $dir . $file; } public static function get_setting_path($uuid=null){ $path = Common::get_uuid_dir($uuid) .DIRECTORY_SEPARATOR. Common::$setting_file; return is_file($path) ? $path : null; } public static function get_setting_data($uuid=null){ $path = Common::get_setting_path($uuid); if(!$path){return null;} $json = file_get_contents($path); return json_decode($json , true); } public static function get_book_json($uuid=null){ return Common::get_uuid_dir($uuid) .".". Common::$output_ext; } public static function get_progress_path($uuid=null){ return Common::get_uuid_dir($uuid) .DIRECTORY_SEPARATOR. Common::$progress_file; } public static function progress_save($uuid=null , $data=[]){ $data["status"] = "progress"; $data["start"] = $data["time_start"] ? $data["time_start"] : time(); $data["time"] = time() - $data["start"]; $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $path = Common::get_progress_path($uuid); return file_put_contents($path , $json); } public static function progress_load($uuid=null){//die('["'.$uuid.'"]'); if(!$uuid){ return '{"status":"error","message":"no-uuid"}'; } $progress_path = Common::get_progress_path($uuid); if($progress_path && is_file($progress_path)){ return file_get_contents($progress_path); } $json_path = Common::get_book_json($uuid); if($json_path && is_file($json_path)){ $data = [ "status" => "success", "uuid" => $uuid, ]; return json_encode($data , JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } return '{"status":"error","message":"no-file","progress-file":"'.$progress_path.'","json-file":"'.$json_path.'"}'; } }

解説

プロジェクトで作ったzip.phpをそのまま貼り付けたので、ポイントを解説しておきます。 基本的な挙動としては、次のフローで行っています。
1. zipファイルに圧縮格納されているファイル一覧を取得する。 2. 画像ファイルのみを抽出して、1枚ずつwebpフォーマットに変換する。 3. 処理をする任意フォルダを作成するために、uuidの発行する。 4. data/tmp/フォルダ以下に、処理の最初で発行したuuidのフォルダを作る。 5. 変換されたwebpファイルをuuidフォルダに保存する。 6. webpをbase64化して、必要情報と一緒にjsonファイルに書き込む。

phpのzip処理

PHPの基本モジュールである、ZipArchiveを使っています。 サーバーコマンドでPDFと同じ用に対応しようとも考えたんですが、できるだけ環境依存しない作りにしたかったので、zip処理はphp内部のみで行いたかったんですね。 (本当はPDFもPHPのみでやりたかったんですが、ライブラリが必要になりそうだったので、とりあえずコマンドに逃げました・・・) コードを見てもらうと、内包してあるファイル毎にfor文を回して、画像のみを処理するやり方で書いてます。

画像フォーマットはGDライブラリにおまかせ

GDライブラリもphpインストールからセットしなければいけないんですが、Dockerfileに次の記載をするだけで簡単にセットできます。 RUN docker-php-ext-configure gd --with-jpeg --with-webp && docker-php-ext-install -j$(nproc) gd *完成後にdocker設定も踏まえたgitを公開する予定なので、詳細はそちらを御覧ください。 webpも問題なくコンバートできるので、今後のdocker設定の為に覚えておいたほうがいいかもですね。

画像サイズを整える

自炊したスキャンデータは、3000pxから4000pxのサイズで画像が保存されているので、200ページぐらいの書籍でも、カラフルなものであれば、数百MBぐらいの容量になっていると思いますが、 スマホやタブレットで読むレベルで考えたら、1000pxぐらいあれば十分ということで、縦横の最大サイズを1000px上限で、計算して画像の大きさを調整しています。 もしかしたら、図鑑のような細かな詳細が必要な場合はもっと大きくしてもいいのですが、いまのところこの仕様にしています。 すると、ページ数の多い書籍であっても、数十MBぐらいに収まります。 1/10ぐらいのサイズ縮小が見込めるので、かなりのメリットですね。 Webp様々とも言えますが・・・

あとがき

上記処理の最後に、jsonファイルが作られたら、uuidフォルダを削除して終了です。 作られたJSONファイルは、ユーザーがダウンロードできるようにしておくことで、そのファイルがあれば、ブックリーダーで効率よく表示ができるようになるという仕様です。 ちなみに、JSONデータや画像フォーマットの仕様はPDF変換の処理とほぼ同じにしているので、この辺はシステマチックにしいたほうが、他のフォーマット採用の時に便利に使えそうですね。 でも今のところは、PDFとZIPのみの対応ということで勘弁してくだされ。