羅針盤 技術航海日誌

株式会社羅針盤の技術ブログです

できるだけWebPで画像をホスティングしたい、そんな私に捧げる話


この記事は羅針盤 アドベントカレンダー 2024の6日目の記事です。
qiita.com

5日目の記事は クリエイティブ職向け macOS 個人的おすすめツール 8つ - 羅針盤 技術航海日誌 でした。


こんにちは、羅針盤の森川です。
最近のマイブームはマッシュルームです。生のままスライスしてオリーブオイルと黒胡椒を振るだけでワインが美味しく飲めます。
マッシュルームは安くていい感じなんですが、オリーブオイルが高騰しすぎて泣きそうです。

2024年色んなことがありましたね(2回目)。
気づいたら世界の97%がWebP対応していました。うぇっぴー!

caniuse.com

PageSpeed Insights でも指摘されるので、SEOが重要なWebサイトの画像はもうJPGはやめてWebPにすることにしました。

少量の画像変換だったらGoogleのSquooshでいいんですが、毎回何枚、何十枚となるとなかなか面倒です。

squoosh.app

webP化

cwebp コマンドを使うとJPEG、PNGをWebPに変換できます。

cwebp -q 80 image.png -o image.webp

developers.google.com

これを使って一括変換すればいいんですが、「これ使ってください、お願いします〜♪」って渡された画像ファイルちゃん達にはJPEGだけでなく、たまにHEICが混じってたり、縦横比がおかしかったり、文字化けしていたりとなんだか不穏です。 文字化けはもはやどうしようもないですが、他を解決するためにはどうしたらいいでしょうか?

tl;dr こちらを使ってください(ただしmac用)

#!/bin/bash

########################################################################
# 概要:
# 指定ディレクトリ内の画像ファイルをWebP形式に変換し、リサイズするスクリプト
########################################################################

### 設定値: 環境変数で指定する ###
# 入力元ディレクトリ
WEBP_INPUT_DIR=${WEBP_INPUT_DIR-'.'}
# 出力先ディレクトリ・プリフィクス
WEBP_OUTPUT_DIR=${WEBP_OUTPUT_DIR-'output/'}
# 出力サイズ: 横幅
WEBP_TARGET_WIDTH=${WEBP_TARGET_WIDTH-500}
# 出力サイズ: 正方形にするか (0: しない, 1: する)
WEBP_TARGET_SQUARE=${WEBP_TARGET_SQUARE-0}
# 一時ディレクトリ(基本的には変更不要)
WEBP_TEMP_DIR=${WEBP_TEMP_DIR-'/tmp/webp'}


# ==============================================


# メイン処理
function run {
    check_dependency
    copy_temp_to_square
    convert_webp
}

# 依存ツールの確認
function check_dependency {
    type cwebp >/dev/null 2>&1 || { echo >&2 "[error] 'cwebp' not installed. (e.g. 'brew install webp')"; exit 1; }
    type identify >/dev/null 2>&1 || { echo >&2 "[error] 'imagemagick' not installed. (e.g. 'brew install imagemagick')"; exit 1; }
}

