PDFのサムネイルが真っ白になる。エラーはどこにも出ない

PDFのサムネイルが真っ白になる現象を表したアイキャッチ WordPress
この記事は約8分で読めます。

古い Ghostscript は、描けなかったページをエラーとして報告しません。正しい寸法の、真っ白なページを返します。

だから、受け取ったものをそのまま保存する実装は、白い画像を保存します。ファイルはできる。サイズもある。成功したように見える。利用者から見えるのは「PDF はある、絵は出ない、理由はどこにも書いていない」という状態だけです。

自作プラグインの 1.4.1 で直したのは、ここでした。やっていることは難しくありません。返ってきたものを見る。それだけです。

以下は、そこにたどり着くまでに何を測って、何を間違えたかの記録です。

症状は、エラーが出ないことでした

PDF をアップロードして、サムネイルが真っ白になる。ログには何も出ません。PHP のエラーも、ImageMagick の警告も、WordPress の通知も。

ファイルは生成されています。16,910 バイトありました。壊れていれば分かりやすいのですが、壊れていない。開けば表示される。ただ、真っ白です。

測ってみると、画素値の標準偏差が 0 でした。全画素が同じ1色。成功したファイルと失敗したファイルの見分けが、サイズでは付きません。

環境は Xserver の sv17007、ImageMagick 6.9.13-25、Ghostscript 9.27、PHP 8.3.33 の FPM です。対象の PDF は 1,194,335 バイトの PowerPoint 出力で、透明グループの中に画像 XObject が14個入っている構造でした。

最初は生成の経路を疑い続けました

サムネイルが出ないなら、作るところが壊れている。そう考えて、生成の処理を何度も読み直しました。

これが遠回りでした。ファイルは正しく作られていて、白かっただけです。しかも途中で、直したのに変わらない回が何度かありました。ブラウザのキャッシュが古かったためです。

そこで方針を変えました。画面を見るのをやめて、ファイルを測る。生成されたファイルを直接開いて、画素の標準偏差を出す。0 なら白紙、そうでなければ絵が出ている。判定が真偽値ひとつになったので、以降の切り分けが速くなりました。

余談ですが、この「画面ではなくファイルを測る」は、以前 Cocoon のサムネイルがぼやけた件でも同じことをしています。あのときは開発者ツールで imgsrc を見て、ファイル名に -320x180 と入っているのを確認しました。見ている画面と、そこに届いているファイルは別物です。何度学んでも、次にまた画面を疑います。

PDF は壊れていませんでした

次に疑ったのが PDF 側です。壊れたファイルなら、描画に失敗するのは当然です。

調べました。xref は正常。/Length の宣言値と実データが91件すべて一致。qQ のグラフィック状態のスタックが393対で釣り合っている。構造としては、どこも壊れていません。

ここで qpdf による修復も試しましたが、直すものがないので何も変わりませんでした。別のレンダラに差し替える案も検討して、これは実装の重さで落としています。

仮説を潰していって、残ったのが環境の差でした。

4つの環境で、同じファイルを描かせました

Local Xserver 旧 sv17007
ImageMagick 7.1.1-15 6.9.10-68 6.9.13-25
Ghostscript 10.04.0 9.25 9.27
このファイル 描ける 真っ白 真っ白(1.4.0 まで)

ImageMagick のバージョンは、6.9.10 と 6.9.13 で結果が同じでした。7.1.1 だけ描けています。ただ、Ghostscript のほうを見ると 10.04 だけが描けている。差が出ているのは ImageMagick ではなく Ghostscript の世代でした。

Local の gs 10.04 で同じファイルを処理すると、622,848 バイトの画像が出ます。警告は1つも出ません。9.27 では白紙が出て、こちらも警告は出ない。どちらも黙って結果を返します。片方は絵で、片方は白紙で。

回避策は見つかりましたが、置き場所がありませんでした

Ghostscript には -dNOTRANSPARENCY というオプションがあります。透明の処理を切って描く指定です。

試すと、はっきり効きました。

デバイスを変えて確かめると、png16m で標準偏差 0.353、pngalpha で 0.347、pamcmyk32 で 0.551。どのデバイスでも絵が出ています。

問題は、このオプションをどこで渡すかでした。

delegates.xml は読まれていませんでした

ImageMagick が外部コマンドを呼ぶときの設定は、通常 delegates.xml にあります。ここに Ghostscript の呼び出し行があるので、そこへオプションを足せばいい。そう考えて書き換えました。

変わりませんでした。

決め手は -debug delegate でした。デリゲートの呼び出しをログに出すオプションです。実行しても、何も出ません。呼ばれていない。

調べると、この環境の ImageMagick は --with-gslib でビルドされていました。Ghostscript を外部コマンドとして起動するのではなく、共有ライブラリとして直接リンクしています。delegates.xml を経由しないので、書き換えても効きません。

残っていたのは環境変数でした

ライブラリとして組み込まれている場合でも、Ghostscript は GS_OPTIONS 環境変数を読みます。ここに -dNOTRANSPARENCY を入れておけば、呼び出しの経路に関係なく届くとのことでした。

