「ArrayBuffer」应用-以自动调整照片方向为例

做者: Cheironjavascript

背景

从网页调起手机拍照时,不少相机程序会自动根据你拍照的方向旋转以调整照片显示,可是上传的照片倒是原始的方向。因而经常形成拍好的照片在网页上面上下左右颠倒。html

对此的解决办法就是,读取照片 EXIF 信息中的 Orientation 字段,以主动旋转照片。本文将详细解读如何使用javascript读取EXIF的信息。java

ArrayBuffer, TypedArray 和 DataView

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,以及下面的例子。

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)
}
复制代码

参考:

  1. Description of Exif file format
  2. ArrayBuffer

原文连接: tech.meicai.cn/detail/59, 也可微信搜索小程序「美菜产品技术团队」,干货满满且每周更新,想学习技术的你不要错过哦。

相关文章
相关标签/搜索