古い 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 のサムネイルがぼやけた件でも同じことをしています。あのときは開発者ツールで img の src を見て、ファイル名に -320x180 と入っているのを確認しました。見ている画面と、そこに届いているファイルは別物です。何度学んでも、次にまた画面を疑います。
PDF は壊れていませんでした
次に疑ったのが PDF 側です。壊れたファイルなら、描画に失敗するのは当然です。
調べました。xref は正常。/Length の宣言値と実データが91件すべて一致。q と Q のグラフィック状態のスタックが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 というオプションがあります。透明の処理を切って描く指定です。
試すと、はっきり効きました。
| 1 2 | なし 11,018 bytes 白紙 -dNOTRANSPARENCY 624,109 bytes 絵 |
デバイスを変えて確かめると、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 |

同じリクエスト、同じファイル、同じサーバー、同じ 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 から入ります。
関連記事
- Cocoon でサムネイルがぼやけた|Retina 設定が見つからなかった話 ── 画面ではなくファイルを測る、という同じ手順で追った別の件です
- Composer を積んだプラグインが、PHP のバージョンで白画面を出す ── 同じプラグインで出した、別の不具合の記録です
- 501 Not Implemented が出た|Xserver の WAF で WordPress が弾かれる ── サーバー側の事情で、書いたコードが通らなくなる話です



コメント