问题在哪里?需要 EXIF.Orientation 出现的场景
前端图片剪裁上传, 利用画布(canvas)将原始图片绘制到 2d content 上后,进行旋转,但旋转成了错误的角度。对这个挺常见的场景步骤的还原:
input[type=“file“].onchange = (e)
获得file
,FileReader
将file
转换成base64
;new image()
,在它的onload()
事件中将获取到的base64
赋值到画布(canvas)上,并且赋予新的宽和高;- 通过
context(‘2d’)
在canvas
上绘制,获得一张同比例不同尺寸的新图; - 最后
canvas.toDataURL()
得到需要的新数据进行上传 base 64 数据。
问题在第 4 步,导致的结果直观的体现为:上传到服务器的图片,或者生成在前台预览的图片的方向错误。(比如,使用 iPhone 拍摄,竖持手机 Home 键在下,得到的图片会逆时针转 90 度),如下图:
为什么会出现图片旋转角度的错误
- 首先来看看在电脑里这张图片是什么样子,通过 Mac 里的 preview.app 打开结果是正确的:
- 接着在浏览器里直接打开这张图是什么样子?结果同样正确的:
- 接着尝试在 html 的
<img>
元素中引用这张图片,结果会怎样?错误出现1。
- 看到这个结果的时候,大概可以意识到:旋转角度错误的问题出现,可能不是上传的时候我们做错了什么导致图片的方向错了,而是我们少做了什么,没有正确将图片旋转成我们需要的方向。
那么怎么样确认如何才是正确的方向?首先在 Mac 上可以通过 preview.app 来获得线索,在 preview 中打开 tools -> show inspector(工具-显示检查器),第二栏的「通用」如下:
在「通用」下有「方向」,对应的值是 6(逆时针旋转 90°);对比图片,猜测这图片是不是被逆时针旋转 90°,所以为了得到正确的显示结果,需要顺时针旋转图片 90°?
Google 了一堆后发现,这里的「方向」指的就是图片 Exif 信息中的 Orientation 数据,而我们没有做的,就是 preview.app(或者说操作系统) 已经帮我们做了的事情:根据 Exif 信息中的 Orientation 不同的值,旋转所要查看的图片,将其以正确的方式显示在浏览器的网页之中,放到我们的需求中,就是在 canvas 上 draw 新图的时候,没有按照正确的方向去 draw。
EXIF 和 Orientation Tag 的一些历史
早期的数码相机所拍摄的图片,图片的 metadata(EXIF)信息中并没有 Orientation Tag,只会按照相机本身设备的默认方向存储图片,比如使用默认为 landscape 的相机,竖持设备,拍出来的图片就被旋转了 90°;早期的图片查看软件,可以暂时将图片旋转成正确的角度以供查看,如果需要一劳永逸的解决角度问题,必须手动修改。
用户手动修改图片的过程:decompress JPEG -> rotate -> re-compress JPEG again,可能会导致再一次的有损压缩,但当时的软件基本可以做到无损的解码-旋转-重新编码,所以旋转问题并不是一个特别严重的问题。
同时,一些相机厂商意识到这个角度问题,想要解决,所以他们在相机产品中加入了 orientation sensor 去识别拍摄图片的角度,这里产生了一个问题:生成图片的 image signal processing chips (ISPs) 并不能按照 orientation sensor 识别的到的角度直接生成一张图片。
于是,厂商们就决定将这些数据写入图片的 metadata(EXIF) 之中,所以实际上相机存储的图片实际上本身就可能为错误的旋转角度(默认 landscape 的设备,拍摄了 portrait 的图片),而图片自带的数据里包含了图片本身正确旋转角度对应的值。导致:如果图片查看软件对 orientation tag 做了支持,显示的图片会以适当的角度呈现在我们面前,否则我们看到的图片,(对设备来说正确的)角度在我们看来就是错的。
结合上文中将图片在各种环境下(操作系统,浏览器,html)打开、查看的例子,很显然操作系统、浏览器本身是对 orientation tag 做了支持的,而在 DOM 中并没有,至少并没有为我们自动作出旋转2。
Orientation Tag 的值和对应的角度
Orientation Tag 有八个值,对应不同的翻转角度:1,2,3,4,5,6,7,8 3。
- case 1: The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
- case 2: The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.
- case 3: The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.
- case 4: The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.
- case 5: The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.
- case 6: The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.
- case 7: The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.
- case 8: The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.
- Other: reserved
描述中出现的 「visual top」 等,原文中说是指**「a display device」**上的上下左右,理解起来就是平时用到图片查看软件,具体的上下左右所指其实和我们日常中的并无出入4:
关于 0th row 和 0th column ,大致的理解是,一张 JPEG 文件,在 compress 和 uncompressed 的时候会由很多的行 row 和 column 组成, 0th row 和 0th column 代表的是拍摄的景物(captured scene)的 上和左,对照如下表格,以 iPhone 拍摄和 case 6 来做个例子:
- 首先 case 6 的 0th row 在显示设备的右,0th column 在显示器的上面;
- 接着,想象一下 iPhone 拍摄,当 home 键在右边的时候,case 为 1,也就是拍摄的和显示 top = 0th row,left = 0th column;
- 然后把 iPhone 旋转成 home 键在下的竖持位置,这个时候,对比 case 1 时候设备 top 和 left,top 被转到了右,left 被转到了上,即顺时针转了 90 度;
- 所以,此时生成出来的 JPEG 在没有自动读取 EXIF 进行旋转的图片查看器里,就是被逆时针旋转了 90 度;
- 于是,相机就在图片的 EXIF 中 Orientation Tag 上加了 6 的值,告诉图片查看器需要顺时针旋转 90 度。
这些值有一张更好的图可以说明它们之间的相互关系5 :[1, 6, 3, 8] 是相互一次顺时针 90 度方向的关系,而 [2, 5, 4, 7] 则对应了 [1, 6 ,3, 8] 的水平镜像 67。
不同设备之间的区别
调用系统摄像头:
EXIF 的问题主要出现在移动端 iPhone 拍摄的图片上,但是使用 iPhone 前后置摄像头进行旋转各种角度拍摄的结果都只在 [1, 6, 3, 8] 之间,不太确定什么情况才会产生 [2, 5, 4, 7];
Android 设备手边能够测试的不多,几台下来,有的是直接没有写入 EXIF.Orientation 信息(比如小米);值得一提的是:貌似部分 Android 手机会无论以什么角度旋转来拍摄图片,在生成图片的时候都会把图片旋转成正确的角度,然后在 orientation 打上 1 的值(比如华为)即:
- **普通设备(包括 iPhone)的拍摄图片到生成图片的过程:**拍摄 -> 生成图片 -> 根据角度给 EXIF.orientation 打上 [1, 6, 3, 8] 间不同的值;
- **部分 Android (测试华为手机)设备:**拍摄 -> 根据设备所持旋转角度生成正确角度的图片 -> 给 EXIF.orientation 打上 1 的值。
App 内调用摄像头:
比如微信对话内拍摄发送给对方的图,是没有 EXIF 信息的;别的没有做太多测试。
解决问题:CSS Way: image-orientation 属性:
W3C 已经有了相关的 CSS3 Candidate Recommendation 和 Working Draft;不过实际中浏览器提供的支持比较差,各家中只有 Firefox 和 iOS Safari 的 latest 的版本对这个属性有支持 。
解决问题:JavaScript Way,步骤、 demo 和原理:
步骤:
假设已经得到了 file,并且通过 readAsArrayBuffer
后得到了我们需要的 view,那么获得 orientation 的值的获取步骤是:
检查 JPEG 的 SOI maker:0xFFD8 是否存在?继续 :中止;
检查 APP1 的 marker:0xFFE1 是否存在 ?继续 :中止;
检查 Exif header 的开始是否为「Exif」(ascii:0x45786966)?继续 :中止;
找到 TIFF header,通过开头两个字节的数据判定字节序:
- 「II」0x4949 -> little-endian;
- 「MM」0x4D4D -> big-endian;
- 两者都不是,something wrong or crazy happened,中止;
找到 IFD0;
- 有 -> 读取其中的 entry 的数量。得到 entry 入口偏移量;
- 无 -> 中指;
找到 IFD0 中的 entries,循环读取查找是否有 tag number 为 0x0112 的 entry:
- 有 -> 继续读取 format,components,value 的值,计算得出 tag 真正的值;
- 无 -> 没有 orientation 信息。
伪代码:
获取 EXIF.orientation:
最对应的旋转:
D is for Demonstration:
CodePen 链接:Get JPG/JPEG image’s embedded orientation information89。
附录
针对文章开头问题的解决方案没有多少行,但要得出这个结论绕不过去的一个疑问就是「 JPEG 文件内部存了什么?」。附录就是来解决这个疑问,内部原理是怎么样的。
附录部分包含两部分内容,「JPEG 中有些什么?」和 《Description of Exif file format》的部分翻译。显然,第一部分的结论基于第二部分的翻译和一些手动的测试10。
JPEG 文件中有些什么?
JPEG 文件的线性结构
Brief on JPEG:
- JPEG(Joint Photographic Experts Group) 指一种对图像压缩标准的简称,日常口中提到的 JPEG 文件,更多的是指 JPEG/JFIF(JPEG File Interchange Format) 文件11。
- JPEG 中的数据均是 Big-Endian 格式;
- JPEG 开头的一部分数据,对于其中包含的图像数据的解码并没有作用;
- Exif 信息就包含这些信息中,内容主要是不同软件,硬件制造商在图片上写入的数据,比如拍摄的环境、设备信息,或者用了什么软件。
- 如表所示,这些数据,会被划分到不同的 APP 区块(application segment);
其中 APP0 是 JFIF application segment, APP1 里面则对应的是 Exif 数据(也就是我们需要找的),APP 13 对应 PhotoShop 写入的一些数据;
表中的 marker 一列是不同 APP 开头部分的标志,但找到它们并不能完全确定之后的就是 APP 内容,还需要对内容部分做出筛选来过滤 marker 的真假。
Brief on APP1:
- 开头是 APP1 maker: 0xFFE1:
- 紧跟的两个字节表示的内容的是 APP1 数据长度的字节数;
- Exif header 部分开始:
- 首先是「Exif」四个字母的 ascii string: 45 78 69 66
- 之后是两个字节的 0:0x0000;
- TIFF header 开始:
- byte order mark;
- TIFF marker;
- 从 TIFF header 到第一个 Image File Directory 的偏移量;
- 之后依次是,IFD0(第一个 Image File Directory)、ExifSubIFD、GPSIFD。而我们需要的 Orientation 信息就在这个 IFD0 之中。
Brief on TIFF header:
- Exif(Exchangeable image file format)建立在 TIFF(Tagged Image File Format)格式之上。所以开头有 8 个字节长度的 TIFF Header,用来表示其中的数据信息和结构;
- 最开始的两个字节代表 TIFF 中数据的 byte order:
- 如果是 「II」(0x4949),数据是 Little-Endian;
- 如果是「MM」(0x4d4d),则是 Big-Endian;
- 之后的两个字节是 TIFF marker,按照数据格式 byte order 的不同,可能会显示为 0x2A00 或者 0x002A;
- 之后的四个字节表示从 TIFF header 的偏移量到 Exif header 的偏移量。一般 TIFF header 后面紧跟的就是 Exif header,所以这个偏移量一般都是 8(0x00000008)。
Brief on Exif’s IFD(Image File Directory):
- TIFF header 之后通常就是 APP1 中所有 IFD(Image File Directory);
- 每个 IFD 开始的两个字节,表示其中 entry 的数量;
- 每个 entry 可以理解成针对不同属性值包裹的文件夹,包含了属性,格式,数量等值。
Brief on entry of IFD(Image File Directory):
- 每一个 entry 由四个部分组成:tag number,format,count,value;一共 12 个字节长;
- tag number 就是属性的 code name,比如说想找 Orientation,对应的 code 是 0x0112(hex);
- format 是数据的格式;
- components 对应数据值的长度;
- value 的值,但不一定是 tag 的值,也有可能是 tag 的值的偏移量;这里有一个计算,format 对应了一个 bytes per component 的值,需要与 components 的值相乘,得到真正数据值(val)的数据长度(bytes length),如果这个数据长度大于 4 个字节,那么 value 的值是 val 的偏移量,如果小于 4,则 value 就是 val。
- 最后取得 tag 值(val)的数据,对应不同数据格式(format),进行 decode。
《Description of Exif file format》 截至 IFD data structure 的翻译
《Description of Exif file format》 截至 IFD data structure 的翻译。(不算是直译,可能是会夹杂一些自己的理解,句子写长或者缩短,推荐对照原文阅读)
这里补充一个 hack,如果在 HTML 中创建一个 iframe,再从中引用这张 img,图片会以正确的方向显示。 ↩︎
Exif Orientation 这一部分参考、翻译自《The most evil feature ever conceived: the Exif Orientation Tag》。 ↩︎
出自 PDF:Exif Version 2.3 的第三十页 ,《4.6.4 TIFF Rev. 6.0 Attribute Information》。 ↩︎
出自 PDF:Exif Version 2.3 的第三十二页。 ↩︎
出自 PDF:Exif Version 2.3 的第三十五页,《Relationship between the orientation tag and rotation processing to display image data on a screen》。 ↩︎
参考文章中说,Exif 的 Orientation 的值为 2, 5, 4, 7 的时候属于 rare case,测试完全不会出现在手持设备的拍摄图片中,任何角度的前置后置摄像图拍摄的图片都没这几个值。 ↩︎
旋转角度(Orientation Tag)这一块理解起来有点抽象,参考《ImpulseAdventure - JPEG / Exif Orientation and Rotation》和《图片Exif 信息中Orientation的理解和对此的处理》。 ↩︎
CodePen 测试的时候,不一定每张图都有 Exif 信息,或者各家厂商写入会不会格式不同我没兼容到错误,可以前去 图虫 EXIF 查看器 alpha 版 查看图片本身是否有信息,Orientation 信息一般都是在 IFD0 中。 ↩︎
除了证明 Exif Orientation 值确实被提取出来的 CodePen 之外,还制作了一个根据不同 Orientation 值来对图片进行对应旋转、镜像的 Codepen:ctx.transform ↩︎
推荐个软件 Hex Friend,debug 这些二进制数据读取操作挺有帮助的;cmd+L 定位到 offset,cmd + F 寻找 text 或者 hex 内容。知乎上找到一个答案,对JPEG、APPn、IFD 的结构也说得挺好,提到了一个 windows 平台的软件 MagicEXIF,打开图片可以得到比较直观的感受)。 ↩︎
技术发展迭代很快,所以这些笔记内容也有类似新闻的时效性,不免有过时、或者错误的地方,欢迎指正 ^_^。
BEST
Lien(A.K.A 胡椒)