作者: Cheiron
背景
从网页调起手机拍照时,很多相机程序会自动根据你拍照的方向旋转以调整照片显示,但是上传的照片却是原始的方向。于是常常造成拍好的照片在网页上面上下左右颠倒。
对此的解决办法就是,读取照片 EXIF 信息中的 Orientation 字段,以主动旋转照片。本文将详细解读如何使用javascript读取EXIF的信息。
ArrayBuffer, TypedArray 和 DataView
ArrayBuffer, TypedArray 和 DataView 共同为 javascript 操作二进制数据提供了便利的途径。
ArrayBuffer 是一块内存,或者说代表了一段存储着二进制数据的内容。他不能直接被读写,只能通过 TypedArray 或者 DataView 来读写。ArrayBuffer 是一个构造函数,接受一个整数作为参数,即表示分配多少字节的内存。如 const ab = new ArrayBuffer(32)
就分配了一段 16字节的连续内存区域,每个字节的默认值是0. 同时,一些 javascript API 的返回结果也是 ArrayBuffer, 比如本文将谈到的 FileReader API, 它的 readAsArrayBuffer 方法就会返回一个 ArrayBuffer 对象。
TypedArray 是一类构造函数的总称,包括 Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array 共 9 种。用这九个构造函数生成的 typed array,和数组具有类似的行为。如都有 length 属性,都可以通过 [] 访问元素,也可以使用数组大部分的方法。
比如上文创建的 ab 对象。可以用
const i8view = new Int8Array(ab)
创建一个8位有符号整数的视图。因为 ab 有 32 个字节,int8 占一个字节,所以 i8view 的每一项相当于 ab 的一个字节,因此i8view.length = 32
,每一项都是 0.
我们也可以用 const ui32view = new Uint32Array(ab)
创建一个32位无符号整数的视图。因为 ab 有 32 个字节,uint32 占四个字节,所以 ui32view 的每一项相当于 ab 的四个字节,因此 ui32view.length = 8
, 因为 ab 的每个字节都是0, 4个字节一起作为 Uint32 计算还是0, 所以,ui32view 的每一项仍然都是 0.
可以看到,在这个过程中,ab 本身没有变化,创建不同视图的过程,只是把 ab 的数据作为 int8, Uint32 或其他格式的数据来处理而已。
Typed array 和 array 的区别在于 typed array 的所有成员都是同一类型(也就是 “typed” 的含义),且完全连续没有空位。如果传入数组长度来初始化,那么所以元素默认值都是 0. TypedArray 只是一种视图,本身不存储数据,数据存在 ArrayBuffer 中。TypedArray 适用于处理简单类型的二进制数据,复杂的就需要 DataView.
DataView 可以定义一个复合视图。比如 Uint8Array 定义的视图,所以元素都是 无符号8位整数,而 DataView 定义的视图,可以第一个字节是 Uint8, 第二个字节是 Int16 等,且可以自定义字节序。具体用法可以参考MDN,以及下面的例子。
JEPG 及 EXIF 的格式
JPEG 文件大体分为两个部分:标记码和压缩数据。
标记码由两个字节组成,前一个是固定值 0xFF,后一个是不同意义对应的数值。如 0xFFD8 表示 SOI (Start of Image),0xFFD9 表示 EOI,即 End of Image. 我们关注的 EXIF 信息与 0xFFE0 0xFFEF 范围的标记有关。这些区域叫做 应用程序保留区N(ApplicationN),如 0xFFE0 是 App0. 我们需要的 EXIF 由 App1 标记,即是位于 0xFFE1 到 下一个 0xFFE1 到 下一个 0xFF 标记之间的数据。
EXIF 的格式
可以看到紧邻 FFE1 标识的后两位,是 APP1 的数据大小,位于 TIFF header 之后的是 IFD0 即 Image File Directory. 它包含了图片信息数据。下面的表格描述了 IFD 的数据格式。
IFD 的格式
TTTT 的 2bytes 数据表示 Tag,ffff 这 2bytes 表示数据的类型。NNNNNNNN 这 4bytes 是组成元素的数量。DDDDDDDD 这 4bytes 是数据本身或数据的偏移量。
在本例中,图像方向 Orientation 的 Tag Number 是 0x0112;数据类型是 unsigned short, 对应的 ffff 是 0x0003, 组成元素只有一个,所以 NNNNNNNN 是 00000001. DDDDDDDD比较麻烦,有两种情况。如果 数据类型 * 组成元素数量 < 4bytes, 那么,DDDDDDDD 就是改标签的值,反之则是数据存储地址的偏移量。Unsigned short 类型的一个组成元素占 2bytes, 只有一个,所以 2bytes * 1 < 4bytes, 因此对于 Orientation 标签来说,DDDDDDDD 就是该标签的值。(有关细节请参考参考文档中的 1)
Orientation 的取值和含义。
一般手机转一圈拍出来的是 1 6 3 8 四个值。
图片处理
先使用 FileReader API 把 input 标签输入的图片读取成 ArrayBuffer
const reader = new FileReader()
reader.onload = async function () {
const buffer = reader.result
const orientation = getOrientation(buffer)
const image = await rotateImage(buffer, orientation)
}
reader.readAsArrayBuffer(file)
再看 getOrientation 函数的实现。
function getOrientation(buffer) {
// 建立一个 DataView
const dv = new DataView(buffer)
// 设置一个位置指针
let idx = 0
// 设置一个默认结果
let value = 1
// 检测是否是 JPEG
if (buffer.length < 2 || dv.getUint16(idx) !== 0xFFD8 {
return false
}
idx += 2
let maxBytes = dv.byteLength
// 遍历文件内容,找到 APP1, 即 EXIF 所在的标识
while (idx < maxBytes - 2) {
const uint16 = dv.getUint16(idx)
idx += 2
switch (uint16) {
case 0xFFE1:
// 找到 EXIF 后,在 EXIF 数据内遍历,寻找 Orientation 标识
const exifLength = dv.getUint16(idx)
maxBytes = exifLength - 2
idx += 2
break
case 0x0112:
// 找到 Orientation 标识后,读取 DDDDDDDD 部分的内容,并把 maxBytes 设为 0, 结束循环。
value = dv.getUint16(idx + 6, false)
maxBytes = 0
break
}
}
return value
}
在来看 rotateImage 的实现:
function rotateImage (buffer, orientation) {
// 利用 canvas 来旋转
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// 利用 image 对象来把图片画到 canvas 上
const image = new Image()
// 根据 arrayBuffer 生成图片的 base64 url
const url = arrayBufferToBase64Url(buffer)
return new Promise((resolve, reject) => {
image.onload = function () {
const w = image.naturalWidth
const h = image.naturalHeight
switch (orientation) {
case 8:
canvas.width = h
canvas.height = w
ctx.translate(h / 2, w / 2)
ctx.rotate(270 * Math.PI / 180)
ctx.drawImage(image, -w / 2, -h / 2)
break
case 3:
canvas.width = w
canvas.height = h
ctx.translate(w / 2, h / 2)
ctx.rotate(180 * Math.PI / 180)
ctx.drawImage(image, -w / 2, -h / 2)
break
case 6:
canvas.width = h
canvas.height = w
ctx.translate(h / 2, w / 2)
ctx.rotate(90 * Math.PI / 180)
ctx.drawImage(image, -w / 2, -h / 2)
break
default:
canvas.width = w
canvas.height = h
ctx.drawImage(image, 0, 0)
break
}
// 也可以使用其他 API 导出 canvas
const data = canvas.toDataURL('image/jpeg', 1)
resolve(data)
}
image.src = url
})
}
arrayBufferToBase64Url 的实现:
function arrayBufferToBase64 (buffer) {
let binary = ''
// 这里用到了 TypedArray
const bytes = new Uint8Array(buffer)
const len = bytes.byteLength
for (let i = 0; i < len; i++) {
// fromCharCode 方法从指定的 Unicode 值序列创建字符串
binary += String.fromCharCode(bytes[ i ])
}
// 使用 btoa 方法从 String 对象创建 base-64 编码的 ASCII 字符串
return window.btoa(binary)
}
参考:
原文链接: tech.meicai.cn/detail/59, 也可微信搜索小程序「美菜产品技术团队」,干货满满且每周更新,想学习技术的你不要错过哦。