画質低下によりファイルサイズを減らすスクリプト

# 概要
画像ファイルのファイルサイズを下げるためのスクリプトを書きました。

# 本文

## 要件
1. たくさんある画像に、ファイルサイズ上限を設ける。
1. 上限を超えているものは、ピクセルサイズはそのままに、画質を落とす。
1. 小さくなりすぎないようにする。

## 一括ソフトでやろうとした
1. ファイル一括加工ソフトは世の中にたくさんある
1. が、「ピクセルサイズそのままに画質低下で指定のファイルサイズに」の機能は、いくつか試した限りでは大雑把
    1. 上限300KBだっつってんのに勢い余って100KBにしちゃったり
    1. 画質最低にしてもそのサイズに届かないようなときに挙動が狂ったり

## というわけでコードを書いた

対象の99％がjpgだったので、jpg専用です。。。

### 環境
imagemagickが入っているWindowsの git bash （ニッチだ。。。）

### 設定ファイルその1

```bash:reduce_image_by_quality_targets.txt
000000.jpg
000001.jpg
000002.jpg
000004.jpg
```

対象ファイル一覧ファイルです。

### 設定ファイルその2

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

INPUT_DIR=/c/dev/ws/tmp/in
OUTPUT_DIR=/c/dev/ws/tmp/out
FILELIST_FILE_PATH=/c/dev/ws/tmp/reduce_image_by_quality_targets.txt
MAX_SIZE_KB=300
MAX_SIZE_BYTE=$(( MAX_SIZE_KB * 1024 ))
```

入出力ディレクトリ、最大ファイルサイズなどの設定です。

### 本体
```bash:reduce_by_quality.sh(一部略)
#!/bin/bash

function main() {
	# 対象ファイルリストファイルを読み込む
	logInfo "MAX_SIZE_BYTE=${MAX_SIZE_BYTE}"
	cat $FILELIST_FILE_PATH | while read line
	do
		logDebug '-------------------------------'
		logTrace "filename=${line}"
		handleOneFile $line
	done
}

function handleOneFile() {
	local readonly fileName=$1
	logDebug "filename=${fileName}"
	
	local readonly inputPath="${INPUT_DIR}/${fileName}"
	local readonly outputPath="${OUTPUT_DIR}/${fileName}"

	# ファイル存在確認
	if [ ! -e $inputPath ]; then
		logError "input file not exists: ${fileName}"
		return
	fi
	# インプットが最初から許容サイズ以下の場合はコピーして終わり
	local inputSize=$(getSize $inputPath)
	logTrace "inputSize=[${inputSize}]"
	if [ "$inputSize" -le "$MAX_SIZE_BYTE" ]; then
		logInfo "not need to compress: ${fileName}"
		cp -fp $inputPath $outputPath
		return
	fi
	
	local qMax=100 # jpgの最高画質
	local qMin=1   # jpgの最低画質
	local q=50     # テストする画質の初期値
	
	while :
	do
		magick $inputPath -quality $q $outputPath
		
		local outputSize=$(getSize $outputPath)
		logTrace "q=${q}, qMin=${qMin}, qMax=${qMax}, size=${outputSize}"

		# qMinとqMaxの差が1になったときの処理。
		if [ $(( qMax - qMin )) -eq 1 ]; then
			# 基準サイズ以下にできなかった場合は警告ログを出す
			outputSize=$(getSize $outputPath)
			if [ "$outputSize" -gt "$MAX_SIZE_BYTE" ]; then
				logWarn "最低品質でもサイズオーバーです。q=${q}, size: ${outputSize} file: ${fileName}"
			else
				logInfo "compress ok. q=${q}, size=${outputSize}, file=${fileName}"
			fi
			return
		fi

		# qをqMinかqMaxのどちらかに設定し、その中間をqとする
		if [ "$outputSize" -le "$MAX_SIZE_BYTE" ]; then
			# 指定サイズより小さくなった場合
			logTrace "q=${q} is size OK: $outputSize"
			qMin=$q
		else
			# 指定サイズよりも大きくなった場合
			logTrace "q=${q} is size NG: $outputSize"
			qMax=$q
		fi
		q=$(( (qMin + qMax) / 2 ))
	done
}

# ファイルサイズを取得
function getSize() {
	filePath=$1
	echo $(ls -la ${filePath} | cut -d ' ' -f 5)
}

