PHP: GDで画像のサイズ変更やサムネイル生成のやりかた

GDを使う動機のひとつとして、ImageMagicが使えないけどGDが入っているような環境で、画像のサイムネイルを作ることがありますが、APIが分かりにくく結構面倒な作業になります。

幸いにもPHPには、[Imagine](https://imagine.readthedocs.org/en/latest/#)というGDやImageMagicのAPIを抽象化して画像を処理できるライブラリがあります。が、サムネイル生成にImagineでは元画像の解像度が大きくなればなるほど、メモリの消費量が増えていくという欠点があり、1000x1000の[この画像](https://placehold.jp/1024x1024.jpg)を500x500にリサイズするだけで13.75MB消費します。従って、解像度の高い画像のサムネイルの作成については、直にGDのライブラリを使ったほうがメモリの消費を30〜50%くらい抑えられます。


*ImagineとGDのメモリ最大消費量*

| 画像サイズ | Imagine | GD |
|:-:|---:|---:|
| 1000x1000  | 13.75MB  | 6.00MB  |
| 1000x2000  | 27.25MB  | 10.25MB  |
| 1000x3000  | 40.50MB |  15.00MB |
| 1000x4000  | 54.25MB  | 19.50MB  |


## 画像のサイズを変形する関数

サムネイル作成のコードを一気に書く前に、画像のサイズを変形するだけの関数を実装しておくと便利です。この関数はアスペクト比の計算を気にしないので、実装時や改修時に混乱しにくて良いです。

```php
/**
 * 画像のサイズを変形して保存する
 * @param string $srcPath
 * @param string $dstPath
 * @param int $width
 * @param int $height
 */
function transform_image_size($srcPath, $dstPath, $width, $height)
{
    list($originalWidth, $originalHeight, $type) = getimagesize($srcPath);
    switch ($type) {
        case IMAGETYPE_JPEG:
            $source = imagecreatefromjpeg($srcPath);
            break;
        case IMAGETYPE_PNG:
            $source = imagecreatefrompng($srcPath);
            break;
        case IMAGETYPE_GIF:
            $source = imagecreatefromgif($srcPath);
            break;
        default:
            throw new RuntimeException("サポートしていない画像形式です: $type");
    }

    $canvas = imagecreatetruecolor($width, $height);
    imagecopyresampled($canvas, $source, 0, 0, 0, 0, $width, $height, $originalWidth, $originalHeight);
    imagejpeg($canvas, $dstPath);
    imagedestroy($source);
    imagedestroy($canvas);
}
```

## アスペクト比を計算するアルゴリズム

次にアスペクト比を計算するアルゴリズムを実装します。いろんなアルゴリズムがあると思いますが、ここで紹介するのは内接サイズを計算するアルゴリズムになります。

```php
/**
 * 内接サイズを計算する
 * @param int $width
 * @param int $height
 * @param int $containerWidth
 * @param int $containerHeight
 * @return array
 */
function get_contain_size($width, $height, $containerWidth, $containerHeight)
{
    $ratio = $width / $height;
    $containerRatio = $containerWidth / $containerHeight;
    if ($ratio > $containerRatio) {
        return [$containerWidth, intval($containerWidth / $ratio)];
    } else {
        return [intval($containerHeight * $ratio), $containerHeight];
    }
}
```

こんな感じで、コンテナのサイズから、最適な内接サイズをタプルで返します。

```php
php > var_dump(get_contain_size(500, 500, 100, 100));
array(2) {
  [0]=>
  int(100)
  [1]=>
  int(100)
}
php > var_dump(get_contain_size(640, 480, 100, 100));
array(2) {
  [0]=>
  int(100)
  [1]=>
  int(75)
}
php > var_dump(get_contain_size(50, 50, 100, 100));
array(2) {
  [0]=>
  int(100)
  [1]=>
  int(100)
}
```

## サムネイルを作る関数を実装する

最後に、上の2つの関数を組み合わせてサムネイルを作成する関数を実装します。

```php
/**
 * 画像のサムネイルを保存する
 * @param string $srcPath
 * @param string $dstPath
 * @param int $maxWidth
 * @param int $maxHeight
 */
function make_thumbnail($srcPath, $dstPath, $maxWidth, $maxHeight)
{
    list($originalWidth, $originalHeight) = getimagesize($srcPath);
    list($canvasWidth, $canvasHeight) = get_contain_size($originalWidth, $originalHeight, $maxWidth, $maxHeight);
    transform_image_size($srcPath, $dstPath, $canvasWidth, $canvasHeight);
}
```

## おわり

GDを使って画像のサイズ変更・アスペクト比を維持したサイムネイル画像の生成方法を紹介しました。もうPHPで画像処理するのはやめたいです(´・ω・｀)