利用JavaScript获取图片的Exif信息

最近做有关图片的工作,发现在浏览器中显示正常的图片。在自己的项目中,上传之后用canvas显示出来就是sideway的形式。依稀记得图片本身有带它的原始方向的信息,到网上查阅资料,获取到了很多有用的信息。好像还没有中文的翻译。所以就准备整理出来,方便自己也是方便其他同学学习~


stackoverflow

(1)

stackoverflow上有个关于这个问题的总结

数字相片通常被保存成带有EXIF方向信息的JPEG格式。为了能够正确的显示照片,照片就需要根据它的方向信息进行旋转/镜像处理,在网页的html代码中直接放上图片,可以看到显示是有问题的。就算是在商业的web app中,对EXIF的方向的支持也是参差不齐的,见文章。一张JPEG照片可能包含如下8种方向信息:

Summary of EXIF Orientation

博客中提供了一种简单的实现方式,带示例。

我们的问题是如何在客户端旋转/镜像图片才能正确的显示,并且能够不影响后续的处理?

有一个JS的库exif-js可以解析EXIT数据,包括方向的属性。Flickr在他们的技术博客中还提到了解析数据量大的照片的性能问题,推荐使用webworkers,见文章(下面会翻译这个文章)。

Console工具能够正确的显示照片。另外还有一个php的脚本也解决了这个问题(注释:连接已失效;因为这个问题其实是很老的问题,这些回答也都是12年左右的。)

(2)

github的项目JavaScript-Load-Image 提供了完整的关于获取EXIF信息的解决方案。

(3)
fiddle上有关于只需要简单获取图片的方向信息并对图片做Transform的代码。获取图片的方向信息正确显示带方向信息的图片,canvas方式


Flickr

上面提到了Flickr的技术文章有介绍,讲解的很详细,有必要整理出来(原始的文章也是12年的老文章啦)。

什么是Exif

Exif是Exchangeable image file format的缩写。是一种标准,用在数字产品中包括照片,声音等。文章描述的是在照片中使用的标准的标示。

Flickr现在是如何解析Exif数据的呢

现在我们是在图片上传到Flickr的服务器后才解析的,然后把数据显示在照片的metadata页面上https://www.flickr.com/photos/rubixdead/7192796744/meta/in/photostream。这个页面显示了相机记录的照片的所有信息,包括相机的类型,图片的大小,曝光设置等。我们现在使用的是ExifToolhttp://www.sno.phy.queensu.ca/~phil/exiftool/ 这个工具来解析数据。不过这个工具是在服务端的,一个可以自动运行的解决方案。

在客户端解析Exif数据的时机

