JPEGファイル内にあるサムネイル画像
JPEGファイルにはサムネイル画像を埋め込める。
JPEGの仕様が柔軟にできているから、いろんなパターンがあるっぽい。
JFIFフォーマットへの埋め込みとか、Exif形式のデータを埋め込んでそこにサムネイルを埋め込む方式とか。
JPEG とは画像フォーマット標準化委員会と、JPEG 委員会によって決められたフォーマット形式の名前を指します。しかし一般に使われている「JPEG」とは、JPEG フォーマットの一形式である JFIF を指す場合が多いです。
(中略)
JFIF 規格にはサムネールの規定がありますが、形式には JFIF-RGB, JFXX-PALETTE, JFXX-RGB の三種類があり、他に Adobe PhotoShop の APP13 など独自形式のものもあります。種類ばかり多くてサポートソフトが少ないため、現状ではあまり活用されていません。特にファイルサイズ優先の WEB 画像では、内蔵サムネールは使わない方が無難でしょう。
Crazy*Planet:お絵描き研究室:JPEG(原理編3)
C-Cube Microsystems、Xing Technology、Digital Origin(Radius)の3社によって提唱された静止画像ファイルの規格。
(中略)
また、拡張性もあり、このフォーマットを拡張したExifなどのフォーマットがある。
JFIF - 通信用語の基礎知識
ExifはJFIFであり、情報はAPP1マーカーとして記録される。代わりに通常のJPEGと違いAPP0マーカーがない。
Exif情報は、TIFFで使われているIFD(Image File Directory)形式が使われている。
(中略)
Exifデータ中には、サムネィル画像を含めることができ、大抵のカメラはサムネイル画像を含める。
カメラにより、JPEG、TIFF-RGB、TIFF-YCbCrのいずれかが使われている。
初期には推奨が無かったようだが、Exif 2.1以降では、160×120ピクセルのJPEGが推奨されるようになったとされる。Exif系の規格DCFでは、常にこのサイズのJPEGとする、と規定されている。
サムネイルがJPEGであれば、このデータ部はJFIFに準拠し、0xFFD8(SOIマーカー)から始まり、データを含み、0xFFD9(EOIマーカー)で終了する。
TIFFであれば、そのまま無圧縮で格納されている。
Exif - 通信用語の基礎知識
Javaによるサムネイル抽出プログラムのソースコード
JPEGファイルからサムネイルを抽出するプログラムをJavaで書いた。
Javaの標準ライブラリで抽出可能なサムネイル(どんなのが抽出できるかよくわかっていない……)と、Exifに埋め込まれているサムネイルを抽出できる(metadata extractorというライブラリを使用)。
Java 5.0 (JDK1.5)以降が必要。
ソースコード。
import java.awt.image.*;
import java.io.*;
import java.util.*;
import javax.imageio.*;
import javax.imageio.stream.*;
import com.drew.metadata.*;
import com.drew.metadata.exif.*;
/**
* JPEG画像ファイルのサムネイル(サムネール)操作。
*/
public class JpegThumbnail {
public static void main(String[] args) throws Exception {
try {
if (args.length > 0) {
if (args[0].equals("extract")) {
String target = args[1];
String output = args[2];
new Extractor(target, output).extract();
} else {
usage();
}
} else {
usage();
}
} catch (Exception e) {
e.printStackTrace();
usage();
}
}
private static void usage() {
System.out.println("");
System.out.println("[JpegThumbnail]");
System.out.println("usage: java JpegThumbnail extract <target directory> <output directory>");
System.out.println("ex: java -classpath metadata-extractor-2.3.1.jar:. JpegThumbnail extract ./target ./output");
System.out.println("");
}
// サムネイル抽出テスト
private static void test2() throws Exception {
String target = "C:/sampledir";
String output = "C:/outputdir";
new Extractor(target, output).extract();
}
// サムネイル付きJPEG画像出力テスト
private static void test1() throws Exception {
writeJpegFile(
new File("C:/sampledir/jpegsample.jpg"),
new File[] {
new File("C:/sampledir/jpegsample2.jpg"),
new File("C:/sampledir/jpegsample3.jpg"),
},
new File("C:/sampledir/thumbnails.jpg"));
}
public static class Extractor {
private File target;
private File outputdir;
int counter = 1;
public Extractor(String target, String outputdir) {
this.target = new File(target);
this.outputdir = new File(outputdir);
}
public void extract() {
doit(target);
}
private void doit(File target) {
// System.out.println(target.getPath()); // debug
if (target.isDirectory()) {
File[] files = target.listFiles();
for (int i = 0; i < files.length; i++) {
doit(files[i]);
}
} else {
if (isJpegFile(target)) {
// System.out.println(target.getPath()); // debug
// standard thumbnails
try {
BufferedImage[] ti = getThumbnailImages(target);
for (int i = 0; i < ti.length; i++) {
String tfilename = outputdir.getPath() + File.separator + counter + ".jpg";
ImageIO.write(ti[i], "jpg", new File(tfilename));
counter++;
System.out.println("extracted: \t" + target.getPath() + "\t -> \t" + tfilename);
}
} catch (IOException e) {
System.out.println("error: \t" + target.getPath() + "\t : " + e.getMessage());
// e.printStackTrace(); // debug
}
// exif thumbnails
try {
BufferedImage[] ti = getExifThumbnailImages(target);
for (int i = 0; i < ti.length; i++) {
String tfilename = outputdir.getPath() + File.separator + counter + ".jpg";
ImageIO.write(ti[i], "jpg", new File(tfilename));
counter++;
System.out.println("extracted exif: \t" + target.getPath() + "\t -> \t" + tfilename);
}
} catch (Exception e) {
System.out.println("error exif: \t" + target.getPath() + "\t : " + e.getMessage());
// e.printStackTrace(); // debug
}
}
}
}
}
private static boolean isJpegFile(File file) {
String name = file.getName();
String ext = getExtension(name);
ext = ext.toLowerCase();
if (ext.equals("jpg") || ext.equals("jpeg")) {
return true;
} else {
return false;
}
}
private static String getExtension(String filename) {
int index = filename.lastIndexOf('.');
if (index != -1 && index < filename.length() - 1) {
return filename.substring(index + 1);
}
return "";
}
private static void writeJpegFile(File image, File[] thumbnailImages, File output) throws IOException {
BufferedImage bi = ImageIO.read(image);
if (thumbnailImages == null) {
thumbnailImages = new File[0];
}
BufferedImage[] bt = new BufferedImage[thumbnailImages.length];
for (int i = 0; i < bt.length; i++) {
bt[i] = ImageIO.read(thumbnailImages[i]);
}
writeJpegFile(bi, bt, output);
}
private static void writeJpegFile(BufferedImage image, BufferedImage[] thumbnailImages, File file) throws IOException {
ImageWriter w = null;
try {
ArrayList<BufferedImage> ts = new ArrayList<BufferedImage>();
for (BufferedImage t : thumbnailImages) {
ts.add(t);
}
IIOImage iioi = new IIOImage(image, ts, null);
w = getJPEGImageWriter();
ImageOutputStream os = ImageIO.createImageOutputStream(file);
w.setOutput(os);
w.write(iioi);
w.dispose();
} catch (IOException e) {
throw e;
} finally {
if (w != null) {
w.dispose();
}
}
}
private static BufferedImage[] getThumbnailImages(File file) throws IOException {
ImageReader r = null;
try {
ArrayList<BufferedImage> images = new ArrayList<BufferedImage>();
r = getJPEGImageReader();
ImageInputStream is = ImageIO.createImageInputStream(file);
r.setInput(is);
int numImages = r.getNumImages(true); // JPEGは1が返るはず
// System.out.println(file.getPath() + ": numImages=" + numImages); // debug
for (int i = 0; i < numImages; i++) {
int numThumbnails = r.getNumThumbnails(i);
// System.out.println(file.getPath() + ": numThumbnails=" + numThumbnails); // debug
for (int j = 0; j < numThumbnails; j++) {
images.add(r.readThumbnail(i, j));
}
}
r.dispose();
return images.toArray(new BufferedImage[images.size()]);
} catch (IOException e) {
throw e;
} finally {
if (r != null) {
r.dispose();
}
}
}
/**
* using: drewnoakes.com - jpeg exif / iptc metadata extraction in java
* http://www.drewnoakes.com/code/exif/
*/
private static BufferedImage[] getExifThumbnailImages(File file) throws Exception {
try {
ExifReader er = new ExifReader(file);
Metadata m = er.extract();
ExifDirectory exif = (ExifDirectory) m.getDirectory(ExifDirectory.class);
if (exif.containsThumbnail()) {
byte[] b = exif.getThumbnailData();
InputStream is = new ByteArrayInputStream(b);
return new BufferedImage[] { ImageIO.read(is) };
} else {
return new BufferedImage[0];
}
} catch (Exception e) {
throw e;
}
}
/**
* @return com.sun.imageio.plugins.jpeg.JPEGImageReader
*/
private static ImageReader getJPEGImageReader() {
Iterator<ImageReader> readers = ImageIO.getImageReadersBySuffix("jpg");
ImageReader r = null;
while (readers.hasNext()) {
r = readers.next();
// com.sun.imageio.plugins.jpeg.JPEGImageReader
// System.out.println(r.getClass().getName());
}
return r;
}
/**
* @return com.sun.imageio.plugins.jpeg.JPEGImageWriter
*/
private static ImageWriter getJPEGImageWriter() {
Iterator<ImageWriter> writers = ImageIO.getImageWritersBySuffix("jpg");
ImageWriter w = null;
while (writers.hasNext()) {
w = writers.next();
// com.sun.imageio.plugins.jpeg.JPEGImageWriter
// System.out.println(w.getClass().getName());
}
return w;
}
}
プログラムの実行結果
サムネイル画像ファイルにはナンバリングしている。
複数のサムネイルが埋め込まれていても出力できる(この実行結果では ./sampledir/thumbnails.jpg)。
$ ls -l
合計 120
-rw-r--r-- 1 okojo okojo 2457 2009-10-18 10:41 JpegThumbnail$Extractor.class
-rw-r--r-- 1 okojo okojo 6889 2009-10-18 10:41 JpegThumbnail.class
-rw-r--r-- 1 okojo okojo 7198 2009-10-18 09:44 JpegThumbnail.java
-rw-r--r-- 1 okojo okojo 88731 2009-10-18 09:44 metadata-extractor-2.3.1.jar
drwxr-x--- 2 okojo okojo 4096 2009-10-18 22:31 output
drwxr-xr-x 4 okojo okojo 4096 2009-10-18 22:31 sampledir
$ java -classpath metadata-extractor-2.3.1.jar:. JpegThumbnail extract ./sampledir ./output > a.txt
$ cat ./a.txt
extracted exif: ./sampledir/F7150029.JPG -> ./output/1.jpg
extracted: ./sampledir/thumbnails.jpg -> ./output/2.jpg
extracted: ./sampledir/thumbnails.jpg -> ./output/3.jpg
error exif: ./sampledir/thumbnails.jpg : expected jpeg segment start identifier 0xFF at offset 32430, not 0x1f
extracted exif: ./sampledir/1/jpegsample.jpg -> ./output/4.jpg
extracted exif: ./sampledir/1/11/IOJ_0005.JPG -> ./output/5.jpg
extracted exif: ./sampledir/2/okj.jpg -> ./output/6.jpg
error: ./sampledir/3000.jpg : Inconsistent metadata read from stream
ちなみに
Debian GNU/Linux etch の javac でコンパイルしたらこんなエラーがでたので、さっさとあきらめて Windows XP 上の Eclipse でコンパイルすることにした。
$ uname -a
Linux zurazura 2.6.18-6-amd64 #1 SMP Fri Aug 21 14:53:35 UTC 2009 x86_64 GNU/Linux
$ cat /etc/debian_version
4.0
$ javac -v
Eclipse Java Compiler v_677_R32x, 3.2.1 release, Copyright IBM Corp 2000, 2006. All rights reserved.
$ javac -1.5 -cp ./metadata-extractor-2.3.1.jar JpegThumbnail.java
(中略)
3. ERROR in JpegThumbnail.java (at line 149)
ArrayList<BufferedImage> ts = new ArrayList<BufferedImage>();
^^^^^^^^^
The type ArrayList is not generic; it cannot be parameterized with arguments <BufferedImage>
classファイル
コンパイル済みのクラスファイルを置いておく。
- JpegThumbnail.class
- JpegThumbnail$Extractor.class
参考資料等
- JpegAnalyzer Plusオンラインヘルプ
-- JPEG,JFIF,JFXX,Exifのフォーマット構造について詳しい資料。すごい。
- MakerNote情報Index
-- メーカー独自のフォーマット(メーカーノート)についての資料。いろんなメーカーノートが載っている。すごい。
- drewnoakes.com - jpeg exif / iptc metadata extraction in java
-- 「metadata extractor」というJpeg, Exif, Iptcを操作するJavaのライブラリ。
- Exif Viewer :: Add-ons for Firefox
-- Firefox 上で画像の Exif 情報を見ることができるアドオン。
# こういうツールなので本当はRubyとかPerlで作ろうと思ったんだけど、いいライブラリが見つからなかった。。。
tags: zlashdot Java Exif Java Jpeg
Posted by NI-Lab. (@nilab)