# 正方形化と一時ディレクトリへのコピー
function copy_temp_to_square {
    mkdir -p ${WEBP_TEMP_DIR}
    for img in `find_image ${WEBP_INPUT_DIR}`; do
        local width=(`identify -format '%w' $img`)
        local height=(`identify -format '%h' $img`)
        local file_name=$(basename $img) # 拡張子有り
        local file_name_base=${file_name%.*}   # 拡張子抜き
        local file_ext=${img##*.}        # 拡張子のみ
        # 小文字に統一
        file_ext=`echo $file_ext | tr '[:upper:]' '[:lower:]'`

        if [ ${width} -eq ${height} -o "${WEBP_TARGET_SQUARE}" = "0" ]; then
          if [ "${file_ext}" = "heic" ]; then
            # HEICの場合はjpgに変換
            echo "[info] convert HEIC: ${file_name}"
            convert $img ${WEBP_TEMP_DIR}/${file_name_base}.jpg
          else
            # 既に正方形・または正方形化指定なしの場合はそのままファイルコピー
            cp $img ${WEBP_TEMP_DIR}/${file_name}
          fi
          continue
        fi

        # 正方形化
        local crop_size=${width}
        if [ ${width} -gt ${height} ]; then
            # heightに合わせてcrop
            crop_size=${height}
        fi

        local output_file_name=${file_name}
        if [ "${file_ext}" = "heic" ]; then
            # HEICの場合はjpgに変換
            output_file_name=${file_name_base}.jpg
        fi
        echo "[info] crop: ${file_name}"
        convert $img -gravity center -crop ${crop_size}x${crop_size}+0+0 ${WEBP_TEMP_DIR}/${output_file_name}
    done
}

# WebP変換
function convert_webp {
    mkdir -p ${WEBP_OUTPUT_DIR}
    for img in `find_image ${WEBP_TEMP_DIR}`; do
        echo "[info] convert: ${img}"
        local width=(`identify -format '%w' $img`)
        if [ ${width} -gt ${WEBP_TARGET_WIDTH} ]; then
            local resize_opt="-resize ${WEBP_TARGET_WIDTH} 0"
        fi
        cwebp -metadata icc -sharp_yuv  -q 80 $img ${resize_opt} -o ${WEBP_OUTPUT_DIR}$(basename ${img%.*}).webp > /dev/null 2>&1
        rm -f $img
    done
}

# 指定ディレクトリから画像ファイルを検索
function find_image {
    local dir=${1-'.'}
    find -E ${dir} -type f -iregex ".*\.(png|jpg|jpeg|gif|heic)"
}

# 実行
run

とりえあず webp(cwebp) と imagemagick (convert と identify) が入っていればなんとか使えると思います。

簡単に解説しておくと、copy_temp_to_square で画像を一時ディレクトリに保存します。正方形化が必要な場合は正方形にするし、HEICの場合はJPEGに変換します。

一時ディレクトリに保存された補正済みのJPEG/PNGファイルを convert_webp で、対象のディレクトリにWebP化して保存しています。

プルリクエストでチェック

画像専用のリポジトリを用意していて、そこでプルリクエスト出してもらってチェックしています。 とりあえずファイルサイズが大きいものが無いか、GitHub Actionsで機械的に判定します。

name: Large File Warning

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  lfs_warning:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: LFS Warning
        uses: ppremk/[email protected]
        with:
          # accepts 'b', 'mb', 'gb' only
          filesizelimit: '307200b'  # 300kb
          labelName: 'warning/large-file'
          labelColor: 'ff69b4'

ここでは300KBにしていますが、画面全体に広がる画像等ではファイルサイズが超えることもあるのでそこは仕方ないです。 トリミング漏れやWebP漏れが無いか自分で気付きやすいようにするためのワークフローです。

なお、WebPはGitHubで見づらいので後日Chrome Extensionを紹介します。

GCSへrsync

GCSへのファイル同期もGitHub Actionsを使います。

まずはステージングと本番共通の reuseable workflow から説明します。

# _common-rsync-static.yml

name: Rsync data to GCS (reuseable workflow)

on:
  workflow_call:
    inputs:
      TARGET_DIR:
        required: true
        type: string
      ENV_NAME:
        required: true
        type: string
      ENV_URL:
        required: true
        type: string
    secrets:
      github-token:
        required: true
      GCP_PROJECT_ID:
        required: true
      GCP_PROJECT_NUMBER:
        required: true

jobs:
  rsync_gcs:
    runs-on: ubuntu-latest
    timeout-minutes: 8
    permissions:
      id-token: write
      contents: read
      deployments: write

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      # ※1
      - name: Check if on main branch for prod dir
        if: ${{ inputs.TARGET_DIR == 'prod' && github.ref != 'refs/heads/main' }}
        run: |
          echo "'prod' dir can only run on the main branch."
          exit 1

      # ※2
      - name: Create deployment on GitHub
        uses: chrnorm/deployment-action@releases/v2
        id: deployment
        with:
          token: "${{ secrets.GITHUB_TOKEN }}"
          environment: ${{ inputs.ENV_NAME }}
          environment-url: ${{ inputs.ENV_URL }}
          initial-status: "in_progress"

      # ※3
      - id: "auth"
        uses: "google-github-actions/auth@v2"
        with:
          workload_identity_provider: projects/${{ secrets.GCP_PROJECT_NUMBER }}/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider
          service_account: image-deploy-github-actions@${{ secrets.GCP_PROJECT_ID }}.iam.gserviceaccount.com
          project_id: ${{ secrets.GCP_PROJECT_ID }}

      # ※3
      - name: "Set up Cloud SDK"
        uses: "google-github-actions/setup-gcloud@v2"
        with:
          project_id: ${{ secrets.GCP_PROJECT_ID }}

      # ※4
      # 先頭が . 以外のファイル・ディレクトリを全てアップロード(差分は削除される)
      - name: Rsync to GCS
        run: |
          gsutil -m rsync -x '(^|/)\.' -r -d ./static.example.com/image gs://static.example.com/${{ inputs.TARGET_DIR }}


      # ※2
      - name: Update deployment status (success)
        if: success()
        uses: chrnorm/deployment-status@v2
        with:
          token: "${{ secrets.GITHUB_TOKEN }}"
          state: 'success'
          deployment-id: ${{ steps.deployment.outputs.deployment_id }}

      # ※2
      - name: Update deployment status (failure)
        if: failure()
        uses: chrnorm/deployment-status@v2
        with:
          token: "${{ secrets.GITHUB_TOKEN }}"
          state: 'failure'
          deployment-id: ${{ steps.deployment.outputs.deployment_id }}

前提として、ここではステージングと本番とで同じGCSバケットを使っています。(同一ドメインにしたいので)
最初のプリフィクスで分けています(説明の分かりやすさのため /prod/, /dev/ にしました)

  • ※1 では本番のディレクトリ /prod/ は mainブランチ のみで可能にしています。間違えて変なブランチを本番で使えないようにしています。
  • ※2 ではGitHub Deploymentsのための設定です。無くてもいいですが、あると分かりやすいので便利。
  • ※3 ではWorkload Identity でのGCP認証をしています。サービスアカウント名やプロバイダー名は固定にしていますが必要に応じて変えてください。
  • ※4 では リポジトリ内の /static.example.com/image/ というディレクトリ配下にあるファイルを全てGCSへアップロードしています。バケット名(ドメイン)は適宜変えてください。

次に各環境のワークフローですが、ここでは開発・ステージング用のサンプルになります。 (本番も内容は一緒です。)

name: Rsync data to GCS (dev)

on:
  workflow_dispatch:

jobs:
  rsync_gcs_development:
    uses: <my-org-name>/<repo-name>/.github/workflows/_common-rsync-static.yml@main
    with:
      TARGET_DIR: dev
      ENV_NAME: development
      ENV_URL: https://console.cloud.google.com/storage/browser/static.example.com
    secrets:
      github-token: ${{ secrets.GITHUB_TOKEN }}
      GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
      GCP_PROJECT_NUMBER: ${{ secrets.GCP_PROJECT_NUMBER }}
  • TARGET_DIR は /prod/ か /dev/ が入るプリフィクスの部分です
  • ENV_NAME は GitHub Deployment で使う環境名です
  • ENV_URL ã‚‚ GitHub Deployment で使うだけなのできちんと設定しなくてもいいです

  • secrets.GCP_PROJECT_ID と secrets.GCP_PROJECT_NUMBER はGCPのウェルカムページにあるプロジェクトIDと番号を入れてください

https://console.cloud.google.com/welcome

さて、今日はこれで寝ます。 素敵なWebP生活を。おやすみなさい。