特定のフォルダに画像を保存するだけで、予め設定した縦横ピクセル数や画質の画像をオンデマンドで自動的に出力する処理をbashで書いてみる

# スクリーンショットの画像を軽くしたり、リサイズしたいだけなのに、GIMPやPhotoshopなどでいちいち変換するのは面倒くさくないですか？

**スクリーンショットを撮る機能はOSやツールごとに異なりますが、クリップボードにコピーするにしろ、ファイルとして保存するにしろ、基本的には無圧縮または可逆圧縮なフォーマットが用いられます。**

しかし無圧縮or可逆圧縮フォーマットはデータ量が多いので、通常はJPEGや減色処理したPNGを用いるなどしてデータ量を削減すると思います。

このような場合に作業者の希望は規定のスペックの画像を得ることです。だから作業量は少ないほうがありがたいです。画像処理ソフトとしてはGIMPやPhotoshopが有名ですけど、今回の目的のために使うにはツールがヘビーすぎます。あるいは、あるいはこの手の変換を目的としたツールもあるにはありますが、どのツールを用いるにしても、基本的に「スクリーンショットをファイルに保存、または撮影した画像を所定のフォルダに保存」したのちに「ツールを起動して変換」せねばならず、ツールの機能がヘビーなのかシンプルなのかに関わらず、案外めんどくさいのです。

そこで今回の実装では、**画像を特定のフォルダに保存するだけで、あらかじめ定義しておいたスペックの画像が自動で生成されるようにしてみます**。この方法では作業者が行わねばならないことは画像を保存することだけです。macOSのようにスクリーンショットが自動でファイル保存される仕組みならば、単にスクリーンショットを撮るだけで欲しいスペックの画像に変換させることも可能です。

# 実装方法

基本的な機能として、**ファイルが所定のフォルダに生成されたら、ImageMagick の convert コマンドで変換する**ようにしてみます。

このときに、**特定のフォルダを自力で polling するのは無駄**なので、ファイル作成に関するイベントが発生するとありがたいですよね。

**ファイル操作に関するイベントは Linux の inotify や macOS の fswatch でキャッチ**できますので、これらを用いることで polling ではなくイベント駆動型で実装できます。

また、変換パターンについては1枚の画像に対してサムネイル生成や通常表示（ただしデータサイズは削減）など、複数の出力が欲しいと思います。そこで**様々な変換パターンを個別の設定ファイルで定義しつつ一括変換を行う**ようにしてみます。

そして、元画像がサブディレクトリ込みで生成されている場合は、変換先にも同じディレクトリ構造を再現しつつ変換してみることにします。

# 動作環境

少々欲張りな気がしますけど、以下の環境で動くようにしてみます。

- macOS
- Ubuntu
- CentOS
- Windows Subsystem for Linux (Bash on Windows)

Windows Subsystem for Linux については Ubuntu でのみ動作確認しています。

**macOS の場合は fswatch** を使います。これは brew 等で インストールします。

**Ubuntu, CentOS, Bash on Windows では inotify-tools に含まれる inotifywait** を使います。CentOS ではバージョンによっては標準リポジトリに含まれておらず EPEL あたりからのインストールが必要です。

さらに、いずれの環境でも ImageMagick をインストールしておきます。

なお、ここから先は基本的に macOS での流れを説明しています。macOS はスクリーンショットを撮る操作で自動的にファイル保存する動作が OS の標準機能なので、このパターンを使うのが一番使いやすいからです。

Ubuntu の場合も PrtScn or Alt + PrtScn でスクリーンショットを画像として保存できますから、macOS 同様の操作が行えます。

Windows の場合は注意点が３つあります。１つは、Bash on Windows を使うこと。そして２つめは、Bash on Windows だとパスの指定が Windows のエクスプローラから見えるパスとは少し違うことです。３つめはOS 標準機能だと Winキー + PrtScn でデスクトップ全体のスクリーンショットを "ピクチャ\スクリーンショット" に保存することしかできない点です。特定のウィンドウをキャプチャしてファイル保存する機能は提供されていません。ですのでスクリーンショットをキャプチャしてファイル保存してくれるツールを組み合わせることが必要です。

