メモ

覚書です

OpenPNE-2.12.5を少し見学(Img.php)

2008-11-01 23:35:32 | プログラミング
●その1 PNGについて
事の発端はOpenPNEなサイトを利用していて遭遇した出来事。
同じPNG画像で圧縮度を変えただけのものを何種類か用意してアップロードしてみました。
img.phpで表示される画像を見てみると、どれもアップロードしたサイズとは違うようで、そして圧縮度に関わらず同じサイズで表示されてました。
GIFでも試してみました。無圧縮GIFやGIF(RLE)、GIF(LZW)などです。(無圧縮とかRLEとかなんか懐かしい^^;)
img.phpで表示される画像はサイズがそれぞれ違いアップロードしたものと同じサイズでした。
どうやらPNGファイルだけ他の画像と扱いが違うみたいでした。

ちょっと不思議に思ったのでOpenPNE-2.12.5のソースを見てみました。

webapp/lib/OpenPNE/Img.phpを覗いてみると、リサイズやフォーマット変換が不要な場合には、次の関数でキャッシュを作成しているようです。

function create_cache_from_raw_img()
{
    $this->create_cache_subdir();

    if ($this->output_format == 'png') {
        touch($this->cache_fullpath);
        $output_gdimg = imagecreatefromstring($this->raw_img);
        imagepng($output_gdimg, $this->cache_fullpath);
    } else {
        $handle = fopen($this->cache_fullpath, 'wb');
        fwrite($handle, $this->raw_img);
        fclose($handle);
    }
}

ここを見るとPNGだけ処理を変えています。
PNGの圧縮率を変えてアップロードしても、表示するときに反映されない理由は分かりました。
いったんimagepng(defaultの圧縮度はいくつかな?)で出力しなおしているわけですね。
256色以下のファイルでパレットの整理をした画像についてはパレットをそのままいかしてくれて反映されるのかな。

いちおう他の画像フォーマットと同じ処理にすると、アップロードされたデータそのままがキャッシュに書き込まれますね。
でも何らかの必要性があって、わざわざ処理を変えているんだろうな。
後で調べてみよう。

推測
不正なPNGファイルでUAが落ちたりする?
高い圧縮率だと表示できないUAがある?


※続き
2.3.0のソースが公開されていたので、眺めてみましたが、すでに同じ処理になってました。
2.3.0はいつリリースされたのかな。2年前かぁ。
ちょっと検索しただけではすぐには分かりそうにありませんでした。
何か経緯があったんでしょうね。
pngだけ一手間かけているんだから理由がきっとあるんだろう。
携帯向けはjpgに変換してるみたいだし関係ないよね?

#別に困っているわけではないです。純粋な興味です。

imagecreatefromstringで画像かどうか確認しているのかな?
でもエラー処理はしていないしなぁ。
アップロード時にチェックしているような気もするから不要な気もする。
それにpng以外はそのまま書き出しているし。

PHPのバージョンによる動作の違いとかがあるのかも。
ということであまり深く考えないことにします。


●その2 プログレッシブJpeg
これは上のソースを眺めていて気づいた点。
携帯向けにプログレッシブJpegをノーマルJpegにしているところがあるけど、効果がない場合があるようです。

        if ($this->requests['f'] == 'jpg') {
            // JPEGの場合、携帯対応
            imageinterlace($source_gdimg, 0);
        }


resize_imgを追ってみたが、ぱっと見おかしな点は見つからない。
もう少しちゃんと調べてみる。
そして分かったこと。
resize_imgがfalseじゃない場合、imageinterlace($source_gdimg, 0)を使わなくてもプログレッシブJpegにはならないみたい。プログレッシブだという情報が失われている模様。create_cacheが無視するのかな?(訂正。後述します。)

そしてテストでアップロードした画像の場合、w=240&h=320(600,600も)を指定している時にリサイズが不要と判断されてfalseが返ってきて、create_cache_from_raw_img()を呼び出しているようです。
そのため携帯でアクセスしていてもプログレッシブJpegのままのデータがキャッシュに書き込まれます。
もしプログレッシブJpegを携帯向けにノーマルJpegにどうしても変更したいならresize_imgでfalseが返ってきても$this->create_cacheが呼ばれるようにしないといけないでしょうね。
というわけで以下のように変更してみました。

        //キャッシュを生成
        if ($output_img) {
            $this->create_cache($output_img);
        } else {
            if ($this->requests['f'] == 'jpg') {
                // JPEGの場合、携帯対応
                $this->create_cache($source_gdimg);
            } else {
                $this->create_cache_from_raw_img();
            }
        }

いまどきの携帯事情を知らないので、不要なことかも知れません^^;
またcreate_cacheがプログレッシブな情報も維持するよう(だ|にな)ったらimageinterlaceの部分も必要ですね。(削る必要はないですね)(訂正。後述)