これで通りました。delegates.xml を数時間触っていたのが、結局は環境変数1つでした。

常時ではなく、再試行にしました

ここが、設計として一番迷ったところです。

-dNOTRANSPARENCY を常に付ければ、白紙は出なくなります。実装も簡単です。ただ、このオプションは透明の処理を切ります。透明を正しく使っている PDF では、見た目が変わります。半透明の重なりが潰れる。影が消える。

gs 10 系のホストでは、そもそもこの問題が起きません。そこに常時オプションを付けると、直す必要のない環境で画質を落とすことになります。

だから、こうしました。まず通常どおり生成する。できたファイルの標準偏差を測る。0 なら白紙と判定して、-dNOTRANSPARENCY を付けてもう一度やり直す。

白紙が出なければ、オプションは付きません。gs 10 系のホストでは、この分岐に入ることすらない。実損が出る側を、必要なときだけに絞りました。

処理が2回になるぶん遅くなりますが、遅くなるのは白紙が出たときだけです。その場合、1回目の結果は捨てるものなので、無駄になっているのは元から失敗していた処理です。

同じリクエストの中で、4つの実装に作らせました

直ったかどうかを確かめるために、比較計測をしました。

条件を揃えるのが難しい計測です。時間をずらすと、サーバーの状態が変わります。別のサーバーだと環境が違う。そこで、同じアップロード処理の中で、4つの実装にサムネイルを作らせました。

実装 ファイルサイズ 標準偏差
WordPress 標準の PDF プレビュー 16,910 bytes 0
ほかのプラグイン A 398 bytes 0
ほかのプラグイン B 66,955 bytes 0
Rapls PDF Image Creator 1.4.1 274,800 bytes 0.310

同一リクエストで4つの実装が生成したサムネイルのファイルサイズ比較。3つは標準偏差0の白紙で、1つだけ0.310の絵が出ている

同じリクエスト、同じファイル、同じサーバー、同じ Ghostscript。3つが白紙を保存して、1つが絵を出しました。

ほかのプラグインの名前は伏せます。測ったのは2つだけで、ファイルは1本、サーバーは1台です。この結果から「ほかは白紙になる」とは言えません。それに、白紙を保存するのは欠落であって故障ではない。gs がエラーを出さない以上、検査していない実装が通してしまうのは当然の帰結です。

WordPress のコアについても同じです。これはバグではありません。コアは Ghostscript が返したものを保存しているだけで、設計として検査していないというだけの話です。

手順を1度間違えました

最初の計測は、やり直しています。

ひとつは、WordPress のコアが同じ命名規則で割り込んでいたことです。コアの生成したサムネイルとプラグインの生成したものが、同じ名前で保存される場面がありました。どちらを測っているのか分からなくなります。

もうひとつは、AVIF 変換のプラグインが元ファイルを消していたことです。測ろうとしたファイルが、測る前に無くなっている。

どちらも、比較そのものとは関係のない事故です。ただ、1回目の数字はこれで汚れていました。計測の結果を信じる前に、計測の手順が汚れていないかを見る必要があります。

150×150 の白紙が64KBになっていました

比較計測の副産物です。

ほかのプラグインが生成した白紙の JPEG が、150×150 で 64,197 バイトありました。白一色の150×150 が64KBというのは、明らかに大きい。

調べると、ICC プロファイルが埋め込まれていました。カラープロファイルは環境によっては数十KBあるそうです。画像本体が白一色で数百バイトでも、プロファイルが乗ればこのサイズになる。

この件は白紙とは別の話ですが、サムネイルのように大量に生成される画像で、毎回プロファイルが埋まっているのは効いてきます。

色の変換についても測ってあります。ICC を使わない簡易変換と、プロファイルを使った変換で、色差 ΔE が 32.1 ありました。ΔE 32 は、並べれば別の色に見える差です。

直ったこと、直っていないこと

1.4.1 が保証しているのは、白紙を保存しないことです。白紙を検出したら再試行する。それでも描けなければ、白紙を保存する代わりに失敗として扱います。

「真っ白にならない」とは書けません。ホスト側の Ghostscript が原因で、再試行しても描けないケースは残ります。守れない約束をすると、レビュー欄で返ってきます。

Ghostscript のバグを直したわけでもありません。回避しているだけです。根本的には、ホスティング事業者に Ghostscript の更新を依頼するのが確実です。

設定画面の Status タブに「Ghostscript」の行が出ていれば、そのサーバーで回避策が使われたということです。出ていなければ、この問題は起きていません。

プラグインの詳細は製品ページにあります。無料版は WordPress.org から入ります。

関連記事

WordPress
この記事を書いた人
rapls

WordPressのプラグインを作っているフリーランスエンジニアです。Web開発はもう6年以上。WordPress.orgで Rapls AI Chatbot、Thanks Mail for Stripe、Rapls PDF Image Creator、Prime Cache の4本を公開し、保守を続けながら、日本語ロケールの翻訳エディター(PTE)も務めています。このブログに書くのは、現場で自分が実際にハマって、調べて、直した話です。

raplsをフォローする
raplsをフォローする

コメント

タイトルとURLをコピーしました