在一次的“上传项目”中,我们发现现代的浏览器可以直接从磁盘中读取照片的数据,直接使用FileReader API(http://www.w3.org/TR/FileAPI/#FileReader-interface)。这让我们意识到,我们可以在照片被上传之前就解析Exif数据。在用户编辑照片的时候,点击上传按钮之前就给她们展示这些信息。

为什么要在客户端做Exif的数据解析呢

既然我们已经在服务端做了Exif的数据解析了,为什么还要考虑在客户端做这个事情呢?因为在客户端解析既快又有效率。这样就可以给用户展示缩略图而不需要在DOM中加载整个图片了(这会耗费掉大量的内存,也回影响性能)。用户也可以在第三方的应用中给照片添加标题,描述,标签等,同同时把这些信息保存到Exif数据中。当用户上传照片的时候,我们就能够给用户展示这些信息了。

使用Web Workers

我们一开始利用JavaScript读取文件的字节信息做了一些测试和研究。发现很少有人利用这种方式来实现(注:这是12年的文章),这种方式不难,但是比较麻烦。很快我们就发现用这种方式的话,用户的浏览器在跑一个10Mb的数据的时候就会处理的很慢了。Web workers可以让我们把要解析的字节数据放到一个独立的cpu线程中去。这样可以释放掉用户的浏览器,当解析Exif的数据的时候,浏览器就可以继续使用上传功能(不会因为需要大量处理解析的任务而卡住)。

Exif处理流

第一件事情就是先开一个web worker的线程。当用户添加一张照片的时候,我们就可以创建事件来处理了。当web worker调用postMessage()事件的时候,我们捕获到这个事件,然后解析Exif数据展示到页面上。其他的工作在这个时候也可以同时处理了,比如解析xmp数据,因为DOM在worker的线程中是不能使用的。

使用Blob.slice()函数,我们可以只用把前128kb的数据给web worker处理,这样可以加快速度。因为Exif数据是保存在前64kb中的,但是iptc有时候也会超过这个限制,特别是xmp标准来组织信息的时候。

1
2
3
4
5
6
7
8
9
if (file.slice) {
filePart = file.slice(0, 131072);//128*1024 b
} else if (file.webkitSlice) {
filePart = file.webkitSlice(0, 131072);
} else if (file.mozSlice) {
filePart = file.mozSlice(0, 131072);
} else {
filePart = file;
}

我们创建了一个FileReader对象,传给Blob来读取。

1
2
3
4
5
6
7
8
9
10
11

binaryReader = new FileReader();
binaryReader.onload = function () {
//这里处理读取到的数据,并传给worker
worker.postMessage({
guid: guid,
binary_string: binaryReader.result
});

};
binaryReader.readAsBinaryString(filePart);//这里读取的是二进制的slice数据

Worker接收到二进制的字符串,把它传递给不同的Exif处理器。一个用来处理Exif的数据,一个用来处理XMP形式的IPTC数据,一个用来处理没有形式的IPTC数据。每一个处理器都使用postMessage()函数把处理后的Exif数据返回给主模块。这些返回的数据,会在后续的上传api的中一起整合上传到后端。

异步处理Exif的解析

当异步处理Exif的数据的时候,我们不能及时的得到信息。还得阻止用户来对照片进行排序等操作,直到Exif数据被解析完,时间被标示好。(因为Filckr的一个功能就是对照片添加标签等,所以这里还讲到了一些标签的问题)。

The Nitty Gritty: 创建Exif解析器,处理数组

创建一个Exif的解析器不难,但是需要考虑一些事情:

  • 哪些Exif信息是我们需要处理的?(Exif, XMP, IPTC, 其他情况 还是 所有情况?)
  • 处理二进制字符的时候,是低字节序还是高字节序?
  • 如何在浏览器中读取二进制数据?
  • 已有的类型数组还是需要自己创建自己的数据结构?

读取二进制数据

接下来这里详细介绍了照片的二进制数据的头信息中,每个字节表示的含义,另外因为文章解析了Exif的很多数据,所以考虑的比较多,代码也比较长,决定用一个简单的例子,整合上文章中的原理来说明。其中例子来源于https://jsfiddle.net/wunderbart/dtwkfjpg/,只获取了Exif中的方向信。ps:下面的顺序是按照倒叙来叙述的,先给程序,然后再到原理…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function getOrientation(file, callback) {
var reader = new FileReader();
reader.onload = function(event) {
var view = new DataView(event.target.result);

if (view.getUint16(0, false) != 0xFFD8) return callback(-2); //SOI 表示图片的开始

var length = view.byteLength,
offset = 2;

while (offset < length) {
var marker = view.getUint16(offset, false);//offset=2
offset += 2;

//offset = 4
if (marker == 0xFFE1) {//APP1

if (view.getUint32(offset += 2, false) != 0x45786966) { //Exif标志 offset=6
return callback(-1);
}

var little = view.getUint16(offset += 6, false) == 0x4949;//低字节方式是true还是false
offset += view.getUint32(offset + 4, little);

//offset = 20 Dir.Entries的位置
var tags = view.getUint16(offset, little);//获取总共有多少个IFD
offset += 2;

//每个IFD 是12字节长
for (var i = 0; i < tags; i++)
if (view.getUint16(offset + (i * 12), little) == 0x0112)//0112是IFD中标签表示orientation
return callback(view.getUint16(offset + (i * 12) + 8, little));
}
else if ((marker & 0xFF00) != 0xFF00) break;
else offset += view.getUint16(offset, false); //直到找到APP1才是我们要的信息
}
return callback(-1);

}

reader.readAsArrayBuffer(file.slice(0, 64 * 1024));//读取的是前64Kb的数据,二进制字符
}

如下图所示的一个图片的前48字节的信息,分成了2个字节一组,一行12个字节的展示:

注意与上面程序的对应:

  • SOI: 表示图片的开始,第0字节开始,长2个字节的值 = 0xFFD8
  • APP1: 第2个字节开始, 长2个字节的值(offset=2, getUint16())= 0xFFE1
  • Exif的标志: 第6字节开始,长4个字节的值=0x45786966
  • little endian : 低字节的存储方式字段,第12字节开始=0x4949
  • Dir. Entries : 有多少个IFD, 第20字节开始,2个字节的数值
  • 接着就是IFD的信息,每个IFD信息总共12个字节,前2字节表示这个IFD的tag, 这个IFD中的第8个字节内容表示数据/地址;例如上图的第一个IFD信息的,前2个字节是0x0E01,表示“图片的描述”,0x9E00表示了这块数据的位置。 程序中,就是读取tag是0x0112表示这块IFD信息是orientation方向数据,然后读取第8个字节的内容得到orientation的信息。

来看下标准JPEG照片的数据信息结构:

Basic Structure of Compressed Data Files
如图所示,注意对照上面程序里面读取的内容,APP1部分包括了APP1的标记,Exif标志等。在JPEG标准中,APP1包括的所有的信息内容不超过64Kb。TIFF结构中包括了文件头信息,最多两个IFD。第一个IFD记录的是主图片的信息。第二个IFD记录的是缩略图的信息。关于IFD的信息如下图:


头信息过了就是Directory Entries的数量,每12字节是一个Dir. Entry的入口,在每一个Directory Entry中的第8字节是value/offset。

这样我们就能够解析Exif信息,找到需要的信息啦~

结论

在解析照片的Exif信息的主要步骤:

  1. 初始化web worker
  2. 获取文件的引用
  3. 获取文件的一部分信息(Blob slice,还记得不,只需要读取前128kb的数据)
  4. 读取二进制字符信息
  5. 找到APP1/APP0的标志
  6. 找到Exif和TIFF的头标志
  7. 找到IFD0和IFD1
  8. 处理程序进入到IFD0和IFD1
  9. 解析的数据返回给worker

这就是读取Exif的完整步骤啦!麻烦的问题就在于各种相机的不同,产生的照片的格式也会随着时间有所变化。

最后的注意:Web workers使得在客户端处理Exif信息变得可行。类似于这样的任务不使用web workers也同样可以,但是在运行任务的时候就会锁住UI线程 —— 对于web app来说,特别是还要去用户有交互的web app,这显然不是一个理想的方案。