画像や動画ファイルの縦横サイズをリストするシェルスクリプト

ディレクトリの下に、縦横サイズが様々な画像や動画ファイルがあって、その中から特定の縦横サイズの画像や動画を探したい時に、これまでFolder Appからファイルを１個１個クリックしてプレビュー表示でサイズを確認していたのですが、数が多いととてもやってられない。画像の縦横ピクセル数、アスペクト比、向き（縦長、横長）などをコマンドラインでまとめて一覧表示できるといいなあと思ったので、シェルスクリプトを書きました。その作り方のメモです。出力サンプルはこんな感じです（下図）。

![fig_1_w800.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/384620f7-2c4a-c933-67bc-88a9da067dd6.png)

**実行環境**

- macOS Mojave 10.14
- bash 3.2.57
- ffmpeg 4.2.1


今回作ったシェルスクリプト`size.sh`は、指定したディレクトリ（省略時はカレント）の中の階層を`find`コマンドで辿って画像や動画ファイルを検索し、見つかった各ファイルについて`ffmpeg`コマンドの出力から画像の幅・高さなどの値を取り出し、結果を整形して標準出力に書き出します。

## メインのループ処理
最初に、メインのループ処理の構造を示します。

```bash:size.sh
while read f; do
  #
  # ファイル f に対する処理をする
  #
  echo $f $dim $w $h $orien $r_w:$r_h
done < <(find -E "$imgdir" -type f -iregex '.*\.(jpe?g|png|mp4)') 
```

`find`が出力した画像ファイルのパス名を１行ずつ`read`で変数`f`に読み込み、`f`に対する処理（後述）をした後、リスト１行分のデータを組み立て、`echo`で出力します。検索対象のファイルは、jpg, png, mp4としました。パス名に空白を含んでいても大丈夫です。

以下、ループ本体の処理をパーツに分けて説明します（リスト1〜5）。

## 画像サイズの取得
`ffmpeg`コマンドは入力ファイルの情報を標準エラー出力に書き出すので、画像サイズなどの項目を含んでいる行を`grep`コマンドで取り出します。下図はmp4動画ファイルに対して実行した例です。着目するのは図で黄色く囲んだ箇所です。
![fig_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/deae2198-6959-e0d6-a06e-5eb251ce03c9.png)
動画の縦横ピクセル数は、`Stream`項目（`Video`を含む行）に表示されています。この例では1920x1080になっていて一見サイズが横長に見えますが、実は再生すると縦長サイズの動画です（スマホを縦にして撮った動画でした）。再生した時に回転して表示されるかどうかは、`Side data`セクションの`displaymatrix`項目にrotationの度数に関する情報があります。そこで、本スクリプトでは`displaymatrix`の行もチェックすることにします。

下に示したリスト1は、`ffmpeg`の出力から画像の幅`$w`、高さ`$h`、および縦横の方向`$orien`を取り出すまでのコードです。`ffmpeg`の出力はこの後も使うので、対象行を`grep`した結果を変数`v`に保存しておきます。縦横サイズの「数字x数字」のパターンは、`grep -o`に正規表現を指定して取り出し、変数`$dim`に保持します。なお、0xで始まる16進数表記とのマッチを避けるため、ここでは「x」の前にくる数字は２桁以上に指定しました。（幅1pxの画像ファイルはない前提）

```bash:1
  v=$(ffmpeg -i "$f" 2>&1 |grep -E 'Stream|displaymatrix:')
  dim=$(echo $v |grep -E -o '[0-9]{2,}x[0-9]+')
  w=${dim%x*}
  h=${dim#*x}
  orien=square
  if [ $w -lt $h ]; then
    orien=vertical
  elif [ $w -gt $h ]; then
    orien=horizontal
  fi
```

次に、動画が縦横回転しているケースに対応するコードを追加します（リスト2）。リスト1で判定した動画の向き`$orien`が横長`horizontal`の場合、`displaymatrix`項目をチェックし、`rotation` に90度が指定されているときは、`$orien`を縦長`vertical`に変えて、`$w`と`$h`も入れ替えます。

```bash:2
  if [ "${f##*.}" = "mp4" ] && [ $orien = "horizontal" ]; then
    rot=$(echo $v | grep -E -o 'rotation of -?90\.00 degrees')
    if [ -n "$rot" ]; then
      orien=vertical
      t=$w
      w=$h
      h=$t
    fi
  fi
```

