imgproxyで画像処理

## はじめに

Webアプリケーション開発において、画像管理は避けては通れない課題です。
最適化された画像の提供はユーザー体験の向上だけでなく、パフォーマンスの最適化にもつながります。

画像のリサイズやフォーマット変換を行うためには、適切なツール・インフラの選定が必要です。
この記事では、imgproxyという画像プロキシサーバーを使った画像処理について紹介します。

### imgproxyとは

imgproxyは、高速でセキュアなオープンソースの画像プロキシサーバーで、URLパラメータを利用して、画像のリサイズやフォーマット変換などの処理を行います。
Go言語と画像処理ライブラリlibvipsを使って実装されています。

https://docs.imgproxy.net/

## 起動方法

https://docs.imgproxy.net/installation

本記事では公式提供されているDockerイメージとdocker compose コマンドを使ってimgproxyを起動します。

下記のymlファイルを用意してください。

```compose.yml
services:
  imgproxy:
    image: ghcr.io/imgproxy/imgproxy:latest
    ports:
      - "8080:8080"
```

ymlファイルの準備ができたら、docker composeでコンテナを起動しましょう。

```shell
docker compose up
```

ブラウザで http://localhost:8080 にアクセスして下記のようなページが表示されれば起動成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222451/5574689a-1b60-0926-1c2c-79ce2e34868d.png)

### 実際に使ってみる

imgproxyを使って画像をリサイズしてみましょう。

- 使用する画像URLはこちら → [URL](https://mars.nasa.gov/system/downloadable_items/40368_PIA22228.jpg)
    - Nasaが公開している画像(7680x4320)
    - imgproxyの公式ドキュメントでも使用されています

上記の画像を300x300にリサイズします。
imgproxyコンテナを起動させた状態で、下記にアクセスしてください。

```text
http://localhost:8080/insecure/rs:fill:300:300/plain/https://mars.nasa.gov/system/downloadable_items/40368_PIA22228.jpg
```

ブラウザに表示された画面のスクショです。
300x300にリサイズされた画像が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222451/3da3c35d-a3e4-bfb7-f90c-d4146be85454.png)

ここでは、簡単な例を紹介しました。実際は署名を使ってセキュアにしたり、元画像のURLを難読化するなど設定して使うことになります。

詳しくは後述していきます。

## URLパラメータ

imgproxyのURLパラメータについて説明していきます。

URLパラメータは、署名、処理オプション、画像ソースURL、拡張子(オプション)で構成されますが、処理対象の画像URL(以降、ソース画像URL)の形式によって若干異なります。

### パターン1: ソース画像URLがそのまま

```text
http://localhost:8080/{signature}/{processing_options}/plain/{source_url}@{extension}
```

- `{source_url}`にはソース画像URLがそのまま入る
- `/{source_url}`の前に`/plain`というパスが入る

### パターン2: ソース画像URLをbase64でエンコード

```text
http://localhost:8080/{signature}/{processing_options}/{encoded_source_url}.{extension}
```

- `{encoded_source_url}`には、Base64でエンコードされたソース画像URLが入る

### パターン3: ソース画像URLを暗号化（PRO版）

```text
http://localhost:8080/{signature}/{processing_options}/enc/{encrypted_source_url}.{extension}
```

- `{encrypted_source_url}`にはAES-CBCで暗号化されたソース画像URLが入る
- `/{encrypted_source_url}`の前に`/enc`というパスが入る

### URLパラメータまとめ

以上が基本的なURLのパターンになります。
URLパラメータを表にまとめました。

