做者: Cheironjavascript
从网页调起手机拍照时,不少相机程序会自动根据你拍照的方向旋转以调整照片显示,可是上传的照片倒是原始的方向。因而经常形成拍好的照片在网页上面上下左右颠倒。html
对此的解决办法就是,读取照片 EXIF 信息中的 Orientation 字段,以主动旋转照片。本文将详细解读如何使用javascript读取EXIF的信息。java
ArrayBuffer, TypedArray 和 DataView 共同为 javascript 操做二进制数据提供了便利的途径。es6
ArrayBuffer 是一块内存,或者说表明了一段存储着二进制数据的内容。他不能直接被读写,只能经过 TypedArray 或者 DataView 来读写。ArrayBuffer 是一个构造函数,接受一个整数做为参数,即表示分配多少字节的内存。如 const ab = new ArrayBuffer(32)
就分配了一段 16字节的连续内存区域,每一个字节的默认值是0. 同时,一些 javascript API 的返回结果也是 ArrayBuffer, 好比本文将谈到的 FileReader API, 它的 readAsArrayBuffer 方法就会返回一个 ArrayBuffer 对象。canvas
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 或其余格式的数据来处理而已。async
Typed array 和 array 的区别在于 typed array 的全部成员都是同一类型(也就是 “typed” 的含义),且彻底连续没有空位。若是传入数组长度来初始化,那么因此元素默认值都是 0. TypedArray 只是一种视图,自己不存储数据,数据存在 ArrayBuffer 中。TypedArray 适用于处理简单类型的二进制数据,复杂的就须要 DataView.函数
DataView 能够定义一个复合视图。好比 Uint8Array 定义的视图,因此元素都是 无符号8位整数,而 DataView 定义的视图,能够第一个字节是 Uint8, 第二个字节是 Int16 等,且能够自定义字节序。具体用法能够参考MDN,以及下面的例子。
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, 也可微信搜索小程序「美菜产品技术团队」,干货满满且每周更新,想学习技术的你不要错过哦。