【Windows】バッチコマンドで繰り返し作業をサボろう！③【ffmpegでPNGからJPG一括変換編】

# INDEX
- 1. 前回のあらすじ
- 2. 目的
- 3. やろうず！
- 4. あとがき

# 1. 前回のあらすじ
②の続き。今回は②と独立した記事なので読まなくても大丈夫。
https://qiita.com/neonemo/items/d512e9fce2d47bcca0ab

PCの環境設定なんかは①の方を参考にしてね。こっちは必須。読み終わったら戻ってきて！
https://qiita.com/neonemo/items/3f178915bf3f9de23f9c

# 2. 目的
- カレントフォルダに入っているpngファイルを一括で良い感じの品質かつサイズのjpgに変換する
    - 4k画質のpngとか扱いにく過ぎるので、具体的には次のような処理をする
        - 品質70%程度のjpgに変換する
        - 画像サイズの長辺が1920pxを超える場合は、縦横比を維持したまま1920pxに縮小する（フルHD相当）

# 3. やろうず！
今回は外部ツールとしてffmpegを組み込んで目的を達成するので、こんな流れになる。
- ffmpegをセットアップする
- バッチコマンドを作る
- 動作確認をする

## 3.1. コマンドの仕様を決める
今回は、若干動きが複雑なのでこんな感じでバッチファイルを2つ作り内部的に2段階の処理をする。
青い箱が今回作成するバッチコマンドになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92447/af207ec2-44bd-85ee-64da-671d39b4e034.png)

で、実際にコマンドを使う時はこう！
想定状況にぶち当たった！画像がでかいぞ！

```winbatch
> dir
 ドライブ D のボリューム ラベルは Application です
 ボリューム シリアル番号は xxxx-xxxx です

 D:\tmp のディレクトリ

2021/01/05  04:30    <DIR>          .
2021/01/05  04:30    <DIR>          ..
2021/01/05  04:30           1,000,000 0001.png
2021/01/05  04:30           1,000,000 0002.png
2021/01/05  04:30           1,000,000 0003.png
               3 個のファイル           3,000,000 バイト
               2 個のディレクトリ  1,737,524,916,224 バイトの空き領域
```

よし、変換しよう！

```winbatch
> convjpg start
```

再びdirで確認。jpgに変換して小さくなった。やったね！

```winbatch
> dir
 ドライブ D のボリューム ラベルは Application です
 ボリューム シリアル番号は xxxx-xxxx です

 D:\tmp のディレクトリ

2021/01/05  04:30    <DIR>          .
2021/01/05  04:30    <DIR>          ..
2021/01/05  04:35           100,000 0001.png
2021/01/05  04:35           100,000 0002.png
2021/01/05  04:35           100,000 0003.png
               3 個のファイル           300,000 バイト
               2 個のディレクトリ  1,737,525,616,224 バイトの空き領域
```