```

縮小にはimagemagickを使っています。
最適な画質値は、下半分あたりのwhileループの中で探してます。中間値刻みでやりました。

### 結果
```
$ ./reduce_image_by_quality.sh
20210701-202103 (reduce_image_by_quality.sh:116) [DEBUG] ========================
20210701-202103 (reduce_image_by_quality.sh:117) [INFO] start.
20210701-202103 (reduce_image_by_quality.sh:6) [INFO] MAX_SIZE_BYTE=307200
20210701-202103 (reduce_image_by_quality.sh:9) [DEBUG] -------------------------------
20210701-202103 (reduce_image_by_quality.sh:10) [TRACE] filename=000000.jpg
20210701-202103 (reduce_image_by_quality.sh:18) [DEBUG] filename=000000.jpg
20210701-202103 (reduce_image_by_quality.sh:25) [ERROR] input file not exists: 000000.jpg
20210701-202103 (reduce_image_by_quality.sh:9) [DEBUG] -------------------------------
20210701-202104 (reduce_image_by_quality.sh:10) [TRACE] filename=000001.jpg
20210701-202104 (reduce_image_by_quality.sh:18) [DEBUG] filename=000001.jpg
20210701-202104 (reduce_image_by_quality.sh:31) [TRACE] inputSize=[104619]
20210701-202104 (reduce_image_by_quality.sh:33) [INFO] not need to compress: 000001.jpg
20210701-202104 (reduce_image_by_quality.sh:9) [DEBUG] -------------------------------
20210701-202104 (reduce_image_by_quality.sh:10) [TRACE] filename=000002.jpg
20210701-202104 (reduce_image_by_quality.sh:18) [DEBUG] filename=000002.jpg
20210701-202104 (reduce_image_by_quality.sh:31) [TRACE] inputSize=[524918]
20210701-202104 (reduce_image_by_quality.sh:47) [TRACE] q=50, qMin=1, qMax=100, size=146898
20210701-202104 (reduce_image_by_quality.sh:79) [TRACE] q=50 is size OK: 146898
20210701-202105 (reduce_image_by_quality.sh:47) [TRACE] q=75, qMin=50, qMax=100, size=194654
20210701-202105 (reduce_image_by_quality.sh:79) [TRACE] q=75 is size OK: 194654
20210701-202105 (reduce_image_by_quality.sh:47) [TRACE] q=87, qMin=75, qMax=100, size=250660
20210701-202105 (reduce_image_by_quality.sh:79) [TRACE] q=87 is size OK: 250660
20210701-202105 (reduce_image_by_quality.sh:47) [TRACE] q=93, qMin=87, qMax=100, size=308618
20210701-202105 (reduce_image_by_quality.sh:83) [TRACE] q=93 is size NG: 308618
20210701-202105 (reduce_image_by_quality.sh:47) [TRACE] q=90, qMin=87, qMax=93, size=276605
20210701-202105 (reduce_image_by_quality.sh:79) [TRACE] q=90 is size OK: 276605
20210701-202106 (reduce_image_by_quality.sh:47) [TRACE] q=91, qMin=90, qMax=93, size=286116
20210701-202106 (reduce_image_by_quality.sh:79) [TRACE] q=91 is size OK: 286116
20210701-202106 (reduce_image_by_quality.sh:47) [TRACE] q=92, qMin=91, qMax=93, size=295617
20210701-202106 (reduce_image_by_quality.sh:79) [TRACE] q=92 is size OK: 295617
20210701-202106 (reduce_image_by_quality.sh:47) [TRACE] q=92, qMin=92, qMax=93, size=295617
20210701-202106 (reduce_image_by_quality.sh:70) [INFO] compress ok. q=92, size=295617, file=000002.jpg
20210701-202106 (reduce_image_by_quality.sh:9) [DEBUG] -------------------------------
20210701-202106 (reduce_image_by_quality.sh:10) [TRACE] filename=000004.jpg
20210701-202106 (reduce_image_by_quality.sh:18) [DEBUG] filename=000004.jpg
20210701-202107 (reduce_image_by_quality.sh:31) [TRACE] inputSize=[2419562]
20210701-202108 (reduce_image_by_quality.sh:47) [TRACE] q=50, qMin=1, qMax=100, size=880744
20210701-202108 (reduce_image_by_quality.sh:83) [TRACE] q=50 is size NG: 880744
20210701-202109 (reduce_image_by_quality.sh:47) [TRACE] q=25, qMin=1, qMax=50, size=805318
20210701-202109 (reduce_image_by_quality.sh:83) [TRACE] q=25 is size NG: 805318
20210701-202109 (reduce_image_by_quality.sh:47) [TRACE] q=13, qMin=1, qMax=25, size=761083
20210701-202109 (reduce_image_by_quality.sh:83) [TRACE] q=13 is size NG: 761083
20210701-202110 (reduce_image_by_quality.sh:47) [TRACE] q=7, qMin=1, qMax=13, size=734253
20210701-202110 (reduce_image_by_quality.sh:83) [TRACE] q=7 is size NG: 734253
20210701-202111 (reduce_image_by_quality.sh:47) [TRACE] q=4, qMin=1, qMax=7, size=717283
20210701-202111 (reduce_image_by_quality.sh:83) [TRACE] q=4 is size NG: 717283
20210701-202112 (reduce_image_by_quality.sh:47) [TRACE] q=2, qMin=1, qMax=4, size=709228
20210701-202112 (reduce_image_by_quality.sh:83) [TRACE] q=2 is size NG: 709228
20210701-202113 (reduce_image_by_quality.sh:47) [TRACE] q=1, qMin=1, qMax=2, size=709187
20210701-202113 (reduce_image_by_quality.sh:68) [WARN] 最低品質でもサイズオーバーです。q=1, size: 709187 file: 000004.jpg
```

### 全コード
https://github.com/kumanobori/propScripts/tree/master/bash/reduce_image_by_quality

# ポエム
一括処理ソフトがうまく動いたり動かなかったりする中、それなりには動くことを期待して手作業を織り交ぜてやったのですが、思ったよりまともに動いてくれませんでした。途中でimagemagickを持ってきて簡易スクリプトを書きながら手作業で進め、終盤で「これスクリプト書いた方が速かったな・・・」となり、終わってから半ば自棄でこれを書きました。作業の4分の1の時間で書けました。
「次があれば秒で終わるぜ！」と言いたいところですが、たぶん二度とないと思います。かなしい。