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)