我们在网上看到一些照片,例如小米 14 那种拍出来的照片,带有边框信息的,看着来很酷,那么我们是否也可以自己实现一个呢,接下来我们将借助 ExifTool 基于 NodeJs 来实现这样的工具来实现这样的功能。
ExifTool
ExifTool 是一个由 Phil Harvey 开发的、用于读取、编写和编辑文件元数据的命令行工具。它广泛支持各种文件格式,尤其是图像、音频和视频文件的元数据处理。ExifTool 是摄影、数字资产管理、数据取证等领域的事实标准工具,被广泛用于处理和分析 EXIF(可交换图像文件格式)数据。
ExifTool 基础知识
EXIF(Exchangeable Image File Format)数据是嵌入在图片和音频文件中的元数据,用于存储拍摄设备的信息和设置。例如:
-
拍摄设备信息:相机品牌、型号、序列号等。
-
拍摄参数:光圈、快门速度、ISO 感光度、焦距等。
-
拍摄时间:记录照片的拍摄日期和时间。
-
地理位置信息:通过 GPS 捕获的经纬度信息。
-
图像处理信息:白平衡、对比度、色彩空间等。
它主要能够执行以下操作:
-
读取元数据:从文件中提取和显示详细的元数据信息。
-
写入元数据:向文件中添加或修改元数据。
-
删除元数据:删除文件中的特定元数据或所有元数据。
-
转换格式:将元数据从一种格式转换为另一种格式,或在不同类型的文件间复制元数据。
-
批量处理:对大量文件进行批量元数据操作。
ExifTool 支持非常广泛的文件格式,涵盖了图片、音频、视频、文档等多种类型。例如:
-
图片格式:JPEG, TIFF, PNG, RAW 文件(如 CR2, NEF, DNG 等)。
-
音频格式:MP3, FLAC, WAV。
-
视频格式:MP4, MOV, AVI。
-
其他:PDF, DOCX, ZIP, ISO, 和许多其他文件类型。
要想使用,我们首先需要在操作系统上安装,如果你使用的是 mac,那么我们可以使用 Homebrew 来进行安装:
brew install exiftool
验证它是否安装成功,那么我们只需要执行以下命令即可:
exiftool -ver
最终输出结果如下图所示:
ExifTool 的基本用法
我们可以使用 ExifTool 来读取图片中的元数据:
exiftool DSC_0142_1.NEF
输出的内容可太多了,它的格式是键值对的形式,除了这些基础的之外,我们还可以修改或者添加元数据,例如修改作者信息:
exiftool -Artist="Moment" DSC_0142_1.NEF
要想删除图片中所有的元数据,那么我们可以执行:
exiftool -all= DSC_0142_1.NEF
当然我们也可以选择性地删除某些特定类型的元数据,例如 GPS 信息:
exiftool -gps:all= DSC_0142_1.NEF
除了前面这些能一下子输出所有信息之外,我们还可以定制输出的格式,比如只显示特定类型的元数据:
exiftool -make -model -exposuretime DSC_0142_1.NEF
这会输出相机品牌、型号和曝光时间。
总的来说,ExifTool 通过解析文件头和元数据块来读取和写入元数据。它支持的标签种类非常多,并且可以深入到文件的各个层级提取信息。它能够处理多种格式和厂商特定的扩展。例如,针对不同厂商的 RAW 格式,ExifTool 能够识别和提取特定的元数据。
借助这些东西,我们可以将它应用于以下场景:
-
摄影工作流:在摄影工作流中,用于整理、标记和优化图像文件。
-
数字取证:在调查中,用于分析图像和文件的来源及其修改历史。
-
数字资产管理:在大型媒体库中,用于管理和索引元数据。
-
隐私保护:在发布或共享照片前,用于删除敏感的 EXIF 数据(如 GPS 信息)。
如何在 NodeJs 项目中实现
要想实现在原有照片的基础上,添加一些边框来显示照片的其他信息,我们可以使用 exiftool-vendored 包,它是一个用于 Node.js 环境的库,它封装了 ExifTool 的功能,方便在 Node.js 项目中直接调用和使用 ExifTool。这个库的主要目的是让开发者在 Node.js 应用程序中处理图像的 EXIF 数据时更加简单和方便,而不必手动管理 ExifTool 的安装和配置。
它的基本使用示例如下图所示:
const { exiftool } = require("exiftool-vendored");
const path = require("path");
async function readExifData() {
const imagePath = path.join(__dirname, "example.jpg");
try {
const exifData = await exiftool.read(imagePath);
console.log("EXIF Data: ", exifData);
} catch (error) {
console.error("Error reading EXIF data:", error);
} finally {
await exiftool.end(); // 确保资源释放
}
}
readExifData();
执行文件,它的输出结果最终如下图所示:
接下来我们将实现一个完整的案例,最终如下代码所示:
const { exiftool } = require("exiftool-vendored");
const path = require("path");
const Jimp = require("jimp");
const { exec } = require("child_process");
const nefImagePath = path.join(__dirname, "DSC_0142_1.NEF");
const jpegImagePath = path.join(__dirname, "DSC_0142_1.jpg");
async function convertNefToJpeg() {
return new Promise((resolve, reject) => {
const command = `exiftool -b -JpgFromRaw -w .jpg "${nefImagePath}"`;
console.log("正在执行命令:", command);
exec(command, (error, stdout, stderr) => {
if (error) {
console.log(`将 NEF 转换为 JPEG 时出错: ${stderr}`);
reject(`将 NEF 转换为 JPEG 时出错: ${stderr}`);
} else {
console.log("JPEG 转换成功。");
resolve();
}
});
});
}
async function readExifData() {
try {
// 第一步: 将 NEF 转换为 JPEG
console.log("正在将 NEF 转换为 JPEG...");
await convertNefToJpeg();
// 第二步: 从原始 NEF 文件读取 EXIF 数据
console.log("正在读取 EXIF 数据...");
const exifData = await exiftool.read(nefImagePath);
console.log("成功读取 EXIF 数据。");
// 第三步: 提取所需的 EXIF 信息
const cameraMake = exifData.Make || "未知制造商";
const cameraModel = exifData.Model || "未知型号";
const lensInfo = exifData.LensID || exifData.LensModel || "未知镜头";
const fNumber = exifData.FNumber || "未知光圈值";
const shutterSpeed = exifData.ShutterSpeed || "未知快门速度";
const isoSpeed = exifData.ISO || "未知 ISO 值";
const exposureTime = exifData.ExposureTime || "未知曝光时间";
const infoText = `${cameraMake} ${cameraModel}\n镜头: ${lensInfo}\n光圈: ${fNumber}\n快门: ${shutterSpeed}\nISO: ${isoSpeed}\n曝光: ${exposureTime}`;
// 第四步: 读取转换后的 JPEG 图像,并添加带有 EXIF 信息的边框
console.log("正在读取转换后的 JPEG 图像...");
const image = await Jimp.read(jpegImagePath);
const borderSize = 100;
// 创建一个带有边框的新图像
const borderedImage = new Jimp(
image.bitmap.width + borderSize * 2,
image.bitmap.height + borderSize * 2,
0xffffffff // 边框的白色背景
);
// 将原图像复合到新图像上(带边框)
borderedImage.composite(image, borderSize, borderSize);
// 加载字体并在边框上打印 EXIF 信息
console.log("正在加载字体并将 EXIF 信息添加到边框...");
const font = await Jimp.loadFont(Jimp.FONT_SANS_32_BLACK);
borderedImage.print(
font,
10, // 文字的 x 位置
10, // 文字的 y 位置
{
text: infoText,
alignmentX: Jimp.HORIZONTAL_ALIGN_LEFT,
alignmentY: Jimp.VERTICAL_ALIGN_TOP,
},
image.bitmap.width + borderSize * 2 - 20 // 文字的最大宽度
);
// 第五步: 保存最终的图像
const outputImagePath = path.join(
__dirname,
"bordered_image_with_exif.jpg"
);
console.log("正在保存带边框的图像至:", outputImagePath);
await borderedImage.writeAsync(outputImagePath);
console.log(
"图像已保存,带有边框和 EXIF 信息,保存路径为:",
outputImagePath
);
} catch (error) {
console.log("出错了: " + error.message);
} finally {
await exiftool.end(); // 释放资源
}
}
readExifData();
在上面的代码中,主要的执行流程有以下几个方面:
-
转换图像格式:使用 exiftool 将 NEF 格式的照片转换为 JPEG 格式,因为 Jimp 不支持 NEF 格式。
-
读取 EXIF 数据:从 NEF 文件中提取 EXIF 元数据,如相机品牌、型号、镜头信息等。
-
处理图像:使用 Jimp 创建一个带有白色边框的图像,并在边框上添加从 EXIF 数据中提取的文本信息。
-
保存最终图像:将处理后的图像保存为一个新的 JPEG 文件。
执行命令后我们可以看到控制台下有如下输出:
最终的输出结果如下图所示:
总结
ExifTool 可用于照片整理、按元数据分类管理;添加或修改版权信息;以及删除敏感数据如 GPS 信息以保护隐私。它在数字取证和照片管理中非常实用。
最后分享两个我的两个开源项目,它们分别是:
这两个项目都会一直维护的,如果你想参与或者交流学习,可以加我微信 yunmz777 如果你也喜欢,欢迎 star 🚗🚗🚗