内容简介:从网页调起手机拍照时,很多相机程序会自动根据你拍照的方向旋转以调整照片显示,但是上传的照片却是原始的方向。于是常常造成拍好的照片在网页上面上下左右颠倒。对此的解决办法就是,读取照片 EXIF 信息中的 Orientation 字段,以主动旋转照片。本文将详细解读如何使用javascript读取EXIF的信息。ArrayBuffer, TypedArray 和 DataView 共同为 javascript 操作二进制数据提供了便利的途径。
从网页调起手机拍照时,很多相机程序会自动根据你拍照的方向旋转以调整照片显示,但是上传的照片却是原始的方向。于是常常造成拍好的照片在网页上面上下左右颠倒。
对此的解决办法就是,读取照片 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, 也可微信搜索小程序「美菜产品技术团队」,干货满满且每周更新,想学习技术的你不要错过哦。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Mathematica Cookbook
Sal Mangano / O'Reilly Media / 2009 / GBP 51.99
As the leading software application for symbolic mathematics, Mathematica is standard in many environments that rely on math, such as science, engineering, financial analysis, software development, an......一起来看看 《Mathematica Cookbook》 这本书的介绍吧!