imagecreatefromjpegのメモリリミットエラーに対処
PHPはメモリを消費しながら動作します。
特に、画像投稿をする場合により多くのメモリを消費します。
なぜならば、画像データそのものがメモリサイズを消費するためです。
今回画像投稿していたところ、次のようなエラーが発生しました。
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 36864 bytes) in
ググって調べてみると、対処方法はすぐわかりました。
ただここで疑問がわきました。
メモリリミットエラーが発生する場合、事前にエラーとしてはじくことはできないものか?
そのあたりも考察と実験をしてみましたので、レポートとして記載します。
今回現象が発生したソース
今回以下のような、画像を縮小する処理でメモリリミットエラーが発生しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<?php $file = "test.jpg"; // 画像情報取得 $file_info = getimagesize($file); // 現在の画像の横幅、高さ $now_width = $file_info[0]; $now_height = $file_info[1]; // 縮小するサイズ設定 $set_width = 400; $set_height = (int)($set_width * $now_height / $now_width); img_size_conv($file, $now_width, $now_height, $set_width, $set_height); function img_size_conv($file, $now_width, $now_height, $set_width, $set_height) { // 画像編集可能形式に変換 $base_images = imagecreatefromjpeg($file); // 下地画像作成 $conv_images = imagecreatetruecolor($set_width, $set_height); // 画像変換 imagecopyresampled($conv_images, $base_images, 0, 0, 0, 0, $set_width, $set_height, $now_width, $now_height); // 画像出力 imagejpeg($conv_images, "output.jpg"); // GD解放 imagedestroy($base_images); imagedestroy($conv_images); } ?> |
メモリリミットエラーの原因
メモリリミットエラーは、次の理由で発生します。
- 消費するメモリが、使用できるメモリの限界値を突破したとき
具体的には、メモリ使用限界値が128MBの場合、129MB以上消費しようとするとエラーになります。
PHPにはmemory_limit値というものがあります。
PHPで使用できるメモリを制限するための設定です。
要はサーバーに搭載されているメモリ全部をPHPで使用したら、サーバー自体が破綻します。
そうならないように、上限値を設定しているものです。
とりあえず次の1行を記述するだけで解決しました
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<?php ini_set("memory_limit", "512M"); $file = "test.jpg"; // 画像情報取得 $file_info = getimagesize($file); // 現在の画像の横幅、高さ $now_width = $file_info[0]; $now_height = $file_info[1]; // 縮小するサイズ設定 $set_width = 400; $set_height = (int)($set_width * $now_height / $now_width); img_size_conv($file, $now_width, $now_height, $set_width, $set_height); function img_size_conv($file, $now_width, $now_height, $set_width, $set_height) { // 画像編集可能形式に変換 $base_images = imagecreatefromjpeg($file); // 下地画像作成 $conv_images = imagecreatetruecolor($set_width, $set_height); // 画像変換 imagecopyresampled($conv_images, $base_images, 0, 0, 0, 0, $set_width, $set_height, $now_width, $now_height); // 画像出力 imagejpeg($conv_images, "output.jpg"); // GD解放 imagedestroy($base_images); imagedestroy($conv_images); } ?> |
最初にini_set(“memory_limit”, “512M”); を記述したらメモリリミットエラーが出なくなりました。
今回memory_limit値の初期値は128MBだったのですが、一時的に512MBに増やすことで解決しました。
ちなみに今回のメモリの使用量を測定してみたところ、画像本体の読み込みだけで約190MB使用していました。
確かにメモリリミットエラーの条件を満たしていました。
でもここで疑問がわきました。
疑問:画像サイズは3MBなのになぜメモリ消費量は190MBもあるのか?
今回現象が発生した画像サイズは約3MBです。
また、他の10MBの画像を投稿してもメモリリミットエラーになりませんでした。
次の疑問を感じました。
- たかだか3MBの画像が約64倍のサイズに膨れ上がるのはなぜなのか?
- 画像サイズが小さいほうがメモリリミットになるのはなぜなのか?
そこでその理由をググって調べてみました。
すると次のことがわかりました。
画像のメモリ消費量は解像度で決まる
PHPで読み込んだ画像で消費されるメモリの計算方法を調べたところ、次のような計算式であることがわかりました。
- 画像の横サイズ(ピクセル)×画像の縦サイズ(ピクセル)×4
画像の消費メモリ量は、画像の横サイズと縦サイズで決まるということがわかりました。
例えば(1)3MBの横2,000ピクセル縦1,500ピクセルの画像と、(2)10MBの横1,000ピクセル縦800ピクセルの場合、(1)のほうがメモリ消費量が多くなります。
×4はなんなのか?
横×縦というのは何となく理解できますが、×4は何の意味があるのでしょうか?
調べてみると、次のような意味でした。
画像は次の4要素で作られています。
R(赤)/G(緑)/B(青)/A(アルファ、透明度)
それぞれ、1バイトずつ使用します。
つまり面積1平方ピクセルあたり4バイト使用することになります。
なので、画像の面積の4倍がメモリ消費量になるという理屈です。
具体的にメモリサイズを計算してみた
実際に計算してみました。
■横2,000ピクセル 縦1,500ピクセルの場合
2,000ピクセル×1,500ピクセル×4バイト = 12,000,000バイト(約11.44MB)
■横1,000ピクセル 縦800ピクセルの場合
1,000ピクセル×800ピクセル×4バイト = 3,200,000バイト(約3,05MB)
■今回現象が発生した画像の場合
8,640ピクセル×5,760ピクセル×4バイト = 199,065,600バイト(約189,84MB)
疑問:画像の解像度(密度)はメモリ消費量に影響しないのか?
画像の面積がメモリ消費量に影響することは理解できました。
ですが、ここで疑問がわきました。
画像の密度はメモリの消費量に影響しないのか?
PHOTOSHOPを使用する場合、解像度72、300、350という設定があります。
これは画面に出力する場合、印刷物として出力する場合の画像密度です。
当然画像密度が高いほど画像サイズは大きくなります。
画像密度が高いほどメモリ消費量が多くなるような気がします。
ですが、実際に試してみたところ画像密度はメモリ消費量に全く影響しませんでした。
画像解像度を上げてその分横幅、縦幅が小さくなるように調整したところ、画像サイズはほぼ同じなのに、メモリ消費量が10分の1ほどに下がりました。
確かにメモリの消費量は画像の面積しか関係していませんでした。
これらを踏まえてメモリリミットエラーを事前にはじく処理をいれてみました。
今回メモリリミットエラーはGDライブラリのimagecreatefromjpeg()関数で発生していました。
この関数は、エラーコードを出力せず、メモリリミットエラーが発生すると、関数内で中断していました。
そのため、エラーコードによる処理中断ができません。
try catch処理を入れてみましたが、うまく作動しませんでした。
どうやら、メモリリミットエラーの場合はtry catch処理でさえ無視されるようでした。
解決方法としては、事前に消費するメモリ量を計算し、memory_limit値より大きい場合はエラーとして処理をしないという方法がよさそうです。
そこで、画像のメモリ消費量計算を利用して、メモリ消費予定量でエラー判定する処理を入れてみました。
※ただしこのままでは正しく動作しません。後述しますが落とし穴があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
<?php $file = "test.jpg"; // 画像情報取得 $file_info = getimagesize($file); var_dump($file_info); // 現在の画像の横幅、高さ $now_width = $file_info[0]; $now_height = $file_info[1]; // 縮小するサイズ設定 $set_width = 400; $set_height = (int)($set_width * $now_height / $now_width); // メモリリミットサイズを取得する $memory_limit = filter_var(ini_get("memory_limit"), FILTER_SANITIZE_NUMBER_INT); // 画像の使用するおおよそのメモリサイズを求める(MB) $memory_size = ($now_width * $now_height * 4) / (1024 * 1024); if ($memory_size > $memory_limit) { echo "メモリリミットサイズを超えます"; exit; } img_size_conv($file, $now_width, $now_height, $set_width, $set_height); function img_size_conv($file, $now_width, $now_height, $set_width, $set_height) { // 画像編集可能形式に変換 $base_images = imagecreatefromjpeg($file); // 下地画像作成 $conv_images = imagecreatetruecolor($set_width, $set_height); // 画像変換 imagecopyresampled($conv_images, $base_images, 0, 0, 0, 0, $set_width, $set_height, $now_width, $now_height); // 画像出力 imagejpeg($conv_images, "output.jpg"); // GD解放 imagedestroy($base_images); imagedestroy($conv_images); } ?> |
画像が使用するメモリがmemory_limit値を超えた場合、エラーとするようにしました。
これで完璧!
と思ったら、落とし穴がありました。
PHPにはその他のメモリ消費がある
今回の例では、画像読み込み時に約190MBのメモリ消費がありました。
なので、memory_limit値を200MBに設定してテストしてみたところ、メモリリミットエラーが発生しました。
なんでだろうと思いながら、何MBにすればエラーにならなくなるか調べてみました。
そしてmemory_limit値を208MBに設定すると、メモリリミットエラーが出なくなりました。
画像が消費するメモリ190MBに対し、誤差が約18MBありました。
なんでだろう?
と思って調べてみると、PHPがそもそも使用するメモリ消費であることがわかりました。
確かにそれはそうです。
メモリ消費量でエラーをはじくためには、「画像が消費するメモリ量」+「PHPが普段使用するメモリ消費量」でチェックしなければいけなかったのです。
しかし、ここでさらに疑問がわきました。
PHPが普段使用するメモリ消費量ってどうやって求めればいいの?
PHPが普段使用するメモリ量は千差万別
PHPが使用するメモリ量は、測定すれば求められます。
なので、測定処理の結果をもとに計算すれば理屈上はOKです。
だけれども、処理をする前に測定結果を求めることはできません。
今回は、処理を実行する前にエラーとしてはじきたいのです。
メモリ消費量を測定で求めることはできません。
ならばどうすればよいのだろう?
と考えましたが、今のところ次のやり方しか見つけられませんでした。
- 固定値を割り振って調整する
あらかじめ測定を繰り返し、PHPが普段使用するメモリ消費量に当たりをつけ、固定値として計算する方法です。もし、誤差が出たら固定値を再度見直すというやり方です。
要は「画像のメモリ消費量」+「PHPが使用すると思われる消費量(固定値)」で計算します。
プログラムの環境や、使用するライブラリで値が異なるため、一様にどのくらいと設定できるものではありません。ですが、おそらくこれで大丈夫だろうという値を決めておけば、メモリリミットエラーを事前にはじくことができます。
そのサンプルがこちらです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
<?php // メモリ消費量初期値(100MB) $consumption = (100 * 1024 * 1024); $file = "test.jpg"; // 画像情報取得 $file_info = getimagesize($file); var_dump($file_info); // 現在の画像の横幅、高さ $now_width = $file_info[0]; $now_height = $file_info[1]; // 縮小するサイズ設定 $set_width = 400; $set_height = (int)($set_width * $now_height / $now_width); // メモリリミットサイズを取得する $memory_limit = filter_var(ini_get("memory_limit"), FILTER_SANITIZE_NUMBER_INT); // 画像の使用するおおよそのメモリサイズを求める(MB) $memory_size = (($now_width * $now_height * 4) + $consumption) / (1024 * 1024); if ($memory_size > $memory_limit) { echo "メモリリミットサイズを超えます"; exit; } img_size_conv($file, $now_width, $now_height, $set_width, $set_height); function img_size_conv($file, $now_width, $now_height, $set_width, $set_height) { // 画像編集可能形式に変換 $base_images = imagecreatefromjpeg($file); // 下地画像作成 $conv_images = imagecreatetruecolor($set_width, $set_height); // 画像変換 imagecopyresampled($conv_images, $base_images, 0, 0, 0, 0, $set_width, $set_height, $now_width, $now_height); // 画像出力 imagejpeg($conv_images, "output.jpg"); // GD解放 imagedestroy($base_images); imagedestroy($conv_images); } ?> |
この例では、PHPの通常メモリ消費量を100MBと仮定して計算しています。
PHPのメモリ消費量が100MB以内であれば正しく動作します。
結論:メモリリミットエラーを回避する処理はケースバイケースで対応
仮定メモリ消費量で計算する方法のほかに、memory_limit値の半分を超えたらエラーとする方法もありますが、帯に短したすきに長しです。
画像のメモリ消費量の上限値を設定する方法がもしかしたら一番現実的かもしれません。
確かに100MB以上を消費する画像は大きすぎます。
ただその場合、画像サイズが1MBしかなくても、投稿エラーになるケースが発生します。
少なくとも、スマホで撮影した写真はサイズエラーにならないようにする必要があります。
メモリリミットエラーの対応方法としては以下のようにすることがよいといえます。
- memory_limit値を大きくする
- memory_limit値を変更できない場合は、画像のピクセルサイズ、消費メモリ量等でエラー処理をする