ブログの画像はでかい

ブログ記事によっては画像を掲載することが多くあります。
画像はテキストに比べて容量が大きく、トラヒックを多く使用します。
ブログを表示する際にも容量が大きければ表示に時間がかかる事になり、(Web屋さん曰く)閲覧率が下がるらしいです。
ブログに掲載する画像は情報が伝われば何でもよいと考えているので、画像の容量を削減してみることにします。

通常ならば、画像をアップロード前にリサイズや軽量化、最適化などを行うべきなのでしょうが、自分はめんどくさがりな上、忘れてしまうことがあります。
何より、いちいち画像編集ソフトを開いていじくりまわすのに結構時間を吸われてしまいます。

このブログはGit管理されており、Gitには任意の時に処理を実行するというGit hooksという機能が存在するので、それを用いて実現してみることにします。

やりたいこと

Git hooksのpre-commitを使用して以下の動作を自動化します。

  • Exif削除
  • 画像解像度縮小
  • WebPへ変換

画像最適化

自分はWebエンジニアではないので、ベストプラクティスがわかりませんが概ね以下のように画像を変換してやるとよいそうです。

  • フォーマット: WebP
  • 画像容量: 250kB程度
  • 解像度: 横幅1000px程度
  • クォリティー: 70-90%

試しにGIMPを使って解像度が4080px x 3072pxの容量3.34MBのJPEG画像を
2040px x 1536pxの解像度にダウンスケールしてWebP形式に変換を行ったら、
容量が291kBになりました。

細部を見ると動画エンコード特有の画質の下がり方はしていますが、このブログの画像をまじまじと見る人はいないと思われますので、このままでよさそうです。 ほぼ同条件でJPEGを書き出した場合は容量が860kBとなったため、WebPの圧縮効率が高いことがわかります。

細かいディテールが多い画像はやはりブロックノイズの餌食になりやすく、いろいろつぶれてしまうようです。(この辺は今後の課題ということで・・・。)

画像変換スクリプトの作成

本来はしっかりとしたプログラムを作った方が良いのでしょうが、
どうせプログラムからlibwebpimagemagickを呼び出して変換する形になると思われますので、
今回はシェルスクリプトで組むことにしました。

ざっと組むと以下のスクリプトとなります。

#!/bin/bash

# Init
SOURCE=$1
DEST=$2
FILE_EXT=$(file -b --mime-type "${SOURCE}")
MAX_WIDTH=1200
TARGET_SIZE=250 # Kiro Byte

# Delete EXIF
OPTS=("-strip")

# Resize
OPTS+=("-resize" "${MAX_WIDTH}x>")

# EXT Check
case $FILE_EXT in
  'image/png')
    OPTS+=("-define" "webp:lossless=true")
    ;;
  'image/jpeg')
    OPTS+=("-quality" "90" "-define" "webp:lossless=false")
    ;;
  *)
  exit
  ;;
esac

# Target Size
size=$((1024 * TARGET_SIZE))
OPTS+=("-define" "webp:target-size=${size}" "-define" "webp:pass=5")

# WebP auto-filter
OPTS+=("-define" "webp:auto-filter=true")

# Multi thread
OPTS+=("-define" "webp:thread-level=1")

# Convert
convert "${OPTS[@]}" "${SOURCE}" "${DEST}"

雑に組むとこんな感じです。 -stripでEXIF情報を消し、-resize 1200x>で横幅1200px以上の場合1200pxになるようにリサイズ、
あとはPNGならばロスレス、JPEGならばクォリティーが90%になるようにオプションを分岐させ、
webp:target-sizeで指定した画像容量になるように調整を行います。

変換以外の諸々を行うスクリプト

Git hooksのpre-commitはコミット前に処理を実行してくれます。
そのためステージングされている画像データーを取得して処理を回してやるのがよさそうです。(処理されたくない画像?その時に例外処理を考えます。)

