最近发现手机上拍的照片,通过pdf-lib.js 库绘制PDF时,图片逆时针旋转了90 度。
经过排查,实际上这个图片是按旋转的数据点阵记录每一像素的,我们在大多图片查看器中看到他是正常的,是因为有一个Orientation(方向)元数据信息,这些图像查看器会根据这个Orientation 的值来决定是否要旋转及怎么旋转展示,但有一些程序并没有这么做。
我的这张图片,他的元数据 Orientation
是 6,用一般的图像查看器都是正常显示,但用 pdf-lib.js
JavaScript 库绘制 PDF 是逆时针旋转了 90 度。
Exif Orientation
是一种用于指示图像方向的元数据字段,最初由日本电子工业发展协会(JEIDA)在1998年引入到 Exif 规范中。它被嵌入在 JPEG、TIFF、RAW 等图像格式中,用于存储与图像拍摄相关的信息,例如相机型号、拍摄日期和时间、曝光时间等。其中,Exif Orientation 用于指示图像的方向,以便在图像浏览器和编辑器中正确显示图像。
Orientation 有 8 种值,如下图所示 8 种值以及它所代表的方向。
智能手机通常使用内置的陀螺仪和加速度计等传感器来检测其方向。这些传感器可以检测设备的重力方向、倾斜角度和方向,并将这些数据传递给设备的操作系统。根据这些数据,操作系统可以确定设备的方向,并在需要时将图像的 Exif Orientation 标记设置为相应的值。例如,当您拍摄一张横向的照片时,设备会检测到其方向为横向,并将 Exif Orientation 标记设置为6。这样,当您在图库中查看照片时,设备就会自动将其旋转为正确的方向。
在拍照时,无论您是横向还是竖向拿设备,图像感光器(Image sensor)总是按照固定的方向来捕捉数据,最终的照片数据通常都是按照固定的方向保存,而不是根据Orientation值转化数据保存相应的旋转后图像。这是为了节省计算资源和处理时间。
因此,许多设备采用 Exif Orientation 标记来标记图像的方向信息,并且图像本身并没有被旋转或转换。这可以避免不必要的处理和降低存储成本。此外,通过保存 Exif Orientation 标记,图像的原始方向信息可以被保留下来,这对于后续处理和编辑图像非常有用。因此,采用 Exif Orientation 标记是一种更加普遍和高效的方式来保存图像方向信息。
知道了问题所在,就要提取 Orientation 元数据信息进行适配。
提取EXIF Orientation Tag 数据
0x0112(274) is the tag for orientation
function getOrientation(buffer) {
return new Promise((resolve, reject) => {
const dataView = new DataView(buffer);
let offset = 0;
let marker = dataView.getUint16(offset, false);
if (marker === 0xffd8) {
offset += 2;
while (offset < dataView.byteLength) {
marker = dataView.getUint16(offset, false);
offset += 2;
if (marker === 0xffe1) {
offset += 2;
if (dataView.getUint32(offset, false) !== 0x45786966) { // Exif
reject(new Error('Invalid Exif header'));
return;
}
const littleEndian = dataView.getUint16(offset += 6, false) === 0x4949;
offset += dataView.getUint32(offset + 4, littleEndian);
const tags = dataView.getUint16(offset, littleEndian);
offset += 2;
for (let i = 0; i < tags; i++) {
if (dataView.getUint16(offset, littleEndian) === 0x0112) {
// 0x0112(274) is the tag for orientation
resolve(dataView.getUint16(offset + 8, littleEndian));
return;
}
offset += 12;
}
reject(new Error('Orientation tag not found'));
return;
} else if ((marker & 0xff00) !== 0xff00) {
break;
} else {
offset += dataView.getUint16(offset, false);
}
}
reject(new Error('Invalid JPEG format'));
return;
}
reject(new Error('Not a valid JPEG file'));
});
}