首先介绍一下什么是EXIFEXIF Exchangeable Image File的缩写,这是一种专门为数码相机照片设定的格式。这种格式可以用来记录数字照片的属性信息,例如相机的品牌及型号、相片的拍摄时间、拍摄时所设置的光圈大小、快门速度、ISO等等信息。除此之外它还能够记录拍摄数据,以及照片格式化方式,这样就可以输出到兼容EXIF格式的外设上,例如照片打印机等。

目前最常见的支持EXIFexif信息信息的图片格式是JPG,很多的图像工具都可以直接显示图片的EXIF信息,包括现在的一些著名的相册网站也提供页面用于显示照片的EXIF信息。本文主要介绍Java语言如何读取图像的EXIF信息,包括如何根据EXIF信息对图像进行调整以适合用户浏览。

目前最简单易用的EXIF信息处理的Java包是Drew Noakes写的metadata-extractor,该项目最新的版本是2.3.0,支持EXIF 2.2版本。你可以直接从www.drewnoakes/code/exif/ 下载该项目的最新版本包括其源码。

需要注意的是,并不是每个JPG图像文件都包含有EXIF信息,你可以在Windows资源管理器单击选中图片后,如果该图片包含EXIF信息,则在窗口状态栏会显示出相机的型号,如下图所示:

