MANGA READER 自炊漫画リーダー
- PHP
- MySQL
- XAMPP / Apache
- PDO
- JavaScript (SPA)
- PDF.js
- bcrypt Auth
- Python
- Ubuntu / MariaDB
- Tailscale VPN
- Figma
OVERVIEW
プロジェクト概要
課題
自炊した漫画を読むアプリは海外製が多く、左綴じ(左から右へ送る)が前提でした。 日本の書物は縦読みが主流で右綴じなので、ページが逆に進み、見開きも左右が入れ替わってしまいます。 さらに、200作品超のライブラリを出版社・作者・ジャンルで整理し、 読書進捗まで自動保存できるものは見つかりませんでした。
目的
右綴じを既定にして、スマホを縦に持ったまま1ページずつ読めるビューアを作りました (左綴じにも切り替えられるので、海外のコミックもそのまま読めます)。 XAMPP (Apache + PHP + MySQL) 上に認証・ライブラリ走査・PDFレンダリング・読書進捗保存までを 一人で構築し、ライブラリ走査は 13秒 → 9ms(約1374倍) に最適化しています。 いまは Ubuntu 機にも同じものを立て、VPN 経由で外出先のスマホからも同じ続きが読めるようにしました。
PROCESS
制作プロセス
-
PLAN
自炊ファイルの構成(PDF / 画像フォルダ混在)を整理し、必要機能を洗い出し。
-
BUILD
XAMPP 上に PHP + MySQL でアプリを構築。PDO で SQL インジェクションを防止。
-
READER
PDF.js で見開き/単一ページ/白紙挿入/左綴じ・右綴じを実装。
-
OPTIMIZE
ライブラリ走査・巻一覧・セッションロックを最適化し、体感速度を一桁向上。
-
OPERATE
読書進捗を自動保存、メタデータ編集 UI、出版社フィルタなど運用機能を継続追加。
-
MOBILE
スマホでの不具合を10項目洗い出し、Figma で16画面を設計してから作り直し。
-
REMOTE
VPN で外出先から接続。遅さの原因を PDF のページツリーまで辿り、3,708冊を階層化。
GALLERY
成果物ギャラリー
UI DESIGN
UI設計で気にした点
まず必要な機能をひととおり実装し、そこから実際に使いながら要らないものを削っていきました。 無くても困らないものは、作る前ではなく使ってみて分かります。 目指したのはシンプルで分かりやすい画面です。決めたのは次の4点です。
作品を選ぶ画面と巻を選ぶ画面で、同じ並べ方は合わない
作品を選ぶ画面は6列に固定しました。7列以上にすると 視線の動線が大きくなって探しにくいからです。表紙は絵で見分けるので、 目を振る幅が狭いほうが速く見つかります。 一方、巻を選ぶ画面は画面いっぱいに並べます。 巻は1から順に並んでいて探すのではなく数えて辿る動きなので、 7列以上でも目当ての巻にたどり着けます。むしろ一度に全巻見えるほうが早い。 スマホでも同じ考え方のまま、画面幅390pxから余白と間隔を引いて 3列・表紙112pxに収まるよう逆算しています (以前は表紙幅を161px固定にしていたため1列になり、画面の半分が空白でした)。
操作ボタンを増やすほど、読むための画面が狭くなる
当初は8つ置くつもりでしたが、読みながら検証を繰り返して4つまで削りました。
残したのは見開き/単ページ・右綴じ/左綴じ・白紙挿入・幅に合わせるです。
削ったものは、押さなくても正しくなる形に置き換えました。
見開きの切り替え:端末の向きと幅から自動。1ページが小さくなりすぎる
768px 未満は単ページに落とします。
ページ送り・戻る:スマホはスワイプ、PC はクリックと矢印キー。
向きは矢印キーに揃えず紙をめくる指の動きに合わせました(右綴じなら右へ払うと次)。
紙面の回転:廃止。マウス時代の名残で、タッチ端末では邪魔になるだけでした。
操作ボタンの置き場所は、PC とスマホで答えが違う
基準は「一番よく使う操作のすぐ近くに置く」の一つだけで、 結果が端末ごとに変わります。 PC の見開きでは操作ボタンを左ページの上、作品名を右ページの上に分けました。 右綴じなので次のページへ進むクリックは左側が圧倒的に多く、 カーソルはもともと左にいます。操作系をそこへ寄せるとマウスの移動距離が最短になります。 スマホは親指の届く範囲が下なので、操作系は下側に集約し、上は戻ると作品名だけ。 なお表示時間は1.5秒では出てから狙って押すまでが間に合わなかったので4秒に延ばしました。 読んでいる間は消えているほうが紙面に集中できるので、常時表示にはしていません。
スマホで、タップのたびに紙面が上下に動いてしまう
スマホはタップでツールバーが出入りします。空いた高さいっぱいに紙面を広げると、
そのたびに絵が跳ねて読む邪魔になります。
そこで紙面は、ヘッダーとフッターが出ている状態の残り領域の中央に固定しました。
上下に帯は出ますが、ツールバーを出し入れしても紙面は1pxも動きません。
高さの基準は 100dvh。100vh だとアドレスバーのぶん下端が切れます。
DATA PIPELINE
蔵書データの下ごしらえ
中古で買った本を自分で裁断・スキャンした PDF なので、紙そのものの状態が読みやすさに直結しました。 ビューアを直すだけでは解けないぶんは、3,724冊ぶんのデータ側を作り直しています。
古い巻は紙が日焼けして、白地が黄ばんで読みづらい
紙の色を推定して白に戻すスクリプトを書きました。明るい画素の中央値を紙の色とみなし
(実測 R247 G241 B200)、それが 255 になるよう各チャンネルを伸ばします。
計算はページ単位です。1ページが3枚の帯画像に分かれている巻があり、
帯ごとに計算すると継ぎ目に段差が出るためです。
難しかったのはカラーページを巻き込まないこと。日焼けは「R≈G > B」に偏った
一色のかぶりなので、それ以外の色みを持つ画素の割合で見分け、カラーは一切触りません
(白黒 0.000 / カラー表紙 0.35〜0.41)。
PDF は画像だけ差し替えるので、ページ数も読書進捗もそのまま使えます。
見開きで右に来るページが、巻によって1つずれている
右綴じの本は偶数ページが右・奇数ページが左です。ずれていると、
本来つながっている2ページが左右に分かれてしまいます。
そこでページ下部のノンブル(印字されたページ番号)を OCR で読み、
印字番号 − PDFページ番号 を求めました。
この差が奇数なら正しく、偶数ならずれていると判定できます。
ずれている巻はPDF の先頭に白紙を1枚入れて位相をずらします。
DB のフラグではなく PDF 自体に焼くので、DB が飛んでも失われません。
絵の一部を数字と誤読する問題は、偶数は右下・奇数は左下という印字位置の規則に
反する読みを捨てることで潰しました。
3,724冊に OCR をかけると終わらない
律速は OCR そのものではなくプロセスの起動コストでした。 切り抜きを縦に連結して1回の呼び出しにまとめ、17倍速くしています。 このとき切り抜きの高さを 90px に揃えると精度も上がりました(有効票 21→36)。 さらに、同じ作品の巻は前付けの構成が同じで差の値もそろうので、 数冊だけ精査して作品の基準を作り、残りは矛盾しないかだけ確認しています。
機械では最後まで決まらない巻が残る
1,499冊を判定して803冊に白紙を入れ、事後の全数検証では読み取れた1,276冊のうち 1,202冊(94%)が正しく、逆だった3冊は訂正しました。 名探偵コナン107冊はノンブルが裁ち落としで読めず、実機で確認して全冊に入れています。 残りは綴じ側の余白の広さが同じリップ元の中でそろうと分かったので、 かたまりごとに1冊だけ正解を教えてもらえば自動判定できる見込みです。
BACKEND
外出先から開くまでの待ち時間
自宅のサーバーに VPN でつないで外出先から読めるようにしたところ、 1冊目が出るまで45秒かかる作品がありました。しかも作品によって差が10倍以上あります。 回線・アプリ・PDF そのものと順に切り分けた結果、手を入れたのは すべてサーバー側とデータ構造でした。
全ページ受け取るまで1枚目が出ない
ファイルを丸ごと受け取ってから描き始めていました。
必要な範囲だけを小分けに要求する設定に変え、先読みも自動任せをやめて
こちらから指示しています。1枚目が出た時点で読み始められ、残りは読んでいる間に届きます。
読んでいる裏では先の10ページ・戻る2ページを取りに行き、
ページを送るたびの待ちも消しました。巻を替えたときに前の巻の先読みが残らないよう、
先読みには通し番号を持たせて古い回の結果は捨てています。
同じ容量なのに、作品によって10倍以上遅い
PDF はページの所在を ページツリー(page tree)という木構造で持ちます。
ところが遅い巻は階層が作られておらず、ルートの /Pages 直下に
全ページが一列に並んでいました。1ページ目を取り出すだけで
ファイルの端から端まで読みに行くことになります。
手元のディスクなら一瞬なので気づけず、細い回線で初めて表面化しました。
16ページごとに中間の /Pages ノードを挟んで階層化する
スクリプトを書いて組み直しています。
ページの中身・枚数・順序・ファイルサイズは一切変わりません。
蔵書の本体を一括で書き換える作業なので、別ファイルに書き出して検証し、
通ったものだけ差し替える方式にしました。対象の2,853冊を処理し、
全3,708冊を事後検証してページ数の変化・描画失敗ともに0件です。
| 呪術廻戦 18巻 | 平坦なまま | 階層化後 |
|---|---|---|
| 1ページ目が出るまで | 45.0秒 | 3.1秒 |
| 落とした量 | 1.66MB | 0.05MB |
| 要求回数 | 219件 | 13件 |
外出先の回線を想定し 遅延150ms・1.5Mbps で計測。 ファイルの中身は変えていないので、差は構造だけから出ています。
RETROSPECTIVE
振り返り・工夫した点
BUILD, THEN CUT
必要な機能をひととおり作ってから、使いながら要らないものを削る進め方をしました。 操作ボタンは当初の8つ想定から4つまで落としています。 机上で数を絞ると「念のため」が残りますが、実際に読んでみると 押さないボタンははっきり分かる。 足すより削るほうが難しく、そのぶん画面は確実にシンプルになりました。
1374× FASTER LIBRARY SCAN
ライブラリ走査の is_file() を撤去し、拡張子だけで判定するように変更しました。
13秒 → 9ms(約1374倍)まで短縮し、開いた瞬間に一覧が出る体感になっています。
45s → 3.1s REMOTE OPEN
外出先で1冊目が出るまで45秒かかっていた原因は、回線ではなく PDF のページツリーが階層化されていなかったことでした。 16ページごとに中間ノードを挟んで階層化し 3.1秒(取得量 1.66MB → 0.05MB)。 「遅い」を体感で終わらせず、どの層の問題かを測って切り分けられたのが一番の収穫です。
RESUME WHERE YOU LEFT OFF
ページ移動・モード切替・白紙挿入を 1.5秒デバウンスで自動保存しています。
タブを閉じる時は navigator.sendBeacon で確実に書き込み、
次回起動時にページ位置・綴じ方向・表示モードまで完全復元されます。
SECURITY FIRST
bcrypt によるパスワードハッシュ、PDO プリペアドステートメントでの SQLi 対策、
ログイン必須化により /api/image.php を直接叩いても 401 を返す設計にしました。
ONE PERSON, FULL STACK
インフラ (XAMPP / Apache Alias) ・ DB スキーマ・ API・SPA フロント・運用までを一人で回し、 各レイヤーのトレードオフを実感を持って理解できました。
LIVE
公開デモ(ゲスト閲覧)
下記URLからゲストとして実際に触れます。ログイン不要で、 「試し読み」作品(青空文庫の名作)を閲覧・ページめくりできます。 自炊した蔵書・編集機能は安全のため非公開です。