初歩のシェルスクリプトで遊ぶ[webp画像に変換したいが難しい(1)]

  • jpeg,png,bmp,webp形式の画像ファイルを、webpに変換するスクリプトが欲しい。
  • 1枚から1000枚程度の画像ファイルを、まとめて変換したい。
  • 作成したwebpファイルは、変換元の画像と同じディレクトリに置く。
  • ファイル名は拡張子を「.webp」に書き換える。名前部分はコピーする。
  • 変換元の画像ファイルは、webpへの変換に成功したならば、削除する。
  • 2枚以上は同時に変換処理をしたい。
  • 画像変換には、cwebpコマンドを使う。
  • cwebpに与えるパラメータは変えられるようにしたい。
  • VirtualBoxWindowsホストのUbuntuゲストから、共有フォルダ内の画像ファイルを変換したい。

単に「変換する」だけで、縮小などの画像処理は行いません。

なんでシェルスクリプトなんて組んでるのか

このブログ、唐突にシェルスクリプトのブログになってるわけですが、そもそもwebp画像にまとめて変換するソフトが欲しかったのです。
出来合いの変換ソフトを探したんですが、期待したものがなかなか見つからなかったんすよ。あるにはあるけど、細かい不安や不満が残るとか。

自分で作ることはできないけれど。でもcwebpとimagemagickを使えば、これを実行するだけのスクリプトくらいなら、私でも書けるような気がした。
確かに、自分用に使えるスクリプトは書けた、のだけれど。

意外と難しかった

  1. ファイルパスの文字列には、空白やらが入る。このへんの、初心者がみんな躓くところで躓いた。
  2. ファイル名の重複など、ファイル名の変換ソフトを作るのと同じ配慮が必要になった。
  3. 複数画像を同時に変換すると、さらにファイル名が衝突したりもした。
  4. たまに「ファイルがビジー状態で云々」が起きた。
  5. ユーザインターフェースが欲しくなった。
  6. アンドゥ的な機能、間違った操作を元に戻す機能が欲しくなった。
  7. のちのちはWindowsbusybox用でも動かせるスクリプトにしたいから、変わった機能は使いたくない。

現状はこんなんなってます。作ったり壊したり古かったりで、全体は人に見せられるもんじゃありません。まぁ使えちゃいるんですが……。
ちなみに名前は「レンコン」。

いま使ってるスクリプト

要cwebp,webpinfo
A new image format for the Web  |  WebP  |  Google Developers

#!/bin/sh
#set -x
# ===============================================================================================
# ■ 画像をwebpへ変換 1枚のみ
# 
# 入力は画像のファイルパス文字列
# 古い画像は処理に成功したら削除する
# 
# ・無劣化webpへ変換
# printf "%s" "元画像ファイルのパス改行区切り" |
# xargs --max-procs=2 --delimiter='\n' -I '{}' cwebpSingle.sh -p "-q 100 -m 6 -lossless" -- '{}'
# 
# 
# 
# ===============================================================================================
cwebpParm=''
#lockDir="${HOME}/cwebpLock"
lockDir="/dev/shm/cwebpLock"
tempDir='/dev/shm'