拍摄设备的型号便是EXIF信息中的其中一个。下面我们给出一段代码将这个图片的所有的EXIF信息全部打印出来。
package com.st;
import java.io.File;
import java.util.Iterator;
import com.drew.imaging.jpeg.JpegMetadataReader;
import adata.Directory;
import adata.Metadata;
import adata.Tag;
import if.ExifDirectory;
/**
* 测试用于读取图片的EXIF信息
* @author Winter Lau
*/
public class ExifTester {
    public static void main(String[] args) throws Exception {
        File jpegFile = new File("D:\\我的文档\\我的相册\\DSCF1749.JPG");
        Metadata metadata = adMetadata(jpegFile);
        Directory exif = Directory(ExifDirectory.class);
        Iterator tags = TagIterator();
        while (tags.hasNext()) {
            Tag tag = (();
            System.out.println(tag);
        }
    }
}


metadata-extractor-2.3.0.jar文件加入到类路径中编译并执行上面这段代码后可得到下面
的运行结果:

[Exif] Make - FUJIFILM
[Exif] Model - FinePix A205S
[Exif] Orientation - Top, left side (Horizontal / normal)
[Exif] X Resolution - 72 dots per inch
[Exif] Y Resolution - 72 dots per inch
[Exif] Resolution Unit - Inch
[Exif] Software - Digital Camera FinePix A205S  Ver1.00
[Exif] Date/Time - 2005:05:13 22:18:49
[Exif] YCbCr Positioning - Datum point
[Exif] Copyright -   
[Exif] Exposure Time - 1/60 sec
[Exif] F-Number - F3
[Exif] Exposure Program - Program normal
[Exif] ISO Speed Ratings - 320
[Exif] Exif Version - 2.20
[Exif] Date/Time Original - 2005:05:13 22:18:49
[Exif] Date/Time Digitized - 2005:05:13 22:18:49
[Exif] Components Configuration - YCbCr
[Exif] Compressed Bits Per Pixel - 3 bits/pixel
[Exif] Shutter Speed Value - 1/63 sec
[Exif] Aperture Value - F3
[Exif] Brightness Value - -61/100
[Exif] Exposure Bias Value - 0 EV
[Exif] Max Aperture Value - F3
[Exif] Metering Mode - Multi-segment
[Exif] Light Source - Unknown
[Exif] Flash - Flash fired, auto
[Exif] Focal Length - 5.5 mm
[Exif] FlashPix Version - 1.00
[Exif] Color Space - sRGB
[Exif] Exif Image Width - 1280 pixels
[Exif] Exif Image Height - 960 pixels
[Exif] Focal Plane X Resolution - 1/2415 cm
[Exif] Focal Plane Y Resolution - 1/2415 cm
[Exif] Focal Plane Resolution Unit - cm
[Exif] Sensing Method - One-chip color area sensor
[Exif] File Source - Digital Still Camera (DSC)
[Exif] Scene Type - Directly photographed image
[Exif] Custom Rendered - Normal process
[Exif] Exposure Mode - Auto exposure
[Exif] White Balance - Auto white balance
[Exif] Scene Capture Type - Standard
[Exif] Sharpness - None
[Exif] Subject Distance Range - Unknown
[Exif] Compression - JPEG (old-style)
[Exif] Thumbnail Offset - 1252 bytes
[Exif] Thumbnail Length - 7647 bytes
[Exif] Thumbnail Data - [7647 bytes of thumbnail data]
从这个执行的结果我们可以看出该照片是在20050513 221849秒拍摄的,拍摄用的相机型号是富士的FinePix A205S,曝光时间是1/60秒,光圈值F3,焦距5.5毫米,ISO值为320等等。

你也可以直接指定读取其中任意参数的值,ExifDirectory类中定义了很多以TAG_开头的整数常量,这些常量代表特定的一个参数值,例如我们要读取相机的型号,我们可以用下面代码来获取。

Metadata metadata = adMetadata(jpegFile);
Directory exif = Directory(ExifDirectory.class);
String model = String(ExifDirectory.TAG_MODEL);

上述提到的是如何获取照片的EXIF信息,其中包含一个很重要的信息就是——拍摄方向。例如上面例子所用的图片的拍摄方向是:Orientation - Top, left side (Horizontal / normal)。我们在拍照的时候经常会根据场景的不同来选择相机的方向,例如拍摄一颗高树,我们会把相机竖着拍摄,使景物刚好适合整个取景框,但是这样得到的图片如果用普通的图片浏览器看便是倒着的,需要调整角度才能得到一个正常的图像,有如下面一张照片。

这张图片正常的情况下需要向左调整90度,也就是顺时针旋转270度才适合观看。通过读取该图片的EXIF信息,我们得到关于拍摄方向的这样一个结果:[Exif] Orientation - Left side, bottom (Rotate 270 CW)。而直接读取ExitDirectory.TAG_ORIENTATION标签的值是8。我们再来看这个项目是如何来定义这些返回值的,打开源码包中的ExifDescriptor类的getOrientationDescription方法,该方法代码如下:
public String getOrientationDescription() throws MetadataException
{
        if (!_ainsTag(ExifDirectory.TAG_ORIENTATION)) return null;
        int orientation = _Int(ExifDirectory.TAG_ORIENTATION);
        switch (orientation) {
            case 1: return "Top, left side (Horizontal / normal)";
            case 2: return "Top, right side (Mirror horizontal)";
            case 3: return "Bottom, right side (Rotate 180)";
            case 4: return "Bottom, left side (Mirror vertical)";
            case 5: return "Left side, top (Mirror horizontal and rotate 270 CW)";
            case 6: return "Right side, top (Rotate 90 CW)";
            case 7: return "Right side, bottom (Mirror horizontal and rotate 90 CW)";
            case 8: return "Left side, bottom (Rotate 270 CW)";
            default:
                return String.valueOf(orientation);
        }
}

从这个方法我们可以清楚看到各个返回值的意思,如此我们便可以根据实际的返回值来对图像进行旋转或者是镜像处理了。在这个例子中我们需要将图片顺时针旋转270度,或者是逆时针旋转90度方可得到正常的图片。

虽然图片的旋转不在本文范畴内,但为了善始善终,下面给出代码用以旋转图片,其他的关于图片的镜像等处理读者可以依此类推。
String path = "D:\\TEST.JPG";
File img = new File(path);
BufferedImage old_img = (ad(img); 
int w = Width();
int h = Height();
BufferedImage new_img = new BufferedImage(h,w,BufferedImage.TYPE_INT_BGR);     
Graphics2D g2d =ateGraphics();
     
AffineTransform origXform = Transform();
AffineTransform newXform = (AffineTransform)(origXform.clone());
// center of rotation is center of the panel
double xRot = w/2.0;
Radians(270.0), xRot, xRot); //旋转270
g2d.setTransform(newXform);
// draw image centered in panel
g2d.drawImage(old_img, 0, 0, null);
// Reset to Original
g2d.setTransform(origXform);
//写到新的文件
FileOutputStream out = new FileOutputStream("D:\\test2.jpg");
try{
ImageIO.write(new_img, "JPG", out);
}finally{
    out.close();
}

旋转后的照片如下:

但是利用上面的代码旋转照片后,原有照片包含的EXIF信息不复存在了。至于照片的镜面翻转可以直接利用Graphic2DdrawImage方法来实现,方法原形如下:
drawImage

public abstract boolean drawImage(Image img,
                  int dx1,
                                  int dy1,
                                  int dx2,
                                  int dy2,
                                  int sx1,
                                  int sy1,
                                  int sx2,
                                  int sy2,
                                  ImageObserver observer)
该方法的使用请参考JDKAPI文档。关于照片旋转后丢失EXIF信息的问题,需要在照片旋转之前先把EXIF信息读出,然后再在旋转后写入新的照片中,你可以使用MediaUtil包来写EXIF信息到图片文件中,关于这个包的使用请参考该项目所给出的例子,本文不再叙述。