RM-BLOG

IT系技術職のおっさんがIT技術とかライブとか日常とか雑多に語るブログです。* 本ブログに書かれている内容は個人の意見・感想であり、特定の組織に属するものではありません。/All opinions are my own.*

【Java】画像ファイルのチェック方法

Javaによる、画像ファイルのチェック方法について。



 


「ユーザにファイルをアップロードさせる」ような機能を作る際、
なんでもかんでもアップロードさせるわけにはいかないので、
大抵以下のようなチェック機構が必要になる。

  1. アップロードするファイルが存在するか
  2. アップロードするファイルは機能仕様上許可しているファイルフォーマットか
  3. アップロードするファイル容量は適切か(システム上制限しているアップロードファイル容量以下か)

とか。

うち、2.については、
例えば、アップロードするファイルを「画像ファイル」と決めている場合は、
「指定されたファイルは本当に画像ファイルか?」
というのをチェックしたい(チェックしなくてはならない)わけである。
経験上、こういうチェックをするにあたっては、たいてい、
「ファイルの拡張子が画像ファイルのものになっているか?」というのが、
簡易的に実施できるということもあってよくチェック方法として採用される。
Javaで実装するとなると以下のような感じか。

	private static boolean isImageFileByExpression(File file) {
		
		if (file != null && file.isFile()) {
			String filename = file.getName();
			String filenameUppered = filename.toUpperCase();
			if (filenameUppered.endsWith(".JPG")
				|| filenameUppered.endsWith(".JEPG")
				|| filenameUppered.endsWith(".PNG")
				|| filenameUppered.endsWith(".GIF")
				|| filenameUppered.endsWith(".BMP")
				|| filenameUppered.endsWith(".TIFF")
			) {
				return true;
			} else {
				return false;
			}
		} else {
			return false;
		}
	}


ファイル名としては拡張子の英字小文字大文字は別物になる(「A.gif」と「A.GIF」で別のファイルになる)ので、
一旦全部大文字にして判定を行うようにする。(別に小文字でもいいけど)
「画像」としての拡張子の種類は他にもある(TIFFとかも?)のでその分だけ列挙する必要があるが、
数が多くなるようなら外だしのプロパティファイルとかにして、そこと照合させるようなつくりにしたほうがいいのだろう。。




大体は↑にあげた拡張子でチェック可能だろうが、ファイルの拡張子というのは、
しょせん「ファイル名の末尾の最後の"."からの文字列」にすぎず、いくらでも自由に加工できる。
極端な話、

  • 中身はテキストファイルなんだけど拡張子だけ「JPG」
  • 中身は画像ファイルなんだけど拡張子だけ「CSV

とかいうことも全然可能なわけである。
(まあ一般人でそんなことするやつほとんどいないだろうが……)
このようなケースだと拡張子のチェックをするだけではそのファイルが画像かどうかの切り分けが付かない。
ので、そういう場合にどうするか?

Javaには画像ファイルを取りあつかうクラスがあり、
それで画像ファイル以外を読み込むと、得られる結果がnullになる。
これを使うことで画像かどうかの切り分けが可能となる。

	private static boolean isImageFileByImageIO(File file) throws Throwable {
		if (file != null && file.isFile()) {
			BufferedImage bi = ImageIO.read(file);
			if (bi != null) {
				return true;
			} else {
				return false;
			}
		} else {
			return false;
		}
	}


実装にあたってはjavax.imageio.ImageIOのインポートが必要である。
ImageIO#readにFileを渡すと、java.awt.image.BufferedImageが得られるが、
引数に渡したFileが画像ファイル以外の場合、このBufferedImageがnullになってかえってくる。
これを利用し、「nullだったら画像じゃない!」と判断するのである。(逆にnullでなければ画像と見做してよい)
BufferedImageには縦・横のサイズを取得するメソッドもあるので、
アップロードした画像ファイルを適切な場所に当てはめるようなとき、
当てはめる先の縦横サイズが一致しているかどうかというチェックも可能である。

