初歩のシェルスクリプトで遊ぶ[JPEG XLでのjpegロスレス圧縮を試す]

今回はシェルスクリプトではなく、コマンドの試用です。

JPEG XL ではjpegファイルを中身そのままでサイズ圧縮できるらしい

が、手軽に試せるツールが見当たらん。

*.jxl — Krita Manual 5.2.0 ドキュメント
有名どころが JPEG XL に対応した、という話はときどき聞きますが、ものを見てみると通常のロッシー圧縮かロスレス圧縮かの対応です。
私が試したかったのはjpegをそのまま中身のデータを保ってファイルサイズを圧縮できて、しかも再び元のjpegファイルに戻せる、という圧縮です。これはwebpとavifではできない処理です。

JPEG XL - ArchWiki
GitHub - libjxl/libjxl: JPEG XL image format reference implementation
Debian -- Details of package libjxl-tools in bookworm
GitHub - uyjulian/ifjxl: JPEG XL plugin for Susie Image Viewer

てっきりソースからどうこうせにゃならんかと思ってましたが、私が使ってるDebianでも、バージョンは少々古いけれど普通に apt で入りました。
コマンドも、オプションを指定しなければならないだろうと決めつけてかかってましたが、オプションなしでjpegファイルを突っ込むと、元に戻せるjpegロスレス圧縮になるみたいです。

$ cjxl input.jpg output.jxl

説明書は詳しく、詳しく

For more settings run cjxl --help or for a full list of options run cjxl -v -v --help.

https://github.com/libjxl/libjxl
$ cjxl -v -v --help
$ cjxl --verbose --verbose --help

説明だいたいこれだけで終わるんですが、--verbose を二回指定してヘルプを出すと、詳しい説明書が読めます。

使ってみた感じ(一般的な8bit420のみ、メタデータも調べていない前提で)

  • 元のjpeg「ファイル」に戻せた。md5の値をとると全く同じになる。
  • 何かしらの最適化がされたjpegファイルは普通にwebpやavifでロッシー圧縮すると逆に容量が増加することもあるが、そういったjpegファイルでも15%程度圧縮できた。
  • つまり、webpやavifで再圧縮するときと比べて、画質劣化や容量増加の問題を気にせずに容量削減ができて、しかも元に戻せる。これは非常に気楽である。
  • 上記のsusieプラグインでも表示できた。
  • .jxlを表示できる画像ビューアから、直接にpngに変換すると、なぜか元のjpegとは微妙に異なった。何か難しいことがあるっぽい。

むしろjpegファイルを普通に画質劣化させて.jxlに変換するほうがややこしい

$ cjxl input.jpg output.jxl -j 0 -d 1 -e 7

「-j」「-d」の指定が両方とも必須です。「-e」はなくてもよい。
まとめて画像ファイルを変換するスクリプトを書くときは、入力がjpegでもpngでもロッシー圧縮させるようなオプション指定をするなり、検討は要ります。

参考スクリプト

私が使ってるスクリプトです。
画像ファイルをまとめて変換するスクリプトに組み込んで使ってるもので、このシェルスクリプトを xargs から実行してます。変換元の画像ファイルを削除するので注意してください。試用するときはrmコマンドの部分を削除してから実行するのがお勧めです。
複数の画像ファイルを同時に変換したいときは、xargsのオプションで -P を指定します。
異常時の終了コードを255にしているのは、xargsが255を受け取ると即座に終了してくれるからです。
拡張子は .jpg をそのまま残し、.jxl を追加しています。二重拡張子です。これは私が個人的にやっていることで、一般的なルールではありません。.tar.gz みたいなものではないのでご注意。

#!/bin/sh
#set -x
# ========================================================================================
# ■ jpegをjxlへ無劣化変換
# 
# ・入力ファイルはjpegのみ扱う
# ・拡張子が.jpeg.jpg.jpeのとき処理し、でなければ何もせずに正常終了する
#     jpeg以外のファイルが混じっていても処理できるように
# ・出力ファイル名は元ファイル名に [.jxl] を加える
# 
# 
# 
# 
# ========================================================================================


if test -f "${1:?}"
then inputImage="${1}"
else exit 255
fi

# jpeg jpg jpe のみ処理対象とする
# これ以外のファイルには手を付けない
case "${inputImage}" in
( *.[jJ][pP][eE][gG] | *.[jJ][pP][gG] | *.[jJ][pP][eE] )
  true
;;
( * )
  exit 0
;;
esac



# 出力ファイル先を確保
outputTemp="`mktemp`"

# jpeg-xlへ変換
if cjxl "${inputImage}" "${outputTemp}" \
         --lossless_jpeg=1 --distance=0 -e 9 \
         --jpeg_store_metadata=1 --verbose ; then
	true
else
	exit 255
fi

# 変換元jpegファイルを削除する
if rm -v -- "${inputImage}"
then true
else exit 255
fi

# jxlファイルを変換元jpegファイルが置かれていたディレクトリへ移動する
if mv -v -n -- "${outputTemp}" "${inputImage}.jxl"
then exit 0
else exit 255
fi
#!/bin/sh
#set -x
# ========================================================================================
# ■ jpegをjxlへ無劣化変換、したものをjpegへ戻す
# 
# ・入力ファイルはjpegを無劣化変換してjxlにしたもののみ扱う
# ・拡張子が.jxlのときjxlinfoで検査、対象ファイルは処理、対象外は何もせずに正常終了する
# 
# ・ファイル名の二重拡張子も考慮して出力拡張子を名付ける
# [.jpg.jxl]->[.jpg]
# [.jxl]    ->[.jpg]
#                
# 
# 
# 
# ========================================================================================


if test -f "${1:?}"
then inputImage="${1}"
else exit 255
fi

# jxl のみ処理対象とする
# これ以外のファイルには手を付けない
case "${inputImage}" in
( *.jxl )
  true
;;
( * )
  exit 0
;;
esac



if jxlinfo "${inputImage}" | grep -e '^JPEG bitstream reconstruction data available$'
then echo "${inputImage}" 1>&2
else exit 0
fi



# 出力ファイル先を確保 djxlの出力先拡張子は.jpgでなければならない
outputTemp="`mktemp --suffix='.jpg'`"

# jpeg-xlへ変換
if djxl "${inputImage}" "${outputTemp}" ; then
	true
else
	exit 255
fi

# 変換元jxlファイルを削除する
if rm -v -- "${inputImage}"
then true
else exit 255
fi


# .jxl を削る
inputImage="${inputImage%.jxl}"

# .jpg があれば残し、なければ付加する
case "${inputImage}" in
( *.[jJ][pP][eE][gG] | *.[jJ][pP][gG] | *.[jJ][pP][eE] )
  true

;;
( * )
  inputImage="${inputImage}.jpg"

;;
esac

# jpgファイルを変換元jxlファイルが置かれていたディレクトリへ移動する
if mv -v -n -- "${outputTemp}" "${inputImage}"
then exit 0
else exit 255
fi