Unity開発でデザイナさんから上がってくる大きすぎる画像をシェルスクリプトで検出してみた

本記事は[Cocone Advent Calendar 2022](https://qiita.com/advent-calendar/2022/cocone) の17日目の記事となります。
# はじめに
僕の所属するチームではデザイナさんが作成した素材データなどをまとめたブランチを開発ブランチにマージする際に画像のサイズを確認する作業があります。
ただ、これをGitLab上で確認すると....

![スクリーンショット 2022-12-17 2.21.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988345/ad38ed42-ae14-e1fb-aa16-b8faf57d6506.png)

変更・追加されたファイル数が多いため、差分が全て表示されません😱
また、Unityで開発しているためメタファイルも大量に生成され、見たいデータを探すのも一苦労です。

そして、なんとこれを今まで目視だけで確認していました...(~~ざっくりと確認すれば良いと先輩には言われましたし、実際そこまで重要な作業ではないのだと思います~~)

今回は、この作業をシェルスクリプトで自動化しようと試みた話を書こうと思います。
ただし、僕自身これが初めてのシェルスクリプトだったので、シェルスクリプトﾁｮｯﾄﾃﾞｷﾙな人からマサカリが飛んできそうな内容になっているかと思います...(~~新卒エンジニアが頑張って書いたので許してください~~)

# やることの整理
ざっくりと今回やることを整理すると
1. 入力情報として、大きすぎると判断する画像の縦横サイズを指定する
2. 「開発ブランチ」と「デザイナさんのブランチ」の差分を取得する
3. 差分から画像ファイルだけを抽出する
4. 抽出した画像ファイルのサイズ情報を取得
5. サイズを比較して超えているもののファイル名とサイズを出力する

となります。

今回はさらに、以下の要件を加えたいと思います。
1. オプションでシェルスクリプト実行後、画像を保存するかを選択できる（デフォルトでは保存する）
2. 画像はpngファイルのみを抽出する

1の要件は画像サイズとしては小さいが無駄が多い画像になっていないかを目視で確認するためです。
2の要件は単純にうちのプロジェクトで使っている画像データがpngだけだったからです。

このシェルスクリプトを実行する時は以下のように叩けるようにします。
第一、第二引数に大きいと判断するしきい値となる幅と高さを設定するようにしています。
また、rmのオプションをつけることで実行後に一度保存した画像を全て削除するようにします。
```　
デフォルト
./diff_img.sh 1024 1024

実行後、画像を削除する
./diff_img.sh 1024 1024 rm
```


# シェルスクリプトの紹介
解説する前に、結論となるシェルスクリプトを紹介します。

なお、これ以降、
開発ブランチ→develop
デザイナさんのブランチ→design
と呼称することとします。

```diff_img.sh
#!/bin/sh

set -xe
img_save_path=画像を保存するパス
project_path=Unityプロジェクトのパス

is_file_size_valid(){
    # 大きいと判断するしきい値
    limit=$1
    # 画像ファイルが一時保存されているフォルダまでのパス
    img_save_path_internal=$2
    # fileコマンドによって得られた画像の情報
    img_info=$3
    # fileコマンドの出力からwidth x heightの部分だけ抜き出す
    img_size_info=`echo "$img_info" | sed -e "s|$img_save_path_internal.*png:PNGimagedata,\([0-9]\{1,4\}x[0-9]\{1,4\}\),.*|\1|"`
    
    width=`echo $img_size_info | cut -d 'x' -f1`
    height=`echo $img_size_info | cut -d 'x' -f2`
    img_size=$((width*height))
    
    if [ $img_size -ge $limit ]
    then
        # ファイル名とwidth x heightだけ抜き出して表示
        echo $img_info | sed -e "s|$img_save_path_internal\(.*png\):PNGimagedata,\([0-9]\{1,4\}x[0-9]\{1,4\}\),.*|\1 \2|"
    fi
}

mainProc(){
    current_path=`pwd`
    cd $project_path
    # developとdesignの差分pngファイルを一時保存フォルダに保存する
    git diff develop design --name-only | grep ".*png" | grep -v ".*meta" | XArgs -L 1 -I{} cp $project_path/{} $img_save_path
    cd $img_save_path
    # 関数化した処理をxArgsで呼び出すためにexport -fする
    export -f is_file_size_valid
    echo "over $1 x $2 files"
    # しきい値を超えるファイルの名前とサイズを表示する
    file $img_save_path/* | sed -e "s| ||g" | XArgs -L 1 -n 1 -I {} bash -c "is_file_size_valid $(($1*$2)) $img_save_path {}"
    if [ "$3" = rm ]
    then
        echo remove img files
        rm $img_save_path/*
    fi
    cd $current_path
}

main(){
    if [ $# -lt 2 ]
    then
        echo Error : please input width and height limits
    elif [[ "$1" =~ ^[0-9]+$ ]] && [[ "$2" =~ ^[0-9]+$ ]]
    then
        mainProc $1 $2 $3
    else
        echo Error : please input of width and height limits
    fi
    
}

main $1 $2 $3

```

# シェルスクリプト解説
## 1. 入力情報として、画像の縦横サイズを指定する
これは単純に入力のエラーチェックをしています。
弾いているエラーケースは
1. 第一、第二引数が数字でない
2. そもそも引数が2つ以上セットされていない

の2つです。

処理はmain()で行っています。

## 2. developとdesignの差分を取得する
これ以降がメインのロジックとなります。
処理はmainProc()で行います。

``` mainProc.sh
mainProc(){
    current_path=`pwd`
    cd $project_path
    # developとdesignの差分pngファイルを一時保存フォルダに保存する
    git diff develop design --name-only | grep ".*png" | grep -v ".*meta" | XArgs -L 1 -I{} cp $project_path/{} $img_save_path
    cd $img_save_path
    # 関数化した処理をxArgsで呼び出すためにexport -fする
    export -f is_file_size_valid
    echo "over $1 x $2 files"
    # しきい値を超えるファイルの名前とサイズを表示する
    file $img_save_path/* | sed -e "s| ||g" | XArgs -L 1 -n 1 -I {} bash -c "is_file_size_valid $(($1*$2)) $img_save_path {}"
    if [ "$3" = rm ]
    then
        echo remove img files
        rm $img_save_path/*
    fi
    cd $current_path
}
```


```git diff develop design --name-only```でブランチ間の差分があるファイル名を取ります。
## 3. 差分から画像ファイルだけを抽出する
２の出力をパイプライン処理で繋いで、```grep ".*png" | grep -v ".*meta"```によってpngファイルだけ取得します(metaファイルは無視)
また、このままだと続きの処理がしにくいので、一度別のフォルダにコピーします。

ここで、どうやって保存すれば良いのか悩みましたが、入力されたテキストを行単位の入力に分割して処理できるXArgsというコマンドの存在を知ったので、今回はこれで対応しました。
```XArgs -L 1 -I{} cp $project_path/{} $img_save_path```

:::note warn
この記事を書くにあたって、再度調べ直したらgitのコマンドで
```git archive design `git diff --name-only origin/develop design` -o archive.zip```
というコマンドを叩くだけで差分を圧縮ファイルとして出力できるそうです。
未検証なので、grepを途中で噛ませられるかは不明ですが、噛ませられるのであればこちらの方が良いと思います。
:::

## 4.抽出した画像ファイルのサイズ情報を取得
ここがかなり難航しました。

まず、CLI初心者(このシェルスクリプトを書くまではgrepやsedすら知らなかったレベル)だったので、どうやって画像ファイルサイズを出すのかというところを調べました。

すると、fileというコマンドがヒットしました。
例えば、今回のように```file $img_save_path/*と```叩くとimg_save_path以下の全てのファイルの属性、サイズなどの情報が出力されます。

```
出力例
$file $img_save_path/*                           
hoge1.png:  PNG image data, 128 x 128, 8-bit/color RGBA, non-interlaced
hoge2.png:     PNG image data, 1024 x 512, 8-bit/color RGBA, non-interlaced
hoge3png:      PNG image data, 256 x 1024, 8-bit/color RGBA, non-interlaced
```

ここで、128 x 128のように画像サイズが出力されています！！
なので、これを抽出できれば勝ちだと考えました

### 抽出方法
他にも方法があったかもしれませんが、シェルスクリプトど素人の僕はsedで置換すればいいかという安直な実装をしました。

まず、前のfileコマンドの出力をパイプラインで繋いで、```sed -e "s| ||g"```を使うことで、無駄なスペースを消します。(スペースがあると正規表現でうまく表せず、置換しにくかったため)
```
ここまでの出力例
$file $img_save_path/* | sed -e "s| ||g"
hoge1.png:PNGimagedata,128x128,8-bit/colorRGBA,non-interlaced
hoge2.png:PNGimagedata,1024x512,8-bit/colorRGBA,non-interlaced
hoge3png:PNGimagedata,256x1024,8-bit/colorRGBA,non-interlaced
```

次に、これをさらにパイプラインでXArgsコマンドに渡して、それぞれの行に対してサイズ比較の処理を実行します。
このサイズ比較処理は単純なコマンド一つでは実現するできないと考え、シェル関数にしました。

しかし、そうすると厄介な問題が潜んでいました...次の項で具体的に説明していきます。

## 5. サイズを比較して超えているもののファイル名とサイズを出力する
ここで、実行したい処理が以下のようになっています。
1. 大きいと判断するサイズ(入力値の積)と```$file $img_save_path/* | sed -e "s| ||g"```の出力結果（画像情報）の各行を入力にする
2. 画像情報から**128x128**のようになっている部分だけ抜き取る
3. さらにxの左と右を数値として抜き出す（128x128なら、128、128と抜き出す）
4. 3で抜き出した2つの値の積を入力値の積と比較して、それより大きい場合はファイル名とサイズを出力する

この処理は単純なコマンドでは難しいため、関数化することを考えました。

関数化した処理がこちら
```is_file_size_valid.sh
is_file_size_valid(){
    # 大きいと判断するしきい値
    limit=$1
    # 画像ファイルが一時保存されているフォルダまでのパス
    img_save_path_internal=$2
    # fileコマンドによって得られた画像の情報
    img_info=$3
    # fileコマンドの出力からwidth x heightの部分だけ抜き出す
    img_size_info=`echo "$img_info" | sed -e "s|$img_save_path_internal.*png:PNGimagedata,\([0-9]\{1,4\}x[0-9]\{1,4\}\),.*|\1|"`
    
    width=`echo $img_size_info | cut -d 'x' -f1`
    height=`echo $img_size_info | cut -d 'x' -f2`
    img_size=$((width*height))
    
    if [ $img_size -ge $limit ]
    then
        # ファイル名とwidth x heightだけ抜き出して表示
        echo $img_info | sed -e "s|$img_save_path_internal\(.*png\):PNGimagedata,\([0-9]\{1,4\}x[0-9]\{1,4\}\),.*|\1 \2|"
    fi
}
```

あとは、これを先ほどのxArgsの呼び出しコマンドとすれば良いだけ！！
しかし、これがまた難航...

いろいろ調べた結果、
```export -f is_file_size_valid```というコマンドで関数をエクスポート　した上で、XArgsで並列実行するコマンドをbash -c "関数名"とすれば良いとわかりました。

まとめると...
```
file $img_save_path/* | sed -e "s| ||g" | XArgs -L 1 -n 1 -I {} bash -c "is_file_size_valid $(($1*$2)) $img_save_path {}"
```
これで、このシェルスクリプト実行時に入力したしきい値より大きい画像ファイルの名前とそのサイズを出力できるようになりました！

そのあとは、オプションのrmがあれば、img_save_pathに保存した画像ファイルを全て削除して全ての処理が終了です。
# よかった点と反省点
## よかった点
なんと言っても差分を見る作業がやりやすくなったことです！
予期せぬ副産物として、ファイル名に日本語のものが含まれると処理的にエラーになるので日本語ファイル検出としても役立っています。(ただし、その場合そこで処理が止まるので、改善したい)

また、技術的にはxArgsでの並列実行やgrepやsedなどのコマンドで正規表現による検索・文字列置換について雰囲気だけでも理解できたことは今後、エンジニアとしてやっていく上でよかったかなと思います。(正規表現よくわからんマンでしたが、自分で書いてみたら少し頭に入ってきた)

## 反省点
不慣れな点が多かったため、やりたいことに対してめちゃめちゃ複雑なコマンドを書いてしまった点です。もう少しいろいろなコマンドについて調べればよりシンプルな記述で処理できたかと思います。
また、チーム内でこのシェルスクリプトを共有したところ色々と追加の要望が来ているので、今後、暇な時間を見てブラッシュアップしていきたいと思います。

# 参考記事(他にもいろいろ調べたが、思い出せた範囲で記載)
知っておくとちょっと便利！xargs コマンドの便利な使い方
https://tech-lab.sios.jp/archives/29544

シェルスクリプト(bash)にてsedコマンドを使用して文字列からダブルクォーテーションを削除する方法
https://www.yukiiworks.com/archives/75

xargsにbashのfunctionを渡す方法
https://wrist.hatenablog.com/entry/20120603/1338691396