なお、今回のスクリプト実装では fswatch or inotifywait のどちらが利用可能かを自動判定しますので、OS の違いによらず利用できます。


# 事前準備

このスクリプトでは、下記3つを決める必要があります。シェル変数についてはスクリプトの最初の箇所に設定箇所がありますので、ここに直書きします。

| 設定項目 | スクリプト内のシェル変数 | 実装サンプルでの指定内容、または参照するファイル |
|-----|-----|-----|
| 変換元画像を格納するフォルダ | SOURCE_DIRECTORY | ~/screenshot/orig/ |
| 変換後の画像を出力するフォルダ | TARGET_DIRECTORY | ~/screenshot/ |
| 変換ジョブの設定ファイル | - | ${TARGET_DIRECTORY}/config* |

そして macOS で上記の場所にスクリーンショットを自動保存させたい場合は、OS側の設定を以下のように設定変更しておきます。

```bash
defaults write com.apple.screencapture location ~/screenshot/orig
killall SystemUIServer
```

さらに、変換ジョブの設定ファイルを作っておきます。以下は設定ファイルの例です。JPEG品質を単に90に変更する例や、品質を90にしつつ横幅は640ピクセルに変更する例です。また、-strip を指定しているので JPEG ⇒ JPEG な変換ではサムネイル画像やGPS情報が削除されます。このような設定ファイルを TARGET_DIRECTORY で指定したパスに作っておけば変換時に自動で読み込まれます。（この例では ~/screenshot/ )

```:config.q90 
format=jpg
imagemagick_opt="-strip -quality 90"
outputdirectory=output
```

```:config.w640_q90 
format=jpg
imagemagick_opt="-strip -resize 640 -quality 90"
```

設定ファイルの中で outputdirectory が指定されている場合は、変換後の画像は 
 ```$TARGET_DIRECTORY/$outputdirectory``` に出力されます。ディレクトリがなければ自動的に作成します。

outputdirectory が指定されていない場合は、```$TARGET_DIRECTORY/設定ファイルの拡張子``` のディレクトリに出力します。

# 実装したコード

