読者です 読者をやめる 読者になる 読者になる

何かを書き留める何か

数学や読んだ本について書く何かです。最近は社会人として生き残りの術を学ぶ日々です。

Pythonでバイナリ列から画像の種類を判別する

拡張子を信じたい人生であった。

画像ファイルがネットワークを介して飛んでくる。 拡張子を信じて保存をしたいが、拡張子だけ書き換えたり中身が改ざんされているかもしれない。 画像フォーマットごとにあるヘッダを読んで画像を識別したい。

表題には「Pythonで」と書いたが、本質的にバイナリ列と正規表現が扱えるプログラミング言語ならば大差は無いと思われる。 意外とPythonで書かれた記事が見当たらないので一応まとめておこうという気になって書いたのがこのエントリ公開の動機である。

基本方針

大抵の画像フォーマットには仕様が定められている。 つまり、何らかの決まったヘッダーが存在する。 そのヘッダー情報を正規表現で読み取って判別しよう、というのが基本方針である。

JPEG

JPEGは離散コサイン変換を利用した非可逆圧縮の画像フォーマットである。 ISO/IEC 10918-1で仕様が定められ、JISでもJIS X 4301で定まっている。 仕様を読み解くと*1、画像の始まりを示すマーカー(SOI)として\xff\xd8が定義されている。 よって、JPEGがどうかを判定するには次のようにすればよい。

import re
import typing


def is_jpg(b: bytes) -> bool:
    """バイナリの先頭部分からJPEGファイルかどうかを判定する。"""
    return bool(re.match(b"^\xff\xd8", b[:2]))

PNG

PNG可逆圧縮の画像フォーマットである。 ISO/IEC 15948で仕様が定められ、PNGファイルの最初の8バイトは\x89\x50\x4e\x47\x0d\x0a\x1a\x0aであると定められている*2。 よって、PNGがどうかを判定するには次のようにすればよい。

import re
import typing


def is_png(b: bytes) -> bool:
    """バイナリの先頭部分からPNGファイルかどうかを判定する。"""
    return bool(re.match(b"^\x89\x50\x4e\x47\x0d\x0a\x1a\x0a", b[:8]))

GIF

GIFは可逆圧縮の画像フォーマットである。 圧縮アルゴリズムのライセンスをめぐるゴタゴタがあったそうであるが私はその歴史について全く知らない。 私にとってGIFは画像ファイルというよりアニメーションのためのフォーマットである。

https://www.w3.org/Graphics/GIF/spec-gif89a.txtにあるGIFの仕様を眺めると、ヘッダーとして最初はGIF、つまり\x47\x49\x46となり、続いてバージョン番号である87a89aが続く。 よって、GIF8がヘッダにあるかどうかを調べればよい*3

import re
import typing


def is_gif(b: bytes) -> bool:
    """バイナリの先頭部分からGIFファイルかどうかを判定する。"""
    return bool(re.match(b"^\x47\x49\x46\x38", b[:4]))

PDF

PDFはAdobe社が開発したドキュメントフォーマットであるが、画像として送り付けられる場合もある。 ヘッダは%PDF-なので、それを調べればよい*4

import re
import typing


def is_pdf(b: bytes) -> bool:
    """バイナリの先頭部分からPDFファイルかどうかを判定する。"""
    return bool(re.match(b"^%PDF", b[:4]))

Windows Bitmap

Windows BitmapはIBMMicrosoftが開発した画像フォーマットである。 ここ最近、見かけた覚えがないがネットワークの向こう側にいる人間にとっては身近な画像フォーマットかもしれないのだ。 冒頭がBMつまり\x42\x4dから始まるのでそれをチェックすればよい。

import re
import typing


def is_bmp(b: bytes) -> bool:
    """バイナリの先頭部分からWindows Bitmapファイルかどうかを判定する。"""
    return bool(re.match(b"^\x42\x4d", b[:2]))

拡張子を信じることができる世界ならば

今までの前提の否定、つまり拡張子と実際のデータが一致している場合は既存のライブラリで間に合う。 それはmimetypesモジュールである。

19.5. mimetypes — ファイル名を MIME 型へマップする — Python 3.6.1 ドキュメント

最後に

よくあるフォーマットをまとめて、バイナリを入れるといい感じにContent-Typeを返してくれるライブラリが欲しいなあとか思ったり思わなかったりする。

*1:実際は「jpeg ヘッダ」で検索したのち、仕様を調べてで裏取りを行った。

*2:最初はWikipediaで知った

*3:これも最初に知ったのはWikipedia

*4:Vimで適当なPDFファイルを開いてバイナリを調べたのち、仕様を調べた。