問題点

PHPで大きな画像をGDバインディングで処理する際、「Fatal error: Allowed memory size of XX bytes exhausted (tried to allocate XX bytes) in XXXX」が発生することがあります。これは、リサンプリングに必要なメモリ消費量に対して、サーバからPHPに割り当てられているメモリ量が少ないために発生します。

原因

通常、PHPのGDバインディングで画像をリサンプリングするには、以下の2つのデータ用にサーバ上でメモリを確保する必要があります。

  1. 元画像をGDフォーマットに展開したデータ
  2. リサンプリング画像の雛形となるGDフォーマットデータ

1.で確保したGDフォーマットのデータからリサンプリング後のピクセルを算出して2.の雛形に流し込むことで、リサンプリングを行っています。もし画像が大きすぎる場合は、1.の途中でメモリを使い切ることになります。エラーの原因はこれです。



標準的な対策

現在一般的に対策とされているのは、スクリプトに「ini_set("memory_limit", "(任意の数値)M");」を記述することで、そのスクリプトの実行セッション限定で、PHPへの割り当てメモリ量を増やす方法です。さっとググってみた限り、たいていは128Mや256Mが設定されているようです。

付録 > php.ini ディレクティブ > コア php.ini ディレクティブに関する説明 > リソース制限 @ php.net

名前 デフォルト 変更の可否 変更履歴
memory_limit "128M" PHP_INI_ALL PHP 5.2.0 より前は "8M"、PHP 5.2.0 では "16M"

以下に設定ディレクティブに関する簡単な説明を示します。
memory_limit integer
スクリプトが確保できる最大メモリをバイト数で指定します。この命令は、 正しく書かれていないスクリプトがサーバーのメモリを食いつぶすことを防止するのに役立ちます。 もし、使用可能メモリに制限を設けたくない場合は、 ここに -1 を指定してください。
PHP 5.2.1 より前のバージョンでは、このディレクティブを使うためには、 コンパイル時に configure で --enable-memory-limit を指定しなければなりません。 このコンパイルフラグは、関数 memory_get_usage() および memory_get_peak_usage() を 5.2.1 より前のバージョンで使う際にも必要となります。
integerを使用する際、その値はバイト単位で測られます。 この FAQ に記載された短縮表記を使用することも可能です。

なお、以下を参照すると、PHP_INI_ALLはどこででも設定できることを意味するようです。そのため、ini_set() でも設定できるようです。ini_setを制限するような設定はなさそうなので、たぶんどんなサーバでも有効かと思います。

インストールと設定 > 実行時設定 > どこで設定を行うのか @ php.net

なお、PHP5.3以降は128MBが標準となるようなので、このエラーにお目にかかることはまずなくなるかと思います。

今回の対策

さて、今回はこの対策は使っていません。エラーの直接的な回避方法をMediumオブジェクトのリサンプリングメソッド「getResamplerBinary」に実装してみました。
これは、リサンプリングにおいて消費しそうなメモリーの量を前もって計算することで、サーバにおいてPHPに割り当てられているメモリサイズでリサンプリング可能かどうか判定し、超過しそうな場合は処理を中止するというものです。あまりにも大きな画像ファイルはリサンプリングされないようになっています。

/nucleus/plugins/mediautils/Medium.php 153行目から168行目
(計算式はバイトオーダー)

// check current available memory
$memorymax = trim(ini_get("memory_limit"));
switch (strtolower ($memorymax[strlen($memorymax)-1])) {
case 'g':
$memorymax *= 1024;
case 'm':
$memorymax *= 1024;
case 'k':
$memorymax *= 1024;
}

// these codes are based on analyzing gd.c in php source code
// if you can read C/C++, please check these elements and notify us if you have some ideas
if ((memory_get_usage()
+ ($this->resampledwidth * $this->resampledheight * 5 + $this->resampledheight * 24 + 10000)
+ ($this->width * $this->height * 5 + $this->height * 24 + 10000))
> $memorymax) {
return FALSE;
}

なお、この計算式はkatsumiさんの協力のもとで求めました。

サーバ環境においては、PHPとは別なプロセスで起動したGDライブラリに処理を投げるようになっているものがあります(Debian系のPHPパッケージがこんな感じ)。この場合はPHPがメモリ管理を行わないため、どんなサイズの画像もリサンプリング出来る能力があります。

しかしこの場合も、先述のメモリ超過判定によって処理が中止されます。これは、GDライブラリが別プロセスかどうかを判定する方法を、私が知らないためです。そのため、このようなサーバ環境の場合は、サーバの能力をフルに発揮できないこととなります。

メモリーの予想使用量は、PHPのソースコードからgdに関するものを参照して求めました。メモリブロックなどの計算に不慣れなので、現在は実メモリ消費量を計算式に反映しました。

将来的には別プロセスなGDライブラリに処理を投げるのが主流となるだろうと思いますので、ひょっとしたら近い将来、この判定式は必要なくなるかもしれません。というか、そうなったらいいなぁ程度なんですけどね。。。

たぶんそれなりに妥当な計算式となっているかと思います。が、もし先述のメモリ超過エラーが発生した場合は、報告してくださると助かります。計算式の見直しをします。