## アスペクト比の計算
画像のアスペクト比を計算するには、幅と高さをその最大公約数で除算して求めます。リスト3は、最大公約数を求めるための関数です。こちらのサイトを参考にさせていただきました：[TECH BOX アスペクト比の求め方](https://tech.arc-one.jp/asepct-ratio/)

関数`gcd()`に幅と高さのピクセル数を引数で渡すと、最大公約数が標準出力に表示されます。

```bash:3
function gcd() {
  if [ $2 -eq 0 ]; then
	echo $1
	return 0;
  fi
  echo $(gcd $2 $(( $1 % $2 )))
}
```

ターミナルに打ち込んで試してみましょう。

```bash:test
$ gcd 1920 1080
120
$ 
```

OK！ ではスクリプトに組み込みます（リスト4）。最大公約数を変数`$b`に、幅と高さに対する除算の結果をそれぞれ変数`$r_w`、`$r_h`に入れました。

```bash:4
  b=$(gcd $w $h)
  r_w=$(($w/$b))
  r_h=$(($h/$b))
```
さらにもうひと手間かけます。考慮するケースとしては、

1. 幅と高さが互いに素、つまり`$b`=1の場合
2. `$b`で除算しても`$r_w`と`$r_h`がまだ大きな整数である場合。たとえば、画像サイズ2246x1340を上記の方法で計算すると比率が1123:670になるが、数字が大きすぎてピンとこない

これらのケースについては、短辺を1としたときの長辺の大きさを小数で表すことにします。ケース2について、整数比での表記はとりあえず1:1〜16:9までカバーできれば十分と思われるので、小数で表記するのは`$r_w`または`$r_h`が16より大きい場合を対象にします。

bashで小数の演算をするには`bc`コマンドが使えます。`bc`への入力で、`scale`に小数点以下の桁数を指定します（リスト5）。

```bash:5
  if [ $b -eq 1 ] || [ $r_w -gt 16 ] || [ $r_h -gt 16 ]; then
    if [ $w -lt $h ]; then
      r_w=1;
      r_h=$(echo "scale=3; $h/$w" | bc)
    else
      r_w=$(echo "scale=3; $w/$h" | bc)
      r_h=1;
    fi
  fi
```

## スクリプト全体のソースコード
リスト1〜5を統合し、さらにコマンドライン引数の処理も加えたスクリプト全体のソースコードはこれです。

<details><summary>size.shのソースコード</summary><div>

```bash:size.sh
#!/bin/bash

imgdir=.

if [ $# -gt 1 ]; then
  echo "usage: $0 [ dir ]"
  exit 1;
fi
if [ $# -eq 1 ]; then
  imgdir=$1
fi

function gcd() {
  if [ $2 -eq 0 ]; then
	echo $1
	return 0;
  fi
  echo $(gcd $2 $(( $1 % $2 )))
}

while read f; do
  v=$(ffmpeg -i "$f" 2>&1 |grep -E 'Stream|displaymatrix:')
  dim=$(echo $v |grep -E -o '[0-9]{2,}x[0-9]+')
  w=${dim%x*}
  h=${dim#*x}
  orien=square
  if [ $w -lt $h ]; then
    orien=vertical
  elif [ $w -gt $h ]; then
    orien=horizontal
  fi

  if [ "${f##*.}" = "mp4" ] && [ $orien = "horizontal" ]; then
    rot=$(echo $v | grep -E -o 'rotation of -?90\.00 degrees')
    if [ -n "$rot" ]; then
      orien=vertical
      t=$w
      w=$h
      h=$t
    fi
  fi

  b=$(gcd $w $h)
  r_w=$(($w/$b))
  r_h=$(($h/$b))

  if [ $b -eq 1 ] || [ $r_w -gt 16 ] || [ $r_h -gt 16 ]; then
    if [ $w -lt $h ]; then
      r_w=1;
      r_h=$(echo "scale=3; $h/$w" | bc)
    else
      r_w=$(echo "scale=3; $w/$h" | bc)
      r_h=1;
    fi
  fi

  echo $f $dim $w $h $orien $r_w:$r_h
done < <(find -E "$imgdir" -type f -iregex '.*\.(jpe?g|png|mp4)') 
```

</div></details>

## 参考
以下の記事を参考にさせていただきました。

- [TECH BOX アスペクト比の求め方](https://tech.arc-one.jp/asepct-ratio/)
- [ディレクトリ内のすべてのファイルをループする](https://qiita.com/yuba/items/489fadb350246b5c94e7)
- [macOS の find コマンドで正規表現を使い、複数の拡張子を検索する](https://qiita.com/shimizumasaru/items/0c6eb5942b88ecffdbff)
- [bashの変数展開によるファイル名や拡張子の取得](https://qiita.com/mriho/items/b30b3a33e8d2e25e94a8)
- [シェルスクリプトで小数演算する](https://qiita.com/0118takuUW/items/22cdf368256f2732782c)