| パラメータ | 意味 | 参考リンク |
| ----- | ----- | ----- |
| signature | [署名](#署名) (署名しない場合`insecure`) | [Signing a URL](https://docs.imgproxy.net/usage/signing_url) |
| processing_options | [処理オプション](#処理オプション) (リサイズのパラメータなど)　| [Processing an image](https://docs.imgproxy.net/usage/processing#processing-options) |
| source_url | ソース画像URL | [Source URL](https://docs.imgproxy.net/usage/processing#source-url) |
| encoded_source_url | [Base64エンコードしたソース画像URL](#base64エンコードしたソース画像url) | [Base64 encoded](https://docs.imgproxy.net/usage/processing#base64-encoded) |
| encrypted_source_url | 暗号化したソース画像URL(PRO版) |  [Encrypted with AES-CBC](https://docs.imgproxy.net/usage/processing#encrypted-with-aes-cbc) |
| extension | 拡張子(オプション) | [Extension](https://docs.imgproxy.net/usage/processing#extension) |

各パラメータの設定方法についても後述していきます。

## Base64エンコードしたソース画像URL

タイトルそのままですが、
URLセーフなBase64形式にエンコードしたソース画像URLのことです。

### Base64エンコードしたソース画像URLの生成方法

Pythonで実装してみました。

```python
def generate_base64_source_url(source_url: str) -> str:
    return base64.urlsafe_b64encode(source_url.encode()).decode()

if __name__ == "__main__":
    source_url = source_url = "https://mars.nasa.gov/system/downloadable_items/40368_PIA22228.jpg"
    print(generate_base64_source_url(source_url))
    # aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvNDAzNjhfUElBMjIyMjguanBn
```

## 処理オプション

対象の画像に適応する操作（リサイズ、クロップ、回転など）を指定します。

主な処理オプションを表に記載しました。

| オプション | 説明 | 例 |
| --- | --- | --- |
| `resize` (`rs`) | リサイズ方法[^1]とサイズを指定 | `rs:fill:300:300` |
| `crop` (`c`) | クロップする領域を指定 | `crop:1000:1000` |
| `rotate` (`rot`) | 回転する角度を指定(90度単位でサポート) | `rotate:180` |
| `blur` (`bl`) | ぼかしを指定 | `blur:30` |
| `background` (`bg`) | 塗りつぶす背景色を指定(RGBまたはHEX) | `bg:255:153:0`, `bg:ff9900` |

[^1]: リサイズの方法については[こちら](https://docs.imgproxy.net/usage/processing#resizing-type)を参照してください

### 処理オプションの例

例えば、
500x500の範囲でクロップし、画像アスペクト比を維持せず300x100にリサイズするなら、処理オプションは下記の文字列になります。

```text
insecure/crop:500:500/resize:force:300:100
```

以下は実際に処理をかけて表示された画像です。
微妙な画像が出来上がりました笑

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222451/2383d6bb-1ca8-5b42-6688-82cbaf9e2b42.png)

これ以外にもさまざまな処理方法が存在します。

https://docs.imgproxy.net/usage/processing#processing-options


## 署名

画像の処理をセキュアに行うための「署名」について紹介します。

署名は画像URLが改竄されていないか確認するための暗号化されたトークンです。画像処理を外部からの不正なリクエストを防ぐことができるためセキュリティを強化できます。

### 署名の有効化

環境変数`IMGPROXY_KEY`と`IMGPROXY_SALT`を設定することで署名を有効化できます。

`IMGPROXY_KEY`と`IMGPROXY_SALT`は、

- `IMGPROXY_KEY`: 16進数でエンコードされたキー
- `IMGPROXY_SALT`: 16進数でエンコードされたソルト

である必要があります。

下記コマンドを使用してキーとソルトを簡単に生成することができます。

```shell
echo $(xxd -g 2 -l 64 -p /dev/random | tr -d '\n')
```

上記で生成したキーとソルトを`IMGPROXY_KEY`と`IMGPROXY_SALT`に設定し、コンテナを再起動してください。

```yml
services:
  imgproxy:
    image: ghcr.io/imgproxy/imgproxy:latest
    ports:
      - "8080:8080"
    environment:
      - IMGPROXY_KEY=16進数でエンコードされたキー
      - IMGPROXY_SALT=16進数でエンコードされたソルト
```

:::note warn
`IMGPROXY_KEY`と`IMGPROXY_SALT`は外部に公開されると署名の意味がなくなるので管理には注意が必要です。
:::

### 署名URLの生成方法

署名付きURLの生成手順は下記の通りです。

1. 署名より後のURLパスを定義しエンコード(エンコードURLパス)
1. キーとソルト+エンコードURLパスを組み合わせてSHA256で暗号化されたHMAC署名を生成
1. 生成されたHMAC署名をBase64エンコードしてURLセーフな形式に変換
1. 上記の署名をURLパスのsigunatureに入れて署名付きURLが完成

といってもわかりづらいと思うので、公式の[Example](https://github.com/imgproxy/imgproxy/blob/master/examples/signature.py)を参考にPythonで署名付きURL生成関数を作成しました。

```python
import base64
import hashlib
import hmac


def generate_base64_source_url(source_url: str) -> str:
    return base64.urlsafe_b64encode(source_url.encode()).decode()


def generate_signature_url_path(key: str, salt: str, path: str) -> str:
    key = bytes.fromhex(key)
    salt = bytes.fromhex(salt)
    path = path.encode()
    digest = hmac.new(key, msg=salt+path, digestmod=hashlib.sha256).digest()
    signature = base64.urlsafe_b64encode(digest).rstrip(b"=")
    signature_url_path = b'/%s%s' % (signature, path)
    return signature_url_path.decode()


if __name__ == "__main__":
    key = "017fd0c61f868beb9b66d74b6a9f1275"
    salt = "ab6d92e9e65e4cd804c502412acb9617"

    process_option = "/rs:fit:300:300"
    source_url = "https://mars.nasa.gov/system/downloadable_items/40368_PIA22228.jpg"
    b64_source_url = generate_base64_source_url(source_url)
    
    path = f"{process_option}/{b64_source_url}"

    signature_url_path = generate_signature_url_path(key, salt, path)
    print(signature_url_path)
    # => /YVu2QQCmvaQFTGkZ_l_NF0bT1ri6JfpEYsaX8mzJq1E/rs:fit:300:300/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvNDAzNjhfUElBMjIyMjguanBn
```

生成されたURLパスにアクセスしてみるとリサイズされた画像が表示されます。

```
http://localhost:8080/YVu2QQCmvaQFTGkZ_l_NF0bT1ri6JfpEYsaX8mzJq1E/rs:fit:300:300/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvNDAzNjhfUElBMjIyMjguanBn
```

下記はブラウザに表示された画面のスクショです。
無事、リサイズされていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222451/352d1849-d666-45c2-a4ea-059c01abd1da.png)


### 不正なリクエストを検証してみる

上記で生成されたURLパスの`rs:fit:300:300`を`rs:fit:100:100`に変更して画像処理部分を変更してリクエストしてみましょう。

```
http://localhost:8080/YVu2QQCmvaQFTGkZ_l_NF0bT1ri6JfpEYsaX8mzJq1E/rs:fit:100:100/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvNDAzNjhfUElBMjIyMjguanBn
```

すると403エラーForbiddenが返ってきます。
URLパラメータを改ざんできないことが確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222451/8ef30325-fcbc-9397-1d72-9164e0243ee8.png)

## おまけ

クラウドストレージ上の画像をimgproxyで処理する方法について紹介します。
弊社でよく使っているGoogle Cloud Storage(GCS) を対象とします。


## まとめ

本記事では、imgproxyを使った画像処理について紹介しました。
imgproxyは高速で安全な画像プロキシサーバーです。豊富な画像処理オプションが存在し、署名によって不正なアクセスを防ぐ機能があります。
導入することで画像処理のロジックをWebアプリケーションから切り離すことができ、サービスの最適化にもつながってきます。

基本的な操作のみの説明になりましたが、imgproxyにはソース画像URLの制限やクラウドストレージとの連携、キャッシュの設定などWebアプリケーションで使用するために必要なさまざまな設定があります。
弊社では、特定GCSバケット内の画像に限定する設定を加え、CloudRun上に起動しています。

記事をみてimgproxyが気になった方はぜひ探ってみてください。

## おわりに

ここまで読んでいただきありがとうございます！
引き続きアドベントカレンダー続きますのでよければ明日以降の記事もご覧ください！

https://qiita.com/advent-calendar/2024/intimatemerger

弊社インティメート・マージャーに少しでも興味を持った方は下記もご覧ください！！

https://recruit.intimatemerger.com/

https://corp.intimatemerger.com/