内容简介:从网页调起手机拍照时,很多相机程序会自动根据你拍照的方向旋转以调整照片显示,但是上传的照片却是原始的方向。于是常常造成拍好的照片在网页上面上下左右颠倒。对此的解决办法就是,读取照片 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, 也可微信搜索小程序「美菜产品技术团队」,干货满满且每周更新,想学习技术的你不要错过哦。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 调整PG分多次调整和一次到位的迁移差别分析
- skynet 模块命名空间调整
- 再谈JVM内存参数调整(200331)
- Martian 4.0,架构大幅调整
- Kubeadm1.14 证书调整
- Bootstrap开发框架界面的调整处理
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。