while test ${#} -ne 0 ; do
case "${1}" in
( -p )
	shift
	cwebpParm="${1}"
;;
( -- )
	shift
	break
;;
( * )
	exit 255
esac
shift
done

if test -s "${1}" ; then
	dirName="$( cd -- "`dirname -- "${1}"`" && pwd )"
	inputImage="${dirName%/}/`basename -- "${1}"`"
else
	exit 255
fi

outputTemp=`mktemp -p "${tempDir}"`

# 直接cwebpが成功するなら良し
if cwebp ${cwebpParm} -o - -- "${inputImage}" 1> "${outputTemp}" ; then
	true
# 直接cwebpが失敗したらconvertを通してみる
elif convert "${inputImage}" -type truecolor -compress None -verbose tiff:- |
cwebp ${cwebpParm} -o -  -- -  1> "${outputTemp}" ; then
	true
# それでも駄目なら諦める
else
	exit 255
fi


# 【ザル】画像変換に成功したか?
if webpinfo -quiet "${outputTemp}" ; then
	true
else
	exit 255
fi

# 変換元画像を削除
for NN in 0 1 2 ; do
sleep ${NN}s
if rm "${inputImage}" ; then
	break
fi
done
if test -s "${inputImage}"
then exit 255
fi


# ロックディレクトリ作成
until mkdir -v "${lockDir}" ; do
sleep 1
done

# 新規ファイル名を作成
imageName="${inputImage%.*}"
while test -f "${imageName}".webp ; do
	imageName="${imageName}_"
done

# 新規ファイル名に変換
if mv -n -v "${outputTemp}" "${imageName}.webp" ; then
	rmdir "${lockDir}"
	exit 0
else
	rmdir "${lockDir}"
	exit 255
fi

つかいかたの例

printf "%s" "${allFiles}" | xargs --max-procs="${cwebpParaCount}"  -I '{}' \
cwebpSingle.sh -p "${cwebpParm}" -- '{}'

改行区切りの画像ファイルパスを標準入力して、xargsで並列に実行。

変換するだけなら簡単だったけど

自作のスクリプトの部品を上に載っけました。1枚の画像ファイルを変換するスクリプトを作って、それをxargsで並列に動かす方式です。
変換だけならすぐできましたが、ちゃんとやるのは難しくて。これも駄目な気がしてます。初期に書いたのよりはマシだと思いたいけれど。
いろいろ注意したところを、ざっと書いてくんで、ご自分で書くときの参考にしてもらえたら。

基本的な流れ

  1. ファイルパスを受け取る。
  2. cwebpにそのまま突っ込んでみる。
  3. もしcwebpが成功したら次の処理へ進む。
  4. 失敗したら、imageMagicktiff画像への変換を挟んでから、cwebpを再度試みる。
  5. 成功したら次へ。失敗したら異常と見なしてexit 255
  6. webpinfoでwebp画像ファイルが正しく作れたか確認する。失敗していたらexit 255
  7. 変換元画像を削除
  8. ロックディレクトリを作成、作れないなら1秒待ってから、再度作成を試みる。
  9. 新規ファイル名を作成
  10. mvコマンドで新規作成webp画像を移動とリネーム
  11. ロックディレクトリを削除

同時変換はどうやる?

xargsのオプションを指定して、複数枚同時に画像変換する方法です。
利点は、第一に、オプションで同時実行が簡単にできること。第二に、何かしらの異常時には「exit 255」するスクリプトにしておけば、xargsが動作中断してくれること。
欠点は、busyboxのxargsに同時実行ができんかったことと。シェル関数をxargsで動かすのが難しいこと。

他の方法だと、たとえば末尾「&」でバックグラウンド実行させればいいんですが、ここでは取りあげません。
幾つかやってはみたのだけれど、私がやると完璧にオモチャになりました。名前付きパイプで変換作業終了の合図を送ってタイミングを取るとか、勉強と遊びと思えば面白いっちゃあ面白かったんすけど。

ファイル名はどうやって作ろう?

基本的に元のファイル名を残して、拡張子だけ「.webp」に変換します、が。たとえばです。

image.jpg
image.png
image.bmp
image.webp

とかだったらどうしましょう。
上記のスクリプトでは、ファイル名の最後に「_」を付けることで対応しようとしてるんだけれど、はたしてちゃんとできてるのやら。

cwebpで変換できる画像とできない画像があるのはどうする?

imageMagickとその仲間でwebpが使えますから、それでいいなら直接に突っ込めばいいんですが、私は最新版のcwebpが使いたいのです、どうしても。
さらに、必要もなくimagemagickを通したくもないのです。
となると、cwebpで直接に扱えるならcwebpのみに突っ込み、扱えない画像形式は、いったんpngtiffに変換しなきゃなりません。その振り分けが必要です。
この振り分けを、たとえば、拡張子でcase文を通し、「jpg,png,tiff」だけ残す方法だと、「使えるけどちょっと、なぁ」なものになる。たとえばtiff画像の一部がcwebpでは使えない、とか。色数で蹴られることがあります。(「-type truecolor」も、そのためです。白黒画像だと自動的に白黒の色数に変更されてしまって、cwebpがエラーになる。)

こういった理由から、まずcwebpに直接に突っ込んでみてから、ダメなら最初から仕切り直して、imagemagickを通してみる、という方式にしてます。

横槍が入っても大丈夫か?

「_」をつけて重複を避けるだけでは、一つずつならいいんですが、複数を同時作業すると、タイミングが悪いと重複しちゃいます。
また、作業中に別のファイルを誰かが追加してきたら、それとも名前が同じになっちゃうかもしれない。

このへん、今のところはmvコマンドの上書き禁止オプションと、簡単なロックディレクトリ(ロックファイル)で対応してます。
まず最後の砦として(あくまで最後の砦)、mvコマンドに「-n」を指定します。これに失敗したら異常と見なして強制終了。exitを255にすると、xargsが終わってくれる、と期待して。

新規ファイル名の作成とmvコマンドの実行は、複数を同時実行できません。この部分は、ロックディレクトリでタイミングをとります。ロックディレクトリを作れたならば処理を進めて、作れないならば待つ。
ロックファイルじゃなくてディレクトリなのは、「mkdir」一発でできるから、ですね。
この、簡単なやり方だと、異常時にロックディレクトリが残っちゃいますが、そのときはロックディレクトリを手作業で削除することで済ませます。

欠点とか問題点とか

  • ロックディレクトリが残るとゴミになるのが問題といえば問題だ。が、ロックを手動削除しないと再実行できないのは、悪いとは言えないかも。
  • この方式だと、標準入力から画像データのバイナリを受け取って変換出力、がやりにくい。
  • 独立したスクリプトファイルだから簡単にxargsできるが、スクリプトファイルをシェル関数に置き換えるのが難しい。xargsも含めたスクリプトをファイル1枚に収めるのが難しい。
  • busyboxのxargsでは、並列に実行ができなかったような気がする。単純に指定じゃダメっぽい。
  • 「ビジー状態です云々」がときどき出るのが分からん。
  • Windowsbusyboxで動かすならWindows用に書き分けたほうが簡単な気もするが、これくらいなんとかならんかな。