しかし、ぶっちゃけると、これがどういう原理で画像読込を実現しているかははっきりとわからないので、
「こういうことやると画像かどうかの判定ができる」ということがわかっているだけである。
API見るといろいろ書いてあるが
ImageIO#readの目的はあくまで画像の読み込みであり、処理内で「読み込めそうなReaderを探す」というのがまず最初にあり、
「読み込めそうなReaderがない」ということから結果的にnullがかえってくる、というのがその実のようだ。
なので(当たり前だが)nullかどうかで判定できるのは「読み込む」という流れから生まれた副産物に過ぎない、
というのは一応意識しておかないといけない。

そもそもファイルというのはテキストだろうが画像だろうが音楽だろうが動画だろうが、
全てバイナリ(バイトコード値)で書かれたものなので、
俺が知らないだけで、テキストでいう文字コードのようなものと同様、画像にも、
「画像自体を表すバイトコード値をもとにそれぞれの画像表示に即した形でエンコード(フォーマット)する方式」が多分、あるんだろうな。
その辺の判定と、判定結果に基づき適切なReaderを使って画像を読み込む処理を、
ImageIO#readが実施しているんだろうな~となんとなく予想している。




ちなみに↑にあげた2つのロジックで下記にあるファイル群をチェックすると、
チェック結果は以下の通りになる。

Noファイル名ファイルの実態内容isImageFileByExpression
(ファイルの拡張子でチェック)の結果isImageFileByImageIO
(ImageIOでのチェック)の結果

1 IMG_0557.JPG.txt 画像 「IMG_0557.JPG」という画像ファイルの末尾に「.txt」を無理やりつけたもの。
ファイル名を変えただけだから実態は写真。
画像ではない 画像である
2 test.txt.bmp 0バイトファイル 中身空っぽの0バイトファイルをこういう名前で保存しただけ。 画像である 画像ではない
3 test.txt.tiff 0バイトファイル 中身空っぽの0バイトファイルをこういう名前で保存しただけ。 画像である 画像ではない
4 IMG_1146.PNG 画像 iPhoneでスクショとった画像。
拡張子も画像。
画像である 画像である
5 IMG_1148.JPG 画像 iPhoneで撮った写真。
拡張子も画像。
画像である 画像である
6 Windows エラー.wav 音声 C:\Windows\Media\Windows エラー.wav。
テキストでも画像でもないが、
「画像ではないバイナリファイル」の例として採用。
画像ではない 画像ではない
7 Windows Error.wav.jpg 音声 C:\Windows\Media\Windows エラー.wav
の、ファイル名末尾に「.jpg」を無理やりつけたもの。
名前変えただけだから、実態は音声ファイルのままで何も変わっていない
画像である 画像ではない


他にも例を挙げだすといろいろあるが、とりあえずこんなところにしておく。
今回紹介した2つの判定ロジックの差が一番わかりやすく出るのはNo1.だろう。
実態は写真(画像)なのに、拡張子判定だと画像と判定されないが、ImageIOだと画像と判定してくれる。

逆に、No.2やNo.3のような例では、拡張子判定だと画像と判定されるが、ImageIOだと画像と判定しない。
まあ0バイトだから、拡張子判定で画像と判定して先に進んでも、
冒頭のチェック例でいうと3.のチェック(ファイル容量のチェック)でひっかかって結局止まりそうだが。

といいつつ、No.1のような例は、
まあ、確かに、実態は画像なのは間違いないが、
「意図的に拡張子を変えている」ということを考えると、
「ImageIO#readの結果でOKになったから」といって画像と判定しきってしまうのも正直早計な気はする。
意図したものかどうかに寄らず、実態に即した拡張子とは異なるわけだから、
「実態は画像であっても、本来あるべきファイルの姿ではない」というように解釈し、
つまり結果的に「画像ではない!」と判定してあげるのが正しい姿なのだろう。
なので、画像チェックとしての正解は、
”「拡張子チェック」+「ImageIO#readでのチェック」で両方がtrueになったら画像”
なのだろうな。

なお、実態が画像であれば、拡張子に関わらず、HTMLのimgタグに設定しても画像として表示してくれるようではある。
これは、画像データをBASE64形式でエンコードしてimgタグのsrc属性に指定するやり方ができることからも、なんとなく想像がつくことではある(実態が画像を指していれば画像になる)が、
画像としての使用用途は別にHTMLに限った話ではないはずだし、やはり拡張子と実態は合っているべきだろう。