自宅サーバで運用してる 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 ファイルのせいなのか?
これ以上追いかけるのはちょっと手に余るな。