ShareGrid

使用 Swift 脚本获取照片 EXIF 信息

EXIF 是什么

EXIF,全称是 Exchangeable Image File Format(可交换图像文件格式),是一种用于记录照片附加信息的标准,主要应用于数码相机和手机拍摄的图像文件中(如 JPEG、HEIF)。

简单来说,EXIF 是照片的“元数据”,记录了拍摄时的各种参数。

常见的 EXIF 信息

类别示例
拍摄设备相机品牌(Canon、Nikon)、型号
拍摄参数快门速度、光圈、ISO、焦距
时间信息拍摄时间(精确到秒)
地理位置GPS 坐标(如果设备开启了定位)
图像信息分辨率、方向(旋转角度)
软件信息后期编辑使用的软件名和版本号

EXIF 的用途

  • 摄影分析:摄影师可以分析照片参数,优化拍摄技巧。
  • 自动旋转:系统或网站可以根据 EXIF 中的方向信息,正确显示照片。
  • 地图标记:如果有 GPS 信息,照片可以在地图上定位。
  • 版权溯源:部分图片中可能嵌入作者信息。

我为什么需要 EXIF

作为一个“业余摄影师”,我希望我的照片可以展示一些拍摄参数信息,对于想要了解它们的人也会更加便捷。

比如

SONY ILCE-7CM2TAMRON E 28-200mm F2.8-5.6 Di III A071ISO 25099mmƒ6.31/3200 s

Apple iPhone 15 Pro MaxTelephoto Camera — 120 mm ƒ2.8ISO 50120mmƒ2.81/1271 sHDR

如何读取 EXIF

作为一个前端,依稀记得有读取 exif 的 npm 包:exifreader 还有 exif-js。虽然 JavaScript 写习惯了,但是 JavaScript 受限于运行环境,能力有限,比如 exifreader 支持读取的文件类型如下:

File typeExifIPTCXMPICCMPFPhotoshopMakerNoteThumbnailImage details
JPEGyesyesyesyesyessome*some**yesyes
TIFFyesyesyesyes???some*some**N/AN/A
PNGyesyesyesyes??????some**noyes
HEIC/HEIFyesnoyesyes??????some**yesno
AVIFyesnoyesyes??????some**yesno
WebPyesnoyesyes??????some**yesyes
GIFN/AN/AN/AN/AN/AN/AN/AN/Ayes

摄影师怎么可能只有这几种照片文件类型呢?自然而然地转向 Swift 可以解决这些烦恼,毕竟 MacOS 是原生支持最多媒体文件类型的 OS,没有之一。

readExif.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#!/usr/bin/env swift

import AppKit
import CoreGraphics
import CoreServices
import Foundation
import ImageIO

func checkImageIsHDR(for fileURL: URL) -> Bool {
guard let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil) else {
print("无法读取图片")
return false
}

guard
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil)
as? [CFString: Any]
else {
print("无法获取图片属性")
return false
}

if let profileName = imageProperties[kCGImagePropertyProfileName] as? String {
print("ICC Profile 名称: \(profileName)")
if profileName.contains("PQ") || profileName.contains("BT.2100") {
print("色彩空间是 PQ")
return true
}
}

// 查看是否有 GainMap
if let auxData = CGImageSourceCopyAuxiliaryDataInfoAtIndex(
imageSource, 0, kCGImageAuxiliaryDataTypeHDRGainMap)
{
print("检测到 HDR Gain Map 数据: \(auxData)")
return true
}
return false
}

func readExifDescription(from fileURL: URL) -> String? {
guard let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil) else {
print("无法读取文件: \(fileURL.path)")
return nil
}

guard
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any]
else {
print("无法提取元数据")
return nil
}

let tiff = properties[kCGImagePropertyTIFFDictionary] as? [CFString: Any]
let exif = properties[kCGImagePropertyExifDictionary] as? [CFString: Any]
// Get the Apple-specific metadata dictionary

// 使用官方支持的字段
let originalLens = exif?[kCGImagePropertyExifLensModel] as? String

