ホンモノのエンジニアになりたい

ITやビジネス、テクノロジーの話を中心とした雑記ブログです。

【Python】imghdr.what()でJPEGファイルが判定できない。なぜ?

はてなマーク


タイトルの通り、imghdr.what()で画像ファイルのフォーマットを調べたら一部のJPEGファイルを上手く検出できない事象に遭遇しました。このエントリではこの問題の原因と対策として、調べてみたこと、やってみたことをまとめます。

はじめに

Webアプリを作って遊んでいたときに、アップロードされた画像のファイル種別を判定しようとしていました。

そこでimghdr.what()を使ったのですが、どうもJPEGファイルなのに分岐にかからずに抜けてしまうファイルが存在することに気づきました。なんでやねん、なんでやねん、と頭をポカスカ叩きながら考えてもよくわからず、とりあえずググってみることに。

すると、幸いなことに既に原因と解決策を公開してくれている人がおりました。

qiita.com

このエントリでは私なりにもう半歩ほど踏み込んで事象を整理してみようと思います。

環境
CentOS Linux release 7.4.1708 (Core)
Python 3.6.5


何で検出できないか?

上で紹介したQiita記事に簡潔にまとめられていますが、ソースコードではこうなっていると。

def test_jpeg(h, f):
    """JPEG data in JFIF or Exif format"""
    if h[6:10] in (b'JFIF', b'Exif'):
        return 'jpeg'


ファイル先頭の7~10バイトのところに'JFIF'、'Exif'という文字列があればJpegと判定する仕様になっています。 WikipediaでJPEGを調べてみると、以下のように書いてあります。

マジックナンバー \xff\xd8

標準では、特定の種類の画像の正式なフォーマットがなく、JFIF形式(マジックナンバー上は、6バイト目から始まる形式部分にJFIFと記されているもの)が事実上の標準ファイルフォーマットとなっている。 JPEG - Wikipedia


マジックナンバーは「FFD8」であるが、JPEGの事実上の標準はJFIFであり、それを表すのは6バイト目からの「JFIF」文字列(を表すバイナリ部分)であると。
手元のJPEGファイルをバイナリエディタで見てみるとこうなっている。

①普通のJPEG
f:id:kwnflog:20181230171758p:plain

②問題のJPEG f:id:kwnflog:20181230171844p:plain

③Exif形式 f:id:kwnflog:20181230171904p:plain

問題のJPEGではマジックナンバーの直後に量子化テーブル定義という、画像情報を解釈するための一種の係数のようなものが入っています。 本来はJFIFだと「FFE0」、Exifだと「FFE1」が来ないといけないっぽいです。

ちなみにLinuxのfileコマンドでは以下のように表示されます。fileコマンドは偉大。

$ file JFIF-ari.jpg
JFIF-ari.jpg: JPEG image data, JFIF standard 1.01

$ file JFIF-nasi.jpg
JFIF-nasi.jpg: JPEG image data

$ file Exif.JPG
Exif.JPG: JPEG image data, EXIF standard 2.2


まとめると、どんな理由かはわかりませんがJPEGファイルのヘッダに事実上の標準とされているJFIF情報が存在しなく、そこを読んでファイル形式を判定するimghdrコマンドも機能しないというのがうまく判定できない理由となるようです。


JFIF、Exif無しファイルが来た時にどーするか?

冒頭でリンクを貼ったQiitaの人と同じような対応をしました。先頭の2バイトがFFD8ならJPEGと判定するロジックです。(ロジックというレベルじゃないですね)

f=open('/path/to/file','rb')
f_head = f.read()[:2]
f.close()
if f_head == b'\xff\xd8':
    print('JPEGです')


これをimghdr.what()に引っかからなかったファイルに実行してJPEG判定をする。

たぶんもっとエレガントな書き方、対応法があるんでしょうけど、目的である画像ファイルフォーマットの判定は出来たのでこれでよしとします。

おわり