easy-thumbnailsで発生するInvalidImageFormatErrorの調査メモ

easy-thumbnailsというPythonライブラリのエラー調査をしたので備忘録

InvalidImageFormatErrorが発生したら

easy-thumbnailsでは、画像読み込み時のエラーを丸めてInvalidImageErrorというエラーとして送出します。

このエラーが発生した場合は、easy-thumbnailsの画像を読み込む関数source_generators.pil_image()に直接画像を渡してみると、実際に発生しているエラーのスタックトレースを確認できます。

>>> from easy_thumbnails.source_generators import pil_image
>>> with open("<対象画像ファイルのパス>", "rb") as f:
>>>     pil_image(f)

実際に私が遭遇した例は以下。

  • 空の(0バイトの)画像ファイル
    • 画像生成処理に不具合があり、空の画像を登録していた
  • tEXtチャンクに大量のテキストを含むファイル

※なお、SVGファイルの場合は、SVGファイルを読み込む際に使用されるsource_generators.vil_image()に渡してみると良さそうです。(動作未確認)

easy-thumbnailsについて

easy-thumbnailsは任意のサイズのサムネイルを動的に生成してくれるライブラリです。

DjangoImageFieldを継承したThumbnailerImageFieldというフィールドを実装しており、このフィールドに画像を入れておくことで、欲しいサイズのサムネイル画像を簡単に生成することができます。

github.com

以下、ドキュメントのサンプルコードから拝借。

settings.pyにて、avatarというエイリアスで設定を定義:

THUMBNAIL_ALIASES = {
    '': {
        'avatar': {'size': (50, 50), 'crop': True},
    },
}

models.py:

from easy_thumbnails.fields import ThumbnailerImageField

class Profile(models.Model):
    user = models.OneToOneField('auth.User')
    photo = ThumbnailerImageField(upload_to='photos', blank=True)

テンプレート内でエイリアスavatarを使用して画像を取得:

{% load thumbnail %}
<img src="{{ profile.photo.avatar.url }}" alt="" />

Pythonコード内でエイリアスavatarにアクセスして画像を取得:

thumb_url = profile.photo['avatar'].url

https://github.com/SmileyChris/easy-thumbnails#example-usage

なお、エイリアスavatarにアクセスしたタイミングで動的にサムネイル画像が生成される。画像はDjangoに使用しているファイルストレージ(S3など)にキャッシュされ、2度目以降のアクセスはキャッシュから画像が取得されます。

InvalidImageFormatErrorの発生箇所を追ってみる

InvalidImageFormatErrorをraiseしているのは、Thumbnailerというクラスのgenerate_thumbnail()という、その名の通りサムネイルを生成する関数。Thumbnailerは、ThumbnailerImageFieldでも継承されているサムネイル生成機能を提供するクラス。

この処理の中で、engine.generate_source_image()という、生成するサムネイルの元画像を生成する関数を読んでおり、この戻り値がNoneの場合にInvalidImageFormatErrorをraiseしている。

        image = engine.generate_source_image(
            self, thumbnail_options, self.source_generators,
            fail_silently=silent_template_exception)
        if image is None:
            msg = "The source file does not appear to be an image: '{name}'"
            raise exceptions.InvalidImageFormatError(msg.format(name=self.name))

https://github.com/SmileyChris/easy-thumbnails/blob/2.8.5/easy_thumbnails/files.py#L387

このgenerate_source_image() の中では、generator関数を実行している。デフォルトでは、このgenerator()としてpil_image(), vil_image()の2つの関数が順に実行され、どちらかが正常な画像を返せばその画像が返却される。

これらの処理の中で例外が発生した場合、fail_silentlyフラグがFalseの場合は明示的にreturnしない(Noneを返す)挙動となっている。なお、このfail_silentlyというフラグは、profile.photo['avatar'].urlのようなエイリアスへのアクセス時のサムネイル生成ではFalseになっている。

            try:
                image = generator(source, **processor_options)
            except Exception as e:
                if not fail_silently:
                    if len(generators) == 1:
                        raise
                    exceptions.append(e)
                image = None

https://github.com/SmileyChris/easy-thumbnails/blob/2.8.5/easy_thumbnails/engine.py#L114-L121

このgenerator()として実行されるのが、冒頭に記載したpil_image(), vil_image()の関数です。これらの中で画像を開いた際に発生した例外が、easy-thumbnails側でInvalidImageFormatErrorとして丸められているということらしい。

pil_image(), vil_image()の実装: https://github.com/SmileyChris/easy-thumbnails/blob/2.8.5/easy_thumbnails/source_generators.py

まとめ

というわけで、冒頭に書いた通り、InvalidImageFormatErrorが発生した場合は直接pil_image(), vil_image()に画像を渡してみると良さそうです。