当前端程序员遇到会写代码的产品经理......学会了如何窥探用户隐私
关注《前端阳光》回复加群,加入技术交流群
1、缘起
事情是这样的,上周我接到了个需求,产品经理想在微信内嵌的H5里,静默获取图片的详细信息,比如拍照的时间、地理位置、设备指纹等。一方面最近活儿太多影响我摸鱼,另一方面是这个需求没办法完全实现,因为有些图片信息就是没有,又懒得解释,于是我和产品经理说:
「这个需求做不了。」
原以为事情到这里就结束了,万万没想到,我遇到了一个会写代码的产品经理......
第二天早上,我的直属技术领导直接找到了我,和我说:
「“xxx(产品经理)”把获取图片信息的Demo写出来了,这个需求可以做,之后就你来吧。」
当时我的内心是这样的:
拿错了拿错了:
总之,最后这个需求还是「落到我头上了」。
所以就有了这篇:「获取图片的Exif数据」。
2、正文开始
像这种附带拍摄时间、GPS地理位置等信息的图片格式叫做「Exif」,英文全称为 Exchangeable image file format
,即可交换图像文件格式 。
之前有一阵子传得沸沸扬扬的微信上传原图会泄漏隐私数据的事儿,其实就是这个Exif的锅。。。实际上这些Exif数据是由拍照设备产生的,并且用户可以自行修改和删除,换句话说,「你可以随便修改Exif数据」。比如这样:
当然,微信上传原图确实会有风险,因为一不小心你就告诉了别人你是在哪里拍的这张照片,但是这个事不能全赖微信,理论上所有上传图片的程序都有泄漏隐私的风险。你可以像上面说的那样直接改数据,也可以点左下角的删除属性和个人信息来「直接删除Exif数据」:
因为Exif数据是可以伪造和删除的,所以只能作为参考,而不能当作确切信息。
那么,如何获取Exif数据呢?
github上有个4.1k star的开源项目exif-js:https://github.com/exif-js/exif-js
用起来很丝滑,我写了个Demo来获取图片的Exif数据:
http://jsrun.net/kwTKp/edit
随便找张手机拍的照片上传看看:
可以看到已经获取到了图片的Exif数据,并且通过百度地图API反查出了具体的地址。
但是对于非设备直接拍照的图片,或者图片经过了压缩处理,比如你从网上直接下载下来的图片等,可能就拿不到Exif数据。
作为一个有追求的前端,至少不能输给产品经理我们不仅要知其然,还要知其所以然。
所以,去看看exif-js的源码:
一千多行,在下告辞
呵,区区一千多行,话不多说,就是肝。
以下是源码分析。
分析比较长,可以不看
直接拉到最后给我点个赞 :)
首先是个匿名自执行函数,这样写的好处是能拥有独立作用域,既避免污染外界代码,也避免被外界代码污染。
接着定义并导出EXIF
对象,这样就可以把EXIT
暴露给外部,以便外部获取并使用EXIF
对象。
这里检查了当前环境的模块化规范,exports
是commonjs
的规范,如果当前环境支持exports
,则使用exports
方式导出模块,如果不支持,则将EXIF
挂到全局对象下。
EXIF上定义了很多属性和方法,比如我们demo里用到的getData
方法:
// exif-js
EXIF.getData = function(img, callback) {
// ...省略部分非核心代码
// 判断img对象上是否有exifdata属性值
if (!imageHasData(img)) {
// 本地上传一般是没有exifdata,需要调用getImageData去获取
getImageData(img, callback);
} else {
// 如果有exifdata属性值直接回调
if (callback) {
callback.call(img);
}
}
})
可以看到入参是img对象和callback回调函数,如果img对象上有exifdata属性值,则直接调用callback,如果没有,则需要调一个方法去获取:getImageData
。
因为图片有可能是我们通过src属性定义的,也有可能是本地上传的,而src属性值有可能是base64格式,也可能是blob或http/https等格式,所以「getImageData」会对不同情况的图片对象进行处理,最后都处理为ArrayBuffer
对象。
ArrayBuffer
是一个字节数组,用来表示通用的、固定长度的原始二进制数据缓冲区。
ArrayBuffer
是一个只能读不能写的对象,因此源码中使用DataView
对象对ArrayBuffer
进行写操作。
上述的几个对象实际上都是JS提供的处理二进制数据的对象,如果想进一步了解,推荐阅读这篇文章:《聊聊JS的二进制家族:Blob、ArrayBuffer和Buffer》:https://zhuanlan.zhihu.com/p/97768916
我们继续看源码:
拿到ArrayBuffer
对象后,就可以对二进制数据进行解析了。
// 解析图片的Exif数据
function findEXIFinJPEG(file) {
var dataView = new DataView(file);
// 根据文件头的前2个字节判断是否为图片文件
if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) {
return false; // not a valid jpeg
}
var offset = 2,
length = file.byteLength,
marker;
// 遍历查找Exif数据信息串
while (offset < length) {
// 这里是将0xFFE1分成两个字节判断,先判断第一个字节是否等于0xFF
if (dataView.getUint8(offset) != 0xFF) {
return false; // not a valid marker, something is wrong
}
marker = dataView.getUint8(offset + 1);
// we could implement handling for other markers here,
// but we're only looking for 0xFFE1 for EXIF data
// 再判断第二个字节是否等于0xE1,也就是255
if (marker == 225) {
return readEXIFData(dataView, offset + 4, dataView.getUint16(offset + 2) - 2);
} else {
offset += 2 + dataView.getUint16(offset+2);
}
}
}
这里有个前置知识点:Exif信息以「0xFFE1」作为开头标记,所以源码中的这段while遍历就是为了找到Exif信息串的开头,然后再调用readEXIFData
方法。
readEXIFData
就是对Exif信息串做解析,即通过遍历二进制串,匹配出属性及对应的值,并放到一个对象tags
中,解析完成后返回这个对象。
除了Exif数据,源码中还解析了IPTC数据(作者,版权,字幕,细节描述等)、XMP数据(Extensible Metadata Platform,Adobe公司提出的关于元数据的创建、处理和交换的一套标准)。
扩展资料
exif-js的API、属性等使用说明可以参考这篇博客:http://code.ciaoca.com/javascript/exif-js/
产品经理还有一个获取设备指纹的需求,因篇幅所限,就下次再写吧,这里给出Demo:http://jsrun.net/2wTKp/edit,用的是fingerprintjs
。
3、总结
作为一个秃头程序员,身处和谐社会,我积极反思了一下自己,实际上不能直接和产品经理说“这个需求做不了”。如果真的做不了,应该给出具体的技术可行性分析,再得出做不了的结论,否则就应该说:“我去看下可行性”,然后去仔细了解下技术实现上的难点,再和产品经理沟通说明。
总而言之就是:
「不能直接说“这个需求做不了”」。
以上都是废话,大家看看就好。
本文的重点其实还是中间那段获取Exif数据的分析。
闲扯两句
中秋快到啦,我的老家最近疫情又严重了,所以回不了家,只能中秋的时候一起云赏月云吃饼了~~希望疫情能早日结束吧。
提前祝大家中秋节快乐,团团圆圆,幸福美满!