EXIF (Exchangeable Image File Format) 是數碼相機照片專用的元數據標準,記錄拍攝參數(光圈、快門、ISO、焦距)、設備信息(品牌、型號)、時間、GPS 坐標、縮略圖等。主流支持格式:JPEG、TIFF、HEIC、RAW 等。
| 庫 | Maven 坐標 | 特點 | 推薦指數 |
|---|---|---|---|
| metadata-extractor | com.drewnoakes:metadata-extractor:2.18.0 | 功能全、無依賴、活躍維護、支持最多格式 | ⭐⭐⭐⭐⭐ |
| Apache Sanselan | org.apache.commons:commons-imaging | Apache 項目、純 Java、功能較全 | ⭐⭐⭐⭐ |
| TwelveMonkeys ImageIO | com.twelvemonkeys.imageio:imageio-jpeg | ImageIO 插件、支持更多格式讀寫 | ⭐⭐⭐ |
| JDK ImageIO | 內置 | 無需依賴、功能極少、不推薦生產用 | ⭐ |
<dependency>
<groupId>com.drewnoakes</groupId>
<artifactId>metadata-extractor</artifactId>
<version>2.18.0</version>
</dependency>
import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.*;
import java.io.File;
import java.io.IOException;
public class ExifReader {
public static void main(String[] args) throws IOException, ImageProcessingException {
File jpegFile = new File("photo.jpg");
Metadata metadata = ImageMetadataReader.readMetadata(jpegFile);
for (Directory directory : metadata.getDirectories()) {
for (Tag tag : directory.getTags()) {
System.out.printf("[%s] %s - %s%n",
directory.getName(), tag.getTagName(), tag.getDescription());
}
// 錯誤信息
for (String error : directory.getErrors()) {
System.err.printf("ERROR: %s - %s%n", directory.getName(), error);
}
}
}
}
public static ExifInfo extractExif(File file) throws IOException, ImageProcessingException {
Metadata metadata = ImageMetadataReader.readMetadata(file);
ExifInfo info = new ExifInfo();
// Exif 子目錄
ExifSubIFDDirectory exif = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
if (exif != null) {
info.setCameraMake(exif.getString(ExifDirectoryBase.TAG_MAKE));
info.setCameraModel(exif.getString(ExifDirectoryBase.TAG_MODEL));
info.setDateTime(exif.getDate(ExifDirectoryBase.TAG_DATETIME_ORIGINAL));
info.setExposureTime(exif.getString(ExifDirectoryBase.TAG_EXPOSURE_TIME));
info.setFNumber(exif.getString(ExifDirectoryBase.TAG_FNUMBER));
info.setIso(exif.getInteger(ExifDirectoryBase.TAG_ISO_EQUIVALENT));
info.setFocalLength(exif.getString(ExifDirectoryBase.TAG_FOCAL_LENGTH));
info.setFlash(exif.getInteger(ExifDirectoryBase.TAG_FLASH) != 0);
}
// GPS 目錄
GpsDirectory gps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
if (gps != null) {
info.setLatitude(gps.getGeoLocation().getLatitude());
info.setLongitude(gps.getGeoLocation().getLongitude());
}
// 縮略圖
JpegDirectory jpeg = metadata.getFirstDirectoryOfType(JpegDirectory.class);
if (jpeg != null && jpeg.hasThumbnail()) {
info.setThumbnail(jpeg.getThumbnail());
}
return info;
}
import java.awt.image.BufferedImage;
import java.io.File;
import javax.imageio.ImageIO;
import com.drew.metadata.exif.ExifDirectoryBase;
public static BufferedImage autoRotate(File imageFile) throws IOException {
BufferedImage image = ImageIO.read(imageFile);
Metadata metadata = ImageMetadataReader.readMetadata(imageFile);
ExifDirectoryBase exif = metadata.getFirstDirectoryOfType(ExifDirectoryBase.class);
if (exif == null) return image;
int orientation = exif.getInt(ExifDirectoryBase.TAG_ORIENTATION);
return switch (orientation) {
case 1 -> image; // 正常
case 2 -> flipHorizontal(image);
case 3 -> rotate180(image);
case 4 -> flipVertical(image);
case 5 -> rotate90(flipHorizontal(image));
case 6 -> rotate90(image); // 順時針 90°
case 7 -> rotate90(flipVertical(image));
case 8 -> rotate270(image); // 逆時針 90°
default -> image;
};
}
private static BufferedImage rotate90(BufferedImage img) {
BufferedImage rotated = new BufferedImage(img.getHeight(), img.getWidth(), img.getType());
Graphics2D g2d = rotated.createGraphics();
g2d.rotate(Math.PI / 2, img.getHeight() / 2.0, img.getWidth() / 2.0);
g2d.drawImage(img, 0, 0, null);
g2d.dispose();
return rotated;
}
// flipHorizontal, flipVertical, rotate180, rotate270 類似實現...
| 問題 | 原因 | 解決 |
|---|---|---|
| 讀取不到 EXIF | 圖片無 EXIF / 被截圖/編輯工具剝離 | 檢查原圖、用 Windows 屬性查看確認 |
| 中文標籤亂碼 | 字符編碼問題 (Windows XP 標題/作者等) | metadata-extractor 2.16+ 已修復,升級版本 |
| 大文件 OOM | ImageIO.read 載入完整像素 | 僅讀取元數據不解碼像素,用 ImageMetadataReader |
| HEIC/RAW 不支持 | 庫版本過舊 | 升級 metadata-extractor 2.18+,或用 TwelveMonkeys |
| Android 環境 | 無標準文件路徑 | 用 ContentResolver.openInputStream(uri) 獲取流 |
ImageMetadataReader.readMetadata(InputStream) 避免臨時文件new DirectoryFilter() 過濾使用 metadata-extractor 是 Java 讀取 EXIF 的最佳選擇:API 清晰、格式支持全、無傳遞依賴、活躍維護。關鍵點:正確處理方向標籤自動旋轉、GPS 坐標解析、縮略圖提取、大文件內存控制。生產環境建議封裝為統一的 ExifService,統一異常處理、日誌、指標監控。