#訂正
create_cacheは問題ありませんでした。
その大元ですでにプログレッシブかどうかの情報が失われるんですね。
$source_gdimg = imagecreatefromstring($this->raw_img)
この時点で失われています。
その後にimageinterlace($source_gdimg, 1);をためしに入れてみましたが、次はresize_imgでリサイズされると、プログレッシブの情報が失われるようです。
imagecopyresampledできっと失われているんでしょう。
PCで日記に表示されるサムネイルがプログレッシブからノーマルJpegになっているのも、これが原因なのでしょう。
またインターレースPNGやGIFもサムネイル(リサイズされた)画像はインターレースが解除されていました。
でもリサイズされないインターレースPNGについてはimagecreatefromstring($this->raw_img)としているのにインターレースが解除されていないのが不思議です。Jpegと挙動が違うのが気にはなります。
リサイズされたPNGについてはresize_imgでJpegと比べると色々と操作しているので、その中で失われているのかも。(単にimagecopyresampledかも?)
あまり突き詰めるとPHPのソースまで追いかける感じになりそうなので、追求はやめました。
GD関数の(問題|仕様)?ですね。

※動作確認環境
Debian lenny(coLinux)
apache-mpm-prefork-2.2.9-10 (Debian)
PHP4.4.4-8+etch6(libapache2-mod-php4)

追記
PHP5.2.6-5(with Suhosin-Patch)(libapache2-mod-php5)でも確認してみましたが同じでした。


追記2
プログレッシブJpegとインターレースPNG、インターレースGIFについて、もう少し調べてみました。
imagecreatefromstring($this->raw_img)によって作成されたGDイメージについてimageinterlace($source_gdimg)で返り値を取得してみました。プログレッシブJpegは0、インターレースPNGとGIFは1が返ってきます。
またimageinterlace($source_gdimg, 1)でインターレースビットをOnにしていても、imagecopyresampledはインターレースビットを受け継がないようです。

追記3
GDのソースに踏み込むつもりはなかったのですが、ついでなので調べてみました。

プログレッシブJpegをGDイメージに読み込んでもインターレースビットがOnにならないのは、どうやらgdImageCreateFromJpegの仕様のようです。
PHPに限らずgd-2.0.35を利用しているのなら、同じ仕様になっていると思われます。

gdImageCreateFromPng、gdImageCreateFromGifを使ってファイルから読み込んで確認してみると、インターレースビットがちゃんと設定されました。
gdImageCreateFromJpegについてはgd-1.8.4ではインターレースビットが設定されていましたが、gd-2.0.35で確認するとインターレースビットが0になります。

ソースを確認すると1.8.4のgdImageCreateFromJpegCtxの中に、
gdImageInterlace(im, cinfo.progressive_mode != 0);
とありますが、2.0.35になるとコメント付で

#if 0
gdImageInterlace(im, cinfo.progressive_mode != 0);
#endif

となっていました。
コメントを読むと、プログレッシブかどうかを正しく判断する方法がないため、ということのようです。
そしてプログレッシブで出力したかったら自分でgdImageInterlace(im, 1)ってしてくれってことかな。(英語苦手なので間違っているかも)
1.8.4でインターレースビットが取得できていたのは、私が用意したJpegがたまたまプログレッシブと判断できるものだったから、なんでしょう。

またJpegに限らずPNGやGIFも含めて、gdImageCopy、gdImageCopyResizedを使った場合、どちらのバージョンでも、インターレースビットはコピーされないようです。gdImageCopyResampled(2.0.35)でもインターレースは引き継がれないみたいです。
PNGやGIFについて、インターレースを引き継いで表示したい場合には、元イメージのインターレースビットをgdImageGetInterlaced(im)で取得して、それをコピー後のイメージに設定するっていう形になりそうです。

#gd-2.0.35はhttp://www.libgd.org/から入手。
#libgd2-xpm-dev(2.0.33)、libgd-dev(1.8.4)パッケージで確認。

このあたりで多分、きっと終了。


#追記4
終了しませんでした。
インターレースビットをリサイズ後もそのまま引き継ぐように書き換えてみました。
PNGは無事引き継がれるようになったけど、GIFは引き継がれない。
試しにgdImageGifでファイルに書き出してみたが、インターレースが消えている。

gd_gif_out.cを調べてみる。
GIFEncode(とGIFAnimEncode)の中で、次のようになってた。

    GifCtx ctx;
    ctx.Interlace = GInterlace;
    ctx.in_count = 1;
    memset(&ctx, 0, sizeof(ctx));

ctx.Interlaceに代入した後、ctx構造体が0で埋められてる。
ctx.Interlace、ctx.in_countともに0になって代入した意味がない。

gd-2.0.36RC1のソースも確認したが、同じでした。
とりあえずlibgdについては機械翻訳な英語でバグ報告してみた。

libgd2-xpmのソースパッケージを修正してリビルド。
インストールしてApacheを再起動してみると、意図した通りに動いてくれました。
一件落着。


#今更ながらOpenPNEに加えた変更点。
上に書いた「キャッシュを生成」の変更に加えて、resize_img関数のimagecopyresampledの後ろに、

imageinterlace($output_gdimg, imageinterlace($source_gdimg));

を追加しました。