## 3.2. ffmpegをセットアップする
### 3.2.1. セットアップ
- 公式（[https://ffmpeg.org/](https://ffmpeg.org/)）からffmpegを入手してくる。
    - 自分は （[CODEX FFMPEG](https://www.gyan.dev/ffmpeg/builds/)）のページからrelease版のこれを入手、任意のフォルダに展開してWindowsのパスを通した。
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92447/89c6ae6a-bbea-4b4d-3110-17cbb9a7a3d3.png)
        - 【[パスを通す](https://www.google.com/search?q=windows+%E3%83%91%E3%82%B9%E3%82%92%E9%80%9A%E3%81%99&oq=windows+%E3%83%91%E3%82%B9%E3%82%92%E9%80%9A%E3%81%99&aqs=chrome..69i57j69i61.13431j0j7&sourceid=chrome&ie=UTF-8)】の意味が分からない場合は、[最初の記事](https://qiita.com/neonemo/items/3f178915bf3f9de23f9c)を見直すかWindowsのパスの通し方を調べて来てくれ。汎用OS上で動かすシステム開発を生業とするなら必要な知識だ。
    - ちなみに自分は、配置場所を`D:\opt\ffmpeg\ffmpeg-4.3.1-2020-11-19-full_build`にして、シンボリックリンクで`D:\opt\ffmpeg\latest`から実フォルダを辿れるようにした。

```winbatch
> cd /d d:\opt\ffmpeg

> mklink /d latest ffmpeg-4.3.1-2020-11-19-full_build
latest <<===>> ffmpeg-4.3.1-2020-11-19-full_build のシンボリック リンクが作成されました

> dir
 ドライブ D のボリューム ラベルは Application です
 ボリューム シリアル番号は xxxx-xxxx です

 d:\opt\ffmpeg のディレクトリ

2020/12/03  01:17    <DIR>          .
2020/12/03  01:17    <DIR>          ..
2020/11/22  19:57    <DIR>          ffmpeg-4.3.1-2020-11-19-full_build
2020/12/03  01:17    <SYMLINKD>     latest [ffmpeg-4.3.1-2020-11-19-full_build]
               0 個のファイル                   0 バイト
               4 個のディレクトリ  551,372,156,928 バイトの空き領域

>
```

### 3.2.2. 確認
コマンドプロンプトで次のような応答が得られればOK

```winbatch
> where ffmpeg
D:\opt\ffmpeg\latest\bin\ffmpeg.exe

> where ffprobe
D:\opt\ffmpeg\latest\bin\ffprobe.exe

>
```

## 3.3. バッチコマンドを作る
### 3.3.1. ソース
- 今回はソースコードが長くなってしまったのでGitHubにアップした。
    - https://github.com/mahny-b/alstroemeria

### 3.3.2. ピックアップ解説
#### 画像の解像度取得にはffprobeコマンドを使う
```winbatch:ffprobeの書式
ffprobe -v [ログ出力レベル] -select_streams [標準出力したい] -show_entries [出力したいパラメータ] -of [出力形式] [出力ファイル]
```

```winbatch:get-resolution.bat(21行目辺り)
rem メイン処理
set CMD_FFPROBE=ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of default=nw=1 "%~1"
```
- 後でfor文で実行する為にffprobeコマンドを一旦変数に格納している
- `%~1`のようにチルダを付けると、括ったダブルコーテーションを除去した状態になる
    - `echo %1`だと`"hello!"`というような時にも…
    - `echo %~1`とすると`hello!`と出力するようになる
- `ffprobe`は`ffmpeg`に含まれるコマンドだ。これを使うと画像以外にも動画サイズやコーデック情報なんかも標準出力してくれる。
- `ffprobe`コマンドをオプション無しでファイルだけ指定するとものすごい量の出力がある。コマンド単独で実行して、どんなパラメータが取れるのかを確認すると理解も深まると思う。
    - 全部解説する気力は無いのでお願いします。マジやばいですアレ。

```winbatch:get-resolution.bat(27行目辺り)
for /f "usebackq" %%a in (`!CMD_FFPROBE!`) do (
    set REPLY=%%a
    echo "!REPLY!" | findstr "width" > nul 2>&1
    set IS_WIDTH=!ERRORLEVEL!

    echo "!REPLY!" | findstr "height" > nul 2>&1
    set IS_HEIGHT=!ERRORLEVEL!

    if "!IS_WIDTH!"=="0" (
        set _AL_WIDTH=!REPLY:~6!
    ) else if "!IS_HEIGHT!"=="0" (
        set _AL_HEIGHT=!REPLY:~7!
    )
)
```
- 27行目は標準出力した解像度を拾う処理になっている
- `REPLY`には`width=xxx`か`height=xxx`という**ffprobeの実行結果**が入ってくる
    - `findstr`はキーワードがヒットすると実行結果コードが`0`となる性質があるので、どっちの値が入っているのかを`echo`と`findstr`コマンドの合わせ技で判断している。
- 最後に`%環境変数名:~m,n%`という指定方法で他言語でいう部分文字列取得（`substr()`のような）処理が出来ので、数字部分のみを取得している
    - `width=`で6文字だから`!REPLY:~6!`だと【6文字目より後ろから最後まで】を取得する
    - `m`は正の数で指定し、先頭からの文字数。省略時は最初から
    - `n`は負の数で指定し、末尾からの文字数を指定する。省略時は末尾から
    - プログラム的には違和感があるけど、負数のみを指定するという**第1引数のmを省略する仕掛け**がある

```winbatch:get-resolution.bat(41行目辺り)
echo !_AL_WIDTH! !_AL_HEIGHT!
```
- これは単純に呼び出し元で拾うための標準出力である。1行で出している理由もありそれは後述する。


#### 標準出力のコマンド結果は`for /f "usebackq"`で拾う
```winbatch:convjpg.bat(13行目辺り)
for /f "usebackq" %%a in (`dir /b`) do (
    set HOGE=%%a
)
```
- 標準出力を1行ずつ回してしまうので、内容を保持したい場合は上書いてしまわない様に変数の持ち方に気を付けよう！
- ついでにHOGEを`%HOGE%`で呼び出してしまうと最初の代入以降値が変わらなくなる。そこで`遅延環境変数`という仕組みを使う必要が出てくる。ビックリマークで括っているアイツだ。
    - 参考: 遅延環境変数 - 「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
        - https://wa3.i-3-i.info/word12518.html

#### さらに標準出力のコマンド結果を分割したい時は`for /f "tokens=1,2,…"`を使う
```winbatch:convjpg.bat(23行目辺り)
for /f "usebackq tokens=1,2" %%b in (`get-resolution "!TARGET_FILE!"`) do (
    set _AL_WIDTH=%%b // ← tokens=1の内容
    set _AL_HEIGHT=%%c // ← tokens=2の内容
)
```
- 最初の例の`usebackq`だけだと、%%aには1行分丸ごと入っていた。tokensを使うとスペースで区切った時の何番目を割り当てるかという動きをする。割り当て用変数`%%a`はアルファベット1文字限定で、a, b, c, …といった具合に順番にtokenの番号を割り当てられる。
    - ちなみに`get-resolution`コマンドは、解像度を`3840 2160`の形式で1行で返すように実装しているので、`tokens`で分割するのに向いている。
    - あれだ。Linuxの`awk`に似てる。`for /f "usebackq tokens=1,3"`と書いたら`awk '{print $1 $3}'`みたいな感じ。

#### サポートファイル（pngとjpg）の振り分けを拡張子で行う
```winbatch:convjpg.bat(20行目辺り)
call :IS_SUPPERTED_EXT "!FILE_EXT!"
```
```winbatch:convjpg.bat(60行目辺り)
rem ------------------------------
rem サポートする拡張子かどうかを返す
rem [0] 拡張子
rem ret: 0: サポートしない / 1: ～する
rem ------------------------------
:IS_SUPPERTED_EXT
    setlocal enabledelayedexpansion
    set ret=0
    if "%~1"==".jpg" (
        set ret=1
    ) else if "%~1"==".png" (
        set ret=1
    )
    endlocal && set ret=%ret%
exit /b %ret%
```

- 見出し通り。`環境変数名:~x`みたいな感じにすると変数全体のダブルコーテーションを取り除いた上に拡張子が取れる。
    - forループの割り当て変数の場合、例えば`%%a`を使っている時、拡張子は`%%~xa`で取れる
- この性質を使って疑似サブルーチンを作ってファイル単位で判定している
- `endlocal && set ret=%ret%` は疑似サブルーチンから戻り値を返す仕掛け。`endlocal`をした時点で遅延環境変数が使えなくなってしまうが`&&`でつないだ時はその行だけ使える。
    - この性質を使うと`for`の遅延環境変数から`setlocat`の外に遅延環境変数が持ってこられるようになる

#### 画像変換用コマンドを組み立てて実行する
```winbatch:ffmpegの書式
ffmpeg -i [入力ファイル] -vf [スケールオプション] -loglevel [ログレベル] -q [jpgの品質] [出力ファイル]
```
```winbatch:convjpg.bat(39行目辺り)
rem 変換コマンドを組み立てて実行する
set CONV_CMD=ffmpeg -i "!TARGET_FILE!" !CONV_SCALE! -loglevel warning -q 5 "!CONV_FILE!"
echo !CONV_CMD!
!CONV_CMD!
```
- `ffmpeg`コマンドを組み立てて実行するだけ
    - コマンドを環境変数`CONV_CMD`に格納しているのは進捗代わりに`echo`したかったから

## 3.4. 動作確認をする
### 3.4.1. 素材
- テストデータ用に高解像度でクッソでかいPNGファイルをいくつか用意した。必要ならどうぞ。（[テスト用PNG](https://drive.google.com/file/d/1C-iJz4yKKGFqMETUTtDJLBhvnbLMwFie/view?usp=sharing)）
    - GoogleDriveなんでその内消すかもしれないのでそん時はゴメン！
- テストデータは好きな場所においてください。自分は`d:\tmp`に格納した。
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92447/487574ca-e0f1-9cc7-558e-b216a230cd2f.png)

### 3.4.2. コマンド
```winbatch
> cd /d d:\tmp
> dir
> convjpg
> dir

```

実際にやったところ

```winbatch
> dir
 ドライブ D のボリューム ラベルは Application です
 ボリューム シリアル番号は xxxx-xxxx です

 d:\tmp のディレクトリ

2021/01/05  07:59    <DIR>          .
2021/01/05  07:59    <DIR>          ..
2021/01/05  05:50         7,429,541 0001.png
2021/01/05  05:50         7,404,285 0002.png
2021/01/05  05:51         7,415,862 0003.png
2021/01/05  07:59        22,229,569 testdatas.zip
               4 個のファイル          44,479,257 バイト
               2 個のディレクトリ  551,269,953,536 バイトの空き領域

> convjpg start
ffmpeg -i "0001.png" -vf "scale=1920:-1" -loglevel warning -q 5 "convjpg_0001.jpg"
[png_pipe @ 0000024dc9b6dfc0] Stream #0: not enough frames to estimate rate; consider increasing probesize
[swscaler @ 0000024dc9ba64c0] deprecated pixel format used, make sure you did set range correctly
ffmpeg -i "0002.png" -vf "scale=1920:-1" -loglevel warning -q 5 "convjpg_0002.jpg"
[png_pipe @ 0000025d7676dfc0] Stream #0: not enough frames to estimate rate; consider increasing probesize
[swscaler @ 0000025d767a6400] deprecated pixel format used, make sure you did set range correctly
ffmpeg -i "0003.png" -vf "scale=1920:-1" -loglevel warning -q 5 "convjpg_0003.jpg"
[png_pipe @ 0000017b86eadfc0] Stream #0: not enough frames to estimate rate; consider increasing probesize
[swscaler @ 0000017b86ee6d40] deprecated pixel format used, make sure you did set range correctly
skipped. / file=[testdatas.zip]

> dir
 ドライブ D のボリューム ラベルは Application です
 ボリューム シリアル番号は xxxx-xxxx です

 d:\tmp のディレクトリ

2021/01/05  08:05    <DIR>          .
2021/01/05  08:05    <DIR>          ..
2021/01/05  08:05           257,887 0001.jpg
2021/01/05  08:05           259,294 0002.jpg
2021/01/05  08:05           259,871 0003.jpg
2021/01/05  07:59        22,229,569 testdatas.zip
               4 個のファイル          23,006,621 バイト
               2 個のディレクトリ  551,291,424,768 バイトの空き領域

```

### 3.4.3. 変換前後の画像の違い
- 正直画質的な違いがあるようには見えない…耄碌したかな…。なお容量圧縮率は約96.5%！やりすぎｗ
- 変換前（フルカラーPNG: 4K:3860x2160／容量:7,429,541byte）
    - ![0001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92447/aafc9abf-27e4-c404-c333-6b47e5146a45.png)
- 変換後（劣化が目立たない程度のJPG: フルHD:1920x1080／容量:257,887byte）
    - ![0001.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92447/00a5c665-db81-f09d-3827-7df59f692f25.jpeg)



# 4. あとがき
3回の連続記事はこれで終わりだ。ぶっちゃけ解説よりもソースを見てください。お願いします（えー

今回はやりたい事が簡単な割に細かい部分で結構苦労した。ラクをするという観点だと矛盾すると思われそうだが、自分はシステム屋さんなので、他人をラクさせても組織の利益になるので良いのである。
組織内の誰かがハズレくじを引くだけの話だ。まあこの場合は切り込み隊長をする自分だな。orz

まあ、それは冗談にしても自動化、半自動化は属人化する部分をツールに外だしする事で人の差を消せる部分もあるので、過去に自動化できないと思っていた作業を再びやる事があったらもう一度自動化出来ないかは考えてみた方が良いと思う。
**人は成長するんだから、過去の自分は閃いてなく今の自分なら閃く可能性がある**しね！

最後に、GitHubに上げたソースコードは、私的改造もするつもりマンマンなのでこの記事との差分が出るかもしれないがその時はごめんなさい。なるべくタグで切るようにします＞＜

以上！