let make = (tiff?[kCGImagePropertyTIFFMake] as? String ?? "").trimmingCharacters(
in: .whitespaces)
let model = (tiff?[kCGImagePropertyTIFFModel] as? String ?? "").trimmingCharacters(
in: .whitespaces)

let iso = exif?[kCGImagePropertyExifISOSpeedRatings] as? [Int] ?? []
let focalLength = exif?[kCGImagePropertyExifFocalLength] as? Double
let aperture = exif?[kCGImagePropertyExifFNumber] as? Double
let shutterSpeedValue = exif?[kCGImagePropertyExifExposureTime] as? Double
let exposureBias = exif?[kCGImagePropertyExifExposureBiasValue] as? Double

// Define lens replacement map
let lensReplacementMap: [String: String] = [
"iPhone 15 Pro Max back triple camera 6.765mm f/1.78": "Main Camera — 24 mm ƒ1.78",
// Add more replacements here:
// "Original Lens Model String": "Desired Replacement String",
"iPhone 15 Pro Max back triple camera 9.03mm f/2.8": "Telephoto Camera — 77 mm ƒ2.8",
"iPhone 15 Pro Max back triple camera 2.22mm f/2.2": "Ultra Wide Camera — 13 mm ƒ2.2",
]

// Apply replacement if found, otherwise use the original lens string
let lens = originalLens.flatMap { lensReplacementMap[$0] } ?? originalLens

// Check for HDR
let isHDR: Bool = checkImageIsHDR(for: fileURL)

// 构造输出格式
var parts: [String] = []
if !make.isEmpty || !model.isEmpty {
parts.append("\(make) \(model)")
}
if let isoValue = iso.first {
parts.append("ISO \(isoValue)")
}
if let fl = focalLength {
parts.append("\(Int(round(fl)))mm")
}
if let ap = aperture {
parts.append(\(String(format: "%.1f", ap))")
}
if let ss = shutterSpeedValue {
if ss >= 1.0 {
parts.append("\(Int(ss)) s")
} else {
let denominator = Int(round(1.0 / ss))
parts.append("1/\(denominator) s")
}
}
if let ev = exposureBias {
if abs(ev) < 0.001 {
parts.append("")
} else {
parts.append("\(String(format: "%+.1f", ev))ev")
}
}
if let lens = lens {
parts.append("\(lens)")
}
// Append HDR tag if the image is HDR
if isHDR {
parts.append("HDR")
}

return parts.map { "{\($0)}" }.joined()
}

// 主程序入口
let args = CommandLine.arguments
guard args.count >= 2 else {
print("用法: readExif.swift <图片路径>")
exit(1)
}

let filePath = args[1]
let fileURL = URL(fileURLWithPath: filePath)

if let description = readExifDescription(from: fileURL) {
print(description)
// copy to clipboard
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(description, forType: .string)
} else {
print("读取失败")
}

如何使用:

  1. 为脚本添加可执行权限

    1
    chmod +x readExif.swift
  2. 运行

    1
    readExif.swift ~/Downloads/testImage.jpg

示例输出: {Apple iPhone 15 Pro Max}{ISO 100}{7mm}{ƒ1.8}{1/22222 s}{}{Main Camera — 24 mm ƒ1.78}

为什么是这个顺序这个格式?因为在本博客主题中我是这样设计展示顺序的😂镜头参数在最后面是因为后来才想加进来展示

镜头名称转换

EXIF 读取出来的镜头焦距是转换成 35mm 画幅的真实焦距,看起来很不直观,所以要做一下映射,使它返回的结果和 Photos 内展示的一样。

iPhone 15 Pro Max 的三颗镜头参数分别对应为:

1
2
3
4
5
{
"iPhone 15 Pro Max back triple camera 6.765mm f/1.78": "Main Camera — 24 mm ƒ1.78",
"iPhone 15 Pro Max back triple camera 9.03mm f/2.8": "Telephoto Camera — 77 mm ƒ2.8",
"iPhone 15 Pro Max back triple camera 2.22mm f/2.2": "Ultra Wide Camera — 13 mm ƒ2.2"
}