```bash:batchconvert_inotify_imagemagick.sh
#!/bin/bash
# Linux の inotifywait または macOS 向けの fswatch を用いて
# イベント駆動式に画像変換を行うスクリプト
#
# 対象フォルダの設定
SOURCE_DIRECTORY=~/screenshot/orig/
TARGET_DIRECTORY=~/screenshot/

abspath() {
    echo "$(cd $(dirname $1) && pwd)/$(basename $1)"
}

fullpath_fswatch=$( which fswatch )
fullpath_inotifywait=$( which inotifywait )
fullpath_convert=$( which convert )
TARGET_DIRECTORY=$( abspath "${TARGET_DIRECTORY}" )
target_sourcedir=$( abspath "${SOURCE_DIRECTORY}/" )

# SOURCE_DIRECTORY が TARGET_DIRECTORY 配下に含まれている場合は処理を行わない
if [[ ${TARGET_DIRECTORY} =~ ${target_sourcedir} ]]; then\
    echo '[FATAL] $TARGET_DIRECTORY includes $SOURCE_DIRECTORY. abort.'
    exit 1
fi

if [ "${fullpath_fswatch}" ]; then
    inotifycmd="${fullpath_fswatch} --event Created --event Updated --event Renamed"
fi

if [ "${fullpath_inotifywait}" ]; then
    inotifycmd="${fullpath_inotifywait} --monitor --recursive --event create,moved_to --format %w%f"
fi

if [ "${inotifycmd}" == "" ]; then
    echo "[FATAL] fswatch or inotifywait cannot find. abort."
    exit 1
fi

if [ "${fullpath_convert}" == "" ]; then
    echo "[FATAL] imagemagick cannot find. abort."
    exit 1
fi

# inotify or fswatch によるファイルの検出処理
#
# なお、本スクリプトではファイル検知と
# 実際の変換を同時に行っているが、実際の実装においては
# イベント検知処理と変換処理は分離したほうがよい。
# 
# イベント検知処理は検知したイベントをデータベース上の
# ジョブキューに積むことだけに専念し、
# 変換処理はジョブキューに積まれたジョブを順番に処理することに
# 専念すれば安定性が上がる。
(
${inotifycmd} "${target_sourcedir}" | while read inputfile ; do

    dir=${inputfile%/*}
    filename=${inputfile##*/}
    ext=${filename##*.}

    # ファイルではなくディレクトリだった場合は処理をスキップ
    if [ -d "${inputfile}" ]; then
        continue
    fi

    # 拡張子が png or jpg の場合だけ先に進む
    if [ "${ext}" != "png" ] && [ "${ext}" != "jpg" ]; then
        continue
    fi

    job_parentdirectory=$( echo "${dir}" | sed s%${target_sourcedir}%%)
#echo "dir=$dir"
#echo "target_sourcedir=$target_sourcedir"
#echo "job_parentdirectory=$job_parentdirectory"

    # イベントが発生したファイルを掴んでいるプロセスが無くなるまで待つ。
    # これをしないとファイルの書き込みが終わる前に処理を進めてしまうことがある。
    # 特に、WAN回線を経由したファイルコピーでは必須。
    while :
    do
        if [ -e "${inputfile}" ]; then
            lsof "${inputfile}" 2>&1 > /dev/null
            if  [ $? -eq 1 ]; then
                break
            fi
            sleep 1
        else
            break;
        fi
    done

    # ファイルが無ければ、処理を継続しない
    if [ ! -e "${inputfile}" ]; then
echo "[WARNING] $inputfile cannot find. skip."
        continue
    fi

    # 変換設定ファイルを読み込んで変換する
    # 変換条件を定義したファイル (config.*) を読む
    # config.* には以下の指定を記述する。
    # format
    # imagemagick_opt
    # outputdirectory
    ls "${TARGET_DIRECTORY}"/config* | while read job_config ; do
        if [ -e ${job_config} ]; then
            outputdirectory=""
            imagemagick_opt=""

            source ${job_config}
        else
            continue
        fi

echo "$job_config"
        if [ "$outputdirectory" == "" ]; then
            outputdirectory=${job_config##*.}
        fi
echo $outputdirectory

        if [ "$outputdirectory" == "" ]; then
            continue
        fi

        target_outputdir=$( abspath "${TARGET_DIRECTORY}/${outputdirectory}/" )
        # 出力先フォルダがなければ作る
        outputdir=$( abspath "${target_outputdir}/${job_parentdirectory}" )
        if [ ! -d "${outputdir}" ]; then
            mkdir -p ${outputdir}
        fi

        # imagemagick による変換の実行箇所
        outputfile=${outputdir}/${filename%.*}.${format}

        echo "convert ${imagemagick_opt} ${inputfile} ${outputfile}"
        convert ${imagemagick_opt} "${inputfile}" "${outputfile}"
    done
done
)
```

# 使い方

必要な環境設定が終わっていたら、あとはこのスクリプトを裏で動かしておくだけです。あとは指定のフォルダに画像が書き出されるごとに、指定のスペックでの変換が行われます。


# ファイルサイズをもっと減らしたい場合は？

mozjpegやGuetzliを使えば画質を維持しつつも、ファイルサイズはもっと減る、かもしれません。

ImageMagick/mozjpeg/Guetzliに関する自分なりの考察は以下にまとめています。
http://qiita.com/kazinoue/items/38b34f9d798400c0d129

# その他の応用例

「ファイルを生成する処理」と「生成されたファイルを加工する処理」がそれぞれ独立して、なおかつ加工する処理が非対話的に実行可能な場合は、同様の方法で処理できます。

たとえば「PDFファイルを保存すると、編集・印刷制限を自動的にかける」とか「所定の書式のPDFに対して自動的にデジタルイメージのハンコを押しつつ、実施内容を自動でログに記録する」とか、けっこういろいんな応用ができると思います。

あるいは、あるフォルダ以下を監視しておき、新規作成されたファイルや編集されたファイルは自動でバックアップする、なんてこともできるかもしれません。