自宅サーバで運用してる Python で作った webアプリがあって、Pillow で画像のサムネイルを作るようになってるんだけど、ときどきサムネイルの作成に失敗していることがあるのに気がついた。
この webアプリを作ったときにどう考えたのか忘れたけど、もとの画像が png
ならサムネイルも png
、もとの画像が jpg
ならサムネイルも jpg
になる。失敗してるのは png
のサムネイルを作るとき(の一部)のようだ。サムネイルをつくるのには Pillow の Image.thumbnail()
関数を使ってる。
で、検証のためのスクリプトを書いた。Python と Pillow のバージョンは次の通り。
- Python 3.10.8
- Pillow 9.5.0
from PIL import Image from os import path import argparse THUMBNAIL_SIZE = (240, 240) def main(): args = parse_arguments() orig_file = args.orig im = Image.open(orig_file) im.thumbnail(THUMBNAIL_SIZE) (name, _) = path.splitext(orig_file) thumb_file = f'thumb_{name}.{args.format}' im.save(thumb_file) print(f'Thumbnail has maked successfully: {thumb_file}') def parse_arguments(): parser = argparse.ArgumentParser( description='Make thumbnail from original image.' ) parser.add_argument( 'orig', action='store', help='original image' ) parser.add_argument( '-f', '--format', action='store', default='jpg', help='specify thumbnail file format (`jpg` to default)' ) args = parser.parse_args() return args main()
使い方は簡単。もと画像のファイルを引数にして実行すればサムネイルが作られる。--format
オプションでサムネイルのフォーマットを指定できる(デフォルトは jpg
)。
takatoh@sofa: Documents > python make_thumbnail.py --help usage: make_thumbnail.py [-h] [-f FORMAT] orig Make thumbnail from original image. positional arguments: orig original image options: -h, --help show this help message and exit -f FORMAT, --format FORMAT specify thumbnail file format (default to `jpg`)
さて、このスクリプトで、用意した png
画像のサムネイルを作ってみる。フォーマットに jpg
を指定したときは期待通りにサムネイルが作成される。
takatoh@sofa: Documents > python make_thumbnail.py --format jpg sample1.png Thumbnail has maked successfully: thumb_sample1.jpg
ところが、フォーマットに png
を指定するとエラーになる。
takatoh@sofa: Documents > python make_thumbnail.py --format png sample1.png Traceback (most recent call last): File "C:\Users\takatoh\Documents\make_thumbnail.py", line 42, in <module> main() File "C:\Users\takatoh\Documents\make_thumbnail.py", line 17, in main im.save(thumb_file) File "C:\Users\takatoh\AppData\Local\Programs\Python\Python310\lib\site-packages\PIL\Image.py", line 2432, in save save_handler(self, fp, filename) File "C:\Users\takatoh\AppData\Local\Programs\Python\Python310\lib\site-packages\PIL\PngImagePlugin.py", line 1318, in _save data = name + b"\0\0" + zlib.compress(icc) TypeError: a bytes-like object is required, not 'str'
png
のときはいつもエラーになるってわけでもない。下の例のように、ちゃんとサムネイルが作成されることもある。
takatoh@sofa: Documents > python make_thumbnail.py --format png sample2.png Thumbnail has maked successfully: thumb_sample2.png
どうも、もとの png
画像によってエラーになることがあるらしい。多分まれなケース。ヒントはエラーメッセージに出ている。TypeError
だ。bytes-like object
でなければならないところで str
が来ている。Pillow の PIL/PngImagePlugin.py ファイルの1318行目でエラーになってる。ソースファイルからこの付近を抜き出してみよう。
icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) if icc: # ICC profile # according to PNG spec, the iCCP chunk contains: # Profile name 1-79 bytes (character string) # Null separator 1 byte (null character) # Compression method 1 byte (0) # Compressed profile n bytes (zlib with deflate compression) name = b"ICC Profile" data = name + b"\0\0" + zlib.compress(icc) chunk(fp, b"iCCP", data)
これは _save()
関数の一部で、下から2行目がファイルの1318行目にあたる。バイト文字列を連結して data
変数に代入してる。name
変数は上の行でバイト文字列のリテラルを代入してるし、b"\0\0"
もバイト文字列のリテラルだ。てことは zlib.compress(icc)
が怪しい。ちょっと話を端折るけど、icc
変数には im.info.get("icc_profile")
で取得した値が入ってる。im
は Pillow の Image
オブジェクト。
そこで、手を動かしてこれを追いかけてみる。
takatoh@sofa: Documents > python Python 3.10.8 (tags/v3.10.8:aaaf517, Oct 11 2022, 16:50:30) [MSC v.1933 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> from PIL import Image >>> import zlib >>> im = Image.open('sample1.png') >>> icc = im.info.get("icc_profile") >>> name = b"ICC Profile" >>> data = name + b"\0\0" + zlib.compress(icc) Traceback (most recent call last): File "", line 1, in TypeError: a bytes-like object is required, not 'str'
サムネイルを作るときと同じエラーが出た。もう少し粒度を細かくしてみよう。zlib.compress(icc)
のとこだけ。
>>> icc_compressed = zlib.compress(icc) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: a bytes-like object is required, not 'str'
ああ、つまり icc
が bytes-like object
じゃなくて str
なわけだ。
>>> type(icc) <class 'str'>
やっぱり。
エラーにならなかった sample2.png ファイルでも試してみよう。
>>> im2 = Image.open('sample2.png') >>> icc2 = im2.info.get("icc_profile") >>> type(icc2) <class 'bytes'>
なるほど、ファイルによって im.info.get("icc_profile")
で得られる値が bytes
だったり str
だったりするらしい。で、str
だと zlib.compress()
でエラーになる、と。
これは Pillow のバグなのか?それとも不適切な png ファイルのせいなのか?
これ以上追いかけるのはちょっと手に余るな。