雑に書いたスクリプトは以下です。
半ば自明な部分もありますが、コメントを書いているので説明は省きます。

#!/bin/bash

# Init
IMG_EXT_REGEXP="\.(jpe?g|JPE?G|png|PNG)$"

# git statusからステージングされている画像ファイルを取得。 (Rはリネームで、その場合$4がリネーム先)
for src in $(git status --porcelain | awk -v ext_regexp="${IMG_EXT_REGEXP}" '$1~/[AMR]/{file=($1=="R"?$4:$2);if(file~ext_regexp){print file}}'); do
  dest=$(echo "${src}" | sed -E "s/${IMG_EXT_REGEXP}/.webp/") # WebPにリネーム

  # img_optimizerでリサイズ&WebPに変換
  echo "Optimizing ${src} to ${dest}..."
  ./scripts/img_optimizer/img_optimizer.sh "${src}" "${dest}"

  # 元画像のステージングを外して、変換後ファイルをステージング
  git restore --staged "${src}"
  git add "${dest}"

  # 元画像はバックアップとして退避
  bk_dir="./assets_bk/$(echo "${src}" | cut -d"/" -f2-5)"
  mkdir -p "${bk_dir}"
  mv "${src}" "${bk_dir}/"

  # 元画像へのパスを含む記事がある場合はWebPにリネームしてステージング
  grep -rlE '!'"\[.+\]\(/${src}\)" ./_posts | while read -r md_path; do
    echo "Updating image link in ${md_path} for ${src} to ${dest}..."
    sed -i -E 's|!\[(.+)\]\(/'"${src}"'\)|![\1](/'"${dest}"')|g' "${md_path}"
    git add "${md_path}"
  done
done

Git hooks

リポジトリルートの.git/hooks内にGit hooksのサンプルスクリプトがあります。
それぞれ.sampleを外すだけで動作します。

coder@Hakoniwa:diary$ ll ./.git/hooks/
total 64
-rwxr-xr-x 1 coder coder  478 Jul  7  2022 applypatch-msg.sample
-rwxr-xr-x 1 coder coder  896 Jul  7  2022 commit-msg.sample
-rwxr-xr-x 1 coder coder 4655 Jul  7  2022 fsmonitor-watchman.sample
-rwxr-xr-x 1 coder coder  189 Jul  7  2022 post-update.sample
-rwxr-xr-x 1 coder coder  424 Jul  7  2022 pre-applypatch.sample
-rwxr-xr-x 1 coder coder 1643 Jul  7  2022 pre-commit.sample
-rwxr-xr-x 1 coder coder  416 Jul  7  2022 pre-merge-commit.sample
-rwxr-xr-x 1 coder coder 1492 Jul  7  2022 prepare-commit-msg.sample
-rwxr-xr-x 1 coder coder 1374 Jul  7  2022 pre-push.sample
-rwxr-xr-x 1 coder coder 4898 Jul  7  2022 pre-rebase.sample
-rwxr-xr-x 1 coder coder  544 Jul  7  2022 pre-receive.sample
-rwxr-xr-x 1 coder coder 2783 Jul  7  2022 push-to-checkout.sample
-rwxr-xr-x 1 coder coder 3650 Jul  7  2022 update.sample
coder@Hakoniwa:diary$

中身はシェルスクリプトなので、シェルコマンドを渡してやるだけで任意のプログラムを実行可能です。

サンプルの中身は別にあってもなくても良いので、pre-commitというファイルを作り、以下を書き込めばよいです。

#!/bin/bash

# Img Optimize
./scripts/img_optimizer/webp-image-converter.sh

おわり

Git hooks、簡易的なCI/CD機能にもつかえるそうなので、その辺もいじってあそんでみたいです。

とりあえず、ブログの軽量化の足掛けとしてWebP対応ができたのでよかったかなぁ・・・。といった感じです。

という投稿をしたら、フォロワーから元画像も参照できるようにしろと言われたので、また今度対応します。