PHPで画像縮小したりobject-fitのようにトリミングするサンプル
画像掲示板のようなシステムはもちろんのこと、サポート用の問い合わせフォームにスクリーンショットを添付したり、業務システムであっても社員の証明写真を添付したり、いまやPHPでWebシステムを組むとき、画像ファイルも考慮に入れるのは必須と言えるでしょう。
そしてユーザーから任意のサイズの画像ファイルがアップロードされる可能性がある以上、PHP側では必ず縮小処理が必要になります。
基本的には拡張機能であるGD2のimagecreateほにゃらら系の関数を使うのが一般的だと思いますが、今回はPHPでCSSの object-fit:contain; や object-fit:cover; と同じような縮小やトリミングをする方法について、備忘録を兼ねて記事にしておきます。
PHPで画像縮小する際の基本
PHPが使えるレンタルサーバーで拡張機能のGD2が使えないところはないと思いますが、自前のWindows開発環境ならphp.iniの ;extension=gd2 のコメントを外す必要がありますのでお忘れなく。
GD2のサンプルというととかく長いものですが、条件を限定して解説するとけっこうシンプルです。
例えばこんな感じ。
<?php
//画像ロード
$image1 = imagecreatefromjpeg('01.jpg');
//白紙の画像を作成
$image2 = ImageCreateTrueColor(640, 480);
//リサイズ処理
imagecopyresampled($image2, $image1, 0, 0, 0, 0, 640, 480, 1920, 1080);
//JPEG 品質85で保存
imagejpeg($image2, '01s.jpg', 85);
?>
- 01.jpg(1920x1080)をロード
- 縮小サイズ(640x480)の空画像をメモリ中に作成
- ロードした画像を空画像へリサイズしながらコピー
- 画像を01s.jpgとして保存
ファイル名も、画像種別も、サイズまで決め打ちなので使い回しは出来ませんが、とりあえずざっくりと動きを理解できるかと思います。
コードがだんだん長くなりますが、やっていることはほとんど変わりません。GIFやPNGに対応したり、ファイル名や画像サイズを任意に指定したり、いい感じの大きさや位置にするために少し計算するだけのことです。
GD2は画像種別によって使う関数が異なる
GD2は画像の種類(GIF/JPEG/PNG)によって呼ぶ関数を変えなくてはならないため、実際の運用ではこんなふうに分岐する必要があります。
function createThumb($filename1, $filename2)
{
//画像ロード
list($w1, $h1, $type) = getimagesize($filename1);
switch ($type) {
case 1://GIF
$image1 = imagecreatefromgif($filename1);
break;
case 2://JPEG
$image1 = imagecreatefromjpeg($filename1);
break;
case 3://PNG
$image1 = imagecreatefrompng($filename1);
break;
default:
return false;
}
}
どのみち元画像($filename1)の幅と高さも必要になってくるので、getimagesize関数で一度に取ってしまうのが一般的でしょう。
画像のロードと同じく、保存時も関数を変えなくてはならないので、これと同じように分岐が必要です。
例)
//変換した画像をファイルに保存
switch ($type) {
case 1://GIF
imagegif($image2, $filename2);
break;
case 2://JPEG
imagejpeg($image2, $filename2, 85);
break;
case 3://PNG
imagepng($image2, $filename2);
break;
}
※JPEGの場合はとりあえず品質85で固定しちゃっています。
サイズを指定して縮小してみる
上述のロードと保存は長ったらしいので省略して記述します。
$w2 = 200;
$h2 = 200;
$image2 = ImageCreateTrueColor($w2, $h2);
//縮小しながらコピー
imagecopyresampled($image2, $image1, 0, 0, 0, 0, $w2, $h2, $w1, $h1);
画像ロード時に幅は$w1、高さは$h1に保存されているため、それを幅$w2高さ$h2にリサイズする、という動きです。
これを実際動かすとどうなるかというと、
■元ファイル
■変換後
なんか見覚えありますね…。
以前の記事「IE11でもobject-fitを使う一番簡単な方法」で例に出したIE11でimgタグにwidth/heightを指定した際の結果と同じです。横長写真はスリムにされ、縦長写真はデブにされる感じ。
縦横比を気にせずに縮小していますから当然そんなふうになります。
長辺基準で縮小したい場合
横長だろうと縦長だろうと、長い辺は必ず200px以下にしたい。つまり、200px×200pxの四角形の中に全部おさまりつつも被写体がひしゃげないようにしたい場合の例を挙げます。要するにCSSでいうところの object-fit:contain; みたいな感じ。
fitContain(200, $w1, $h1, $w2, $h2);
$image2 = ImageCreateTrueColor($w2, $h2);
//縮小しながらコピー
imagecopyresampled($image2, $image1, 0, 0, 0, 0, $w2, $h2, $w1, $h1);
//長辺基準でリサイズする際の幅、高さを計算する
function fitContain($resize, $w1, $h1, &$w2, &$h2)
{
if ($w1 > $h1) {
$base = $w1; //横長画像
} else {
$base = $h1; //縦長画像(もしくは正方形)
}
//リサイズしたいサイズとの縮小比率を算出
$rate = ($base / $resize);
if ($rate > 1) {
$w2 = floor((1 / $rate) * $w1);
$h2 = floor((1 / $rate) * $h1);
} else {
//リサイズしたいサイズ以下の場合にはサイズ変更しない
$w2 = $w1;
$h2 = $h1;
}
}
うーん、かなり昔に書いたソースなのでいまいちな気がする……。ま、まぁ動くのでヨシ。
imagecopyresampled関数は、変換元と変換先の画像情報を受け取ってリサイズしてくれる素敵な関数ですが、与えられた幅、高さを愚直に守ってリサイズするため、縦横比を保ったままリサイズしたいならこちら側で適切に計算してあげないといけません。
具体的には先述の横長写真は649x433pxなのですが、これを200x200px以内に収めるのなら、200x133pxの写真にしないといけないんですね。
そのため、元画像の横幅÷200pxで縦横比率を計算して、200pxを基準に高さを133pxと算出しているのが上述のfitContainなわけです。
横長写真だけじゃなくて、縦長写真でも同じように計算するため、ちょっとコードが長くなっていますが、やっていることはただ縦横比率を計算している(長いほうを短いほうで割っている)だけです。
これを実際に実行するとこんな感じ。
わかりやすいように200x200pxの灰色の枠で囲ってみましたが、どうでしょう。ちゃんと200x200pxの枠内に収まっていますよね。object-fit:contain;と同じ動作だと思います。
指定した矩形サイズでトリミングしたい
先述のように指定した枠内(200x200px)で縦横比を保った場合、縦横比の異なる、つまり縦長写真と横長写真が混在する環境ではデコボコな写真が並ぶことになります。
外側の枠線とかつければそんなに変ではないと思いますが、余白が出るのを好まないデザインもあるでしょうし、多少大胆にトリミング(カット)してでも枠内一杯に表示したいというケースもあるでしょう。そんなときに使われるのがCSSのobject-fit:cover;だったりするのですが、これを実際の画像加工でやってみます。
fitCover(200, $w1, $h1, $w2, $h2);
$image2 = ImageCreateTrueColor($w2, $h2);
//縮小しながらコピー
imagecopyresampled($image2, $image1, 0, 0, 0, 0, $w2, $h2, $w1, $h1);
//矩形範囲でトリミング(左上から切り取る)
function fitCover($resize, &$w1, &$h1, &$w2, &$h2)
{
$w2 = $resize; //出力先は問答無用で矩形範囲のサイズ
$h2 = $resize; //
if ($w1 > $h1) {
$w1 = $h1; //横長画像は幅を高さに合わせる
} else {
$h1 = $w1; //縦長画像は高さを幅に合わせる
}
}
最初の例のfitContainの代わりにfitCoverという関数を用意して、そこで幅、高さなどを計算しています。
fitContainとは異なり、縦横比の計算がないのですごくシンプルですね。
コメントでも書きましたが、横長写真は幅を高さに合わせる、縦長写真は高さを幅に合わせる、とただそれだけですからね。
実際に実行してみるとこんな感じになります。
ダメじゃん。いやたしかに200x200pxにトリミングされてますけど、写真の左上から切り取っているので、イケてません。
でも当たり前なんですよね。
//縮小しながらコピー
imagecopyresampled($image2, $image1, 0, 0, 0, 0, $w2, $h2, $w1, $h1);
このimagecopyresampledに0, 0, 0, 0と渡している部分がありますが、これはコピー元とコピー先のX/Y座標です。0,0だから当然左上からなわけです。
ということで、幅、高さに加えて、コピー元の開始座標も計算するようにしましょう。
指定した矩形サイズで真ん中からトリミングしたい
fitCoverの代わりにfitCover50関数を作りました。
(CSSのobject-position:50% 50%;に因んで)
$x = 0;
$y = 0;
fitCover50(200, $w1, $h1, $w2, $h2, $x, $y);
$image2 = ImageCreateTrueColor($w2, $h2);
//縮小しながらコピー
imagecopyresampled($image2, $image1, 0, 0, $x, $y, $w2, $h2, $w1, $h1);
//矩形範囲でトリミング(真ん中を切り取る)
function fitCover50($resize, &$w1, &$h1, &$w2, &$h2, &$x, &$y)
{
$w2 = $resize; //出力先は問答無用で矩形範囲のサイズ
$h2 = $resize; //
if ($w1 > $h1) {
$x = floor(($w1 - $h1) / 2); //開始位置調整
$w1 = $h1; //横長画像は幅を高さに合わせる
} else {
$y = floor(($h1 - $w1) / 2); //開始位置調整
$h1 = $w1; //縦長画像は高さを幅に合わせる
}
}
最初の例から先程の例まで、一貫して幅と高さの計算に終始していましたが、ここでようやくx/y座標の登場です。
でもやっていることは簡単。
長いほうと短いほうの差の半分だけ開始位置をズラしてあげているだけです。
横長写真なら「(幅-高さ)÷2」だし、縦長写真なら「(高さ-幅)÷2」の位置からコピーを開始するっていうわけです。
真ん中へんをトリミングするんですから、そういうことですよね。
実際に動かすとこんな感じ。
これでおおよそCSSのobject-fit:cover;と同じ動きじゃないかなと思います。
まとめ
今はほんとCSSが便利になりましたから、保存している写真が縦長/横長混在でも、object-fit:cover;いっぱつで綺麗なタイル状に表示してくれます。
なので、あまり使う機会もないのですけど、IE11とかobject-fit:cover;に対応していないブラウザもありますし、あるいはサムネイルのファイルサイズを1バイトでも削りたい、なんて環境もあるかも知れないので、PHPでobject-fitみたいに実画像を縮小するコードを紹介してみました。
参考にする人がいるかはわからないけれど、最後に全体ソースもアップしておきますね。
<?php
createThumb('01.jpg', '01s.jpg');
function createThumb($filename1, $filename2, $resize = 200)
{
//画像ロード
list($w1, $h1, $type) = getimagesize($filename1);
switch ($type) {
case 1://GIF
$image1 = imagecreatefromgif($filename1);
break;
case 2://JPEG
$image1 = imagecreatefromjpeg($filename1);
break;
case 3://PNG
$image1 = imagecreatefrompng($filename1);
break;
default:
return false;
}
$x = 0;
$y = 0;
fitCover50($resize, $w1, $h1, $w2, $h2, $x, $y);
//fitContain($resize, $w1, $h1, $w2, $h2);
$image2 = ImageCreateTrueColor($w2, $h2);
//縮小しながらコピー
imagecopyresampled($image2, $image1, 0, 0, $x, $y, $w2, $h2, $w1, $h1);
//変換した画像をファイルに保存
switch ($type) {
case 1://GIF
imagegif($image2, $filename2);
break;
case 2://JPEG
imagejpeg($image2, $filename2, 85);
break;
case 3://PNG
imagepng($image2, $filename2);
break;
}
//メモリ解放
ImageDestroy($image1);
ImageDestroy($image2);
}
//長辺基準でリサイズ
function fitContain($resize, $w1, $h1, &$w2, &$h2)
{
if ($w1 > $h1) {
$base = $w1; //横長画像
} else {
$base = $h1; //縦長画像(もしくは正方形)
}
//リサイズしたいサイズとの縮小比率を算出
$rate = ($base / $resize);
if ($rate > 1) {
$w2 = floor((1 / $rate) * $w1);
$h2 = floor((1 / $rate) * $h1);
} else {
//リサイズしたいサイズ以下の場合にはサイズ変更しない
$w2 = $w1;
$h2 = $h1;
}
}
//矩形範囲でトリミング(左上から切り取る)
function fitCover($resize, &$w1, &$h1, &$w2, &$h2)
{
$w2 = $resize; //出力先は問答無用で矩形範囲のサイズ
$h2 = $resize; //
if ($w1 > $h1) {
$w1 = $h1; //横長画像は幅を高さに合わせる
} else {
$h1 = $w1; //縦長画像は高さを幅に合わせる
}
}
//矩形範囲でトリミング(真ん中を切り取る)
function fitCover50($resize, &$w1, &$h1, &$w2, &$h2, &$x, &$y)
{
$w2 = $resize; //出力先は問答無用で矩形範囲のサイズ
$h2 = $resize; //
if ($w1 > $h1) {
$x = floor(($w1 - $h1) / 2); //開始位置調整
$w1 = $h1; //横長画像は幅を高さに合わせる
} else {
$y = floor(($h1 - $w1) / 2); //開始位置調整
$h1 = $w1; //縦長画像は高さを幅に合わせる
}
}
?>
あんまりテストしていないので、使うときはしっかりテストしてからどぞ(*´ω`*)