前言
上一章我们大概了解如何通过js库来获取Exif信息。那么这些库是怎么实现的呢。我们用比较流行的Exifjs来做分析和学习。
通过学习Exifjs,我们大概可以发现,我们需要了解以下知识:
1、Exif标识
2、base64的由来和作用,以及常用api
3、js如何操作二进制流,这也是js实现读取exif信息的核心
知识点梳理
1、Exif信息标识
当照片拍摄的时候,会把Exif参数信息存放到jpeg格式文件的原始数据内部。
通过查看Exifjs源码,会发现它有一个映射关系,如下图:
那么这个映射关系是如何来的呢,通过查看Exif官方标准,发现exifjs的映射表与下图的tagId、tagName的映射关系相同。
知道这些关系以后,后面我们会根据tagId来进行exif信息的操作。
2、base64相关
base64大家可能比较熟悉,其实它不算Exif信息获取的一个核心功能,在实现中的作用也只是为了兼容图片数据格式。这里我们单独罗列出来是因为它与我们前端开发有着紧密的联系。那么了解了它的由来、作用以及相关的api对我们日常的工作还是会有一定作用的。
对于base64的由来、作用,建议大家去看看base64笔记,这里阮一峰老师做了专业、详细的讲解。
我这里主要说一下相关api的应用:
1、window.atob和window.btoa,用法很简单,作用也很简单就是有关base64的解码和编码
const str = "Man";
const base64 = window.btoa(str); // 字符串编码成base64
const result = window.atob(base64) // base64解码成字符串
console.log(base64) //TWFu
console.log(result) // Man
3、js中有关二进制数据的操作
js中关于二进制的操作主要有两大块,blob和ArrayBuffer。
3.1 Blob
Blob是一个大类,我们常用的File对象,继承自Blob对象并扩展支持用户上传。所以Blob具有的功能,基本上File都会具有。
3.1.1Blob构造函数Blob( array, options )
array,数组里面的元素共同组合成Blob的数据源。
options 可能会指定如下两个属性:
- type,默认值为 "",它代表了将会被放入到blob中的数组内容的MIME类型。
- endings,默认值为"transparent",用于指定包含行结束符\n的字符串如何被写入。 它是以下两个值中的一个: "native",代表行结束符会被更改为适合宿主操作系统文件系统的换行符,或者 "transparent",代表会保持blob中保存的结束符不变。
下面我们对于不同类型的数据调用一下Blob,来看看都会返回什么。
var data1 = 1;
var data2 = true;
var data3 = 'a';
var data4 = [1];
var data5 = { value: "test" };
var blob1 = new Blob([data1]);
var blob2 = new Blob([data2])
var blob3 = new Blob([data3])
var blob4 = new Blob([data4])
var blob5 = new Blob([data5])
var blob6 = new Blob([data1, data3])
var blob7 = new Blob([JSON.stringify(data5)])
var blob8 = new Blob([JSON.stringify(data5)], {type : 'application/json'})
var blob9 = new Blob([data5],{type : 'application/json'})
console.log(blob1, 'data1')
console.log(blob2, 'data2')
console.log(blob3, 'data3')
console.log(blob4, 'data4')
console.log(blob5, 'data5')
console.log(blob6, 'data6(data1,data3)')
console.log(blob7, 'data7(data5)')
console.log(blob8, 'data8(data5)')
console.log(blob9, 'data9(data5)')
1-9对应的数据返回如下:
可见Blob有两个属性 size表示包含数据的字节大小,type的值刚才所讲的options.type。
需要注意的一点是 data5, data7这两个创建Blob以后size不一样。而区别就在于是否进行了JSON.stringify。
data5.toString()是[object Object],length 正好是15。JSON.stringify(data5).length是16。看来是跟toString有关系。
我们创建一个新的data10来验证一下。
data10.toString()是test,length正好是4,JSON.stringify(data10).length是2。
这是因为在创建Blob的时候,会调用传入数据的toString方法。
3.1.2 slice方法
这个方法返回一个新的 Blob 对象,包含了源 Blob 对象中指定范围内的数据。用法跟数组的是一样的。
var sliceData = 'abcdef123';
var dataBlob = new Blob([sliceData]);
var dataBlob1 = dataBlob.slice(0, 1);
console.log(dataBlob, dataBlob1) // Blob {size: 9, type: ""} Blob {size: 1, type: ""}
async function getTextFromBlob() {
var result1 = await new Response(dataBlob).text(); // 以字符串的方式提取blob的内容
var result2 = await new Response(dataBlob1).text();
console.log(result1, result2) //abcdef123 a
}
getTextFromBlob();
3.1.3 window.URL.createObjectURL
这个方法可以把blob转换成一个Blob URL,我们常用的就是在用户上传的时候把File转换成一个Blob URL进行访问。Blob URL的优点就是比base64生成的图片要短小。但是缺点就是它并没有保存图片信息,而是浏览器根据一定的规则生成一个标识来指向真实资源的。所以它是强依于赖浏览器的。当创建Blob URL的环境销毁的时候(比如调用createObjectURL的页签关闭、刷新)这个Blob URL也就无法访问了。
3.2 ArrayBuffer相关
ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。ArrayBuffer不能直接操作,需要通过类型数组对象或者DataView进行操作。
3.2.1 ArrayBuffer
ArrayBuffer只有一个参数,那就是要创建内存的大小,也就是字节数。创建出来的ArrayBuffer内容初始化是0。
具有的属性有byteLength,为字节大小,不可改变。
具有isView静态方法,判断参数是否为ArrayBuffer视图;还有slice方法类似数组的用法。
const buffer = new ArrayBuffer(10)
console.log(buffer.byteLength); // 10
console.log(ArrayBuffer.isView(buffer)); // false
const view = new Int8Array(10);
console.log(ArrayBuffer.isView(view)); // true
const buffer1 = buffer.slice(0, 1);
console.log(buffer, buffer1)
/**
ArrayBuffer {
[Uint8Contents]: <00 00 00 00 00 00 00 00 00 00>,
byteLength: 10
}
ArrayBuffer { [Uint8Contents]: <00>, byteLength: 1 }
*/
3.2.2 类型数组对象TypedArray
TypedArray是一个类数组视图,用于操作ArrayBuffer创造的二进制缓冲区的数据,包括9个视图Int8Array、Uint8Array等(每位占用的字节数不同)。TypedArray使用方法类似于普通的数组,但是还是有一些差异:
1、TypeArray是视图不是一种数据存储结构,它要操作的数据是ArrayBuffer中的数据。而Array本身就是用来存储数据的。
2、TypedArray中所有成员的类型是一样的,数组对成员的类型没有限制。
3、TypedArray初始值是0,数组是empty
const view1 = new Int8Array(10);// 在内存中生成一个缓冲区
const arr = new Array(10);
console.log(view1, arr)
/*
Int8Array(10) [
0, 0, 0, 0, 0,
0, 0, 0, 0, 0
]
[ <10 empty items> ]
*/
4、typedArray不同视图单个元素对应容纳的数值会有范围,比如int8Array是有符号的8位二进制数值范围[-128 ,127]
TypedArray实例化
有四种传参方式分别是:length,TypedArray、object,buffer(byteOffet,length)
const buffer = new ArrayBuffer(10);
const view1 = new Int8Array(buffer); // buffer , 可选byteoffset,length未传
const view2 = new Int8Array(5);
const buffer1 = view2.map((v, i) => i)
const view3 = new Int8Array(buffer1.buffer, 1, 2); // buffer, byteoffset偏移坐标,length视图长度
const view4 = new Int8Array(view2) // typedArray
const view5 = new Int8Array({ value: 1, length: 3 }) // object的时候会调用typedArray.from方法创建一个新的类数组
const view6 = new Int8Array([1, 2, 3, 4]) // object
const view7 = new Int16Array(buffer) // int16 每位占用两个字节,所以10个字节的ArrayBuffer的视图长度为5,也就是分成了5段
console.log(view1, view2, view3, view4, view5)
/**
view1:
Int8Array(10) [
0, 0, 0, 0, 0,
0, 0, 0, 0, 0
]
view2:
Int8Array(5) [ 0, 0, 0, 0, 0 ]
view3:
Int8Array(2) [ 1, 2 ]
view4:
Int8Array(5) [ 0, 0, 0, 0, 0 ]
view5:
Int8Array(3) [ 0, 0, 0 ]
view6:
Int8Array(4) [ 1, 2, 3, 4 ]
view7:
Int16Array(5) [ 0, 0, 0, 0, 0 ]
**/
3.2.3 DateView视图
DataView 视图是一个可以从ArrayBuffer对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。字节序此处就不展开了,有兴趣的同学可以传送了解字节序。大部分的计算机(所有英特尔处理器)都采用小字节序。
DateView的构造函数接受参数new DataView(buffer [, byteOffset [, byteLength]]),类似于我们上面讲的TypedArray中的buffer传参模式。
const buffer = new ArrayBuffer(10);
const view1 = new Int8Array(buffer);
view1[1] = 2;
const view = new DataView(buffer, 1, 2);
console.log(view)
console.log(view.getInt8(0))
console.log(view.getInt16(0)) //一次获取两个字节
console.log(view.getInt16(1)) //Offset is outside the bounds of the DataView
/**
view:
DataView {
byteLength: 2,
byteOffset: 1,
buffer: ArrayBuffer {
[Uint8Contents]: <00 02 00 00 00 00 00 00 00 00>,
byteLength: 10
}
}
view.getInt8(0)
2
view.getInt16(0)
512
view.getInt16(1)
Offset is outside the bounds of the DataView
**/
DateView不用考虑平台的字节序,指的是它制定字节序。两个字节及以上的getXXX存在两个参数“偏移量和字节序”,字节序默认是高字节序。getInt8只有一个参数,getInt16有两个参数。
const buffer = new ArrayBuffer(10);
const view1 = new Int8Array(buffer);
view1[1] = 2;
const view = new DataView(buffer, 1, 2);
console.log(view.getInt8(0)) // 2
console.log(view.getInt8(1)) // 0
console.log(view.getInt16(0)) // 512
console.log(view.getInt16(0, true)) // 2 字节序为true代表小字节序
以下是写入字节,超过两个字节也涉及到字节位的问题,同以上的get获取类似。
const buffer = new ArrayBuffer(10);
const view1 = new Int8Array(buffer);
view1[1] = 2;
const view = new DataView(buffer, 1, 2);
console.log(view.getInt8(0)) // 2
console.log(view.getInt8(1)) // 0
view.setInt8(1, 5)
console.log(view.getInt8(0)) // 2
console.log(view.getInt8(1)) // 5
view.setInt16(0, 5)
console.log(view.getInt8(0)) // 0
console.log(view.getInt8(1)) // 5
view.setInt16(0, 5, true)
console.log(view.getInt8(0)) // 5
console.log(view.getInt8(1)) // 0
4、获取Exif信息
主要思想就是,通过JPEG的格式和标志来判断信息。
关于Exif信息的含义,参考资料为文章:“EXIF信息及含义”。
1、JPEG文件都是以十六进制 '0xFFD8’开始,以’0xFFD9’结束。在JPEG数据中有像’0xFF**'这样的数据,这些被称为“标志”,它表示JPEG信息数据段。0xFFD8 表示SOI(Start of image 图像开始),0xFFD9表示EOI(End of image 图像结束)。这两个特殊的标志没有附加的数据,而其他的标志在标志后都带有附加的数据。
2、从0xFFE0 ~ 0xFFEF 的标志是“应用程序标志”,Exif使用0xFFE1标志Exif信息,Exif数据是从ASCII字符"Exif"和2个字节的0x00开始,后面就是Exif的数据了。
3、TIFF头指的是TIFF格式的前8个字节。前两个字节定义了TIFF数据采用何种字节顺序。如果是0x4949 ,表示采用"Intel"的小字节序,如果为0x4d4d ,表示采用高字节序。
4、TIFF头的最后4个字节是第一个IFD(Image File Directory, described in next chapter 图像文件目录,描述下一个字符)的偏移量。在TIFF格式中所有的偏移量都是从TIFF头的第一个字节(0x4949或者0x4d4d)开始计算的到所在位置的字节数目,这个偏移量也不例外。通常第一个IFD是紧跟在TIFF头后面的,所以它的偏移量为’0x00000008’。
5、接着TIFF头的是第一个IFD。它包含了图像信息数据。在下表中,开始的两个字节(‘EEEE’)表示这个IFD所包含的目录实体数量。然后紧跟着就是实体对象(每个实体12个字节)。在最后一个目录实体后面有一个4字节大小的数据(表中的是’LLLLLLLL’),它表示下一个IFD的偏移量。如果这个偏移量的值是’0x00000000’,就表示这个IFD是最后一个IFD。
完整代码如下:
class MyExif {
constructor(image) {
return this.getImageExif(image)
}
getImageExif(image){
if(image.src) {
return this.domImageExif(image.src)
} else if(image instanceof Blob){
return this.blobImageExif(image)
}
}
async blobImageExif(file) {
var _t = this
return new Promise(res => {
var fileReader = new FileReader();
fileReader.onload = function(e) {
res(_t.findExifInJPEG(e.target.result));
};
fileReader.readAsArrayBuffer(file);
})
}
async domImageExif(src){
let result;
if (/^data\:/i.test(src)) { // Data URI
result = this.base64Url(src);
} else {
result = await this.blobUrl(src);
}
return Promise.resolve(result)
}
base64Url(base64){
base64 = base64.replace(/^data\:([^\;]+)\;base64,/gmi, '');
const binary = atob(base64);
const len = binary.length;
const buffer = new ArrayBuffer(len); // 根据长度创建二进制缓冲区
const view = new Uint8Array(buffer); // 创建视图操作
for (let i = 0; i < len; i++) {
view[i] = binary.charCodeAt(i); //获取字符串的Unicode
}
return this.findExifInJPEG(buffer)
}
async blobUrl(src){
const buffer = await this.httpToBlob(src);
return this.findExifInJPEG(buffer)
}
httpToBlob(url){
return new Promise(res => {
var http = new XMLHttpRequest();
http.open("GET", url, true);
http.responseType = "arraybuffer";
http.onload = function(e) {
if (this.status == 200 || this.status === 0) {
res(this.response);
}
};
http.send();
})
}
findExifInJPEG(buffer){
var dataView = new DataView(buffer);
// JPEG文件都是以十六进制 '0xFFD8’开始
if ((dataView.getUint16(0) != 0xFFD8)) {
console.log("Not a valid JPEG");
return false; // not a valid jpeg
}
let length = buffer.byteLength;
let exifStartIndex = this.getExifPosition(dataView, length);
if(exifStartIndex) {
// 根据上一次获取的索引,然后根据索引后4位判断后续是否是Exif信息
if(this.getStringFromDB(dataView, exifStartIndex, 4) !== 'Exif') {
console.log('Not valid EXIF data!');
return ;
}
// 这里exifStartIndex + 6的原因是Exif数据是从ASCII字符"Exif"和2个字节的0x00开始,后面就是Exif的数据了。EXif+2个字节 正好是6
return this.getTIFFInfo(dataView, exifStartIndex + 6)
} else {
return undefined
}
}
getExifPosition(dataView, byteLength) {
let offset = 2; //因为前两位是0xFFD8,ArrayBuffer 使用的视图是Uint8Array所以从2(0xFFD8是两位)开始遍历
while(offset < byteLength) {
// Exif使用APP1(0xFFE1)标志
if(dataView.getUint8(offset) === 0xFF && dataView.getUint8(offset + 1) === 0xE1) {
//APP1的数据从"SSSS"后开始 所以加4
return offset + 4;
}
offset++;
}
}
getStringFromDB(dataView, start, length){
let max = start + length;
let result = "";
for(let i = start; i < max; i++) {
result += String.fromCharCode(dataView.getUint8(i))
}
return result;
}
getTIFFInfo(dataView, tiffOffset) {
//前两个字节定义了TIFF数据采用何种字节顺序
let bigEndian = dataView.getUint16(tiffOffset) === 0x4D4D;
// 然后的两个字节总是2个字节长度的0x002A
if (dataView.getUint16(tiffOffset + 2, !bigEndian) != 0x002A) {
console.log("Not valid TIFF data! (no 0x002A)");
return false;
}
//TIFF头的最后4个字节是第一个IFD的偏移量。
const firstIFDOffset = dataView.getUint32(tiffOffset + 4, !bigEndian);
//通常第一个IFD是紧跟在TIFF头后面的,所以它的偏移量为’0x00000008’
if (firstIFDOffset < 0x00000008) {
console.log("Not valid TIFF data! (First offset less than 8)", dataView.getUint32(tiffOffset+4, !bigEndian));
return false;
}
// 0x0112 代表Orientation
return this.getTag(dataView, tiffOffset + firstIFDOffset, bigEndian, 0x0112)
}
getTag(dataView, dirStart, bigEndian, tag){
const length = dataView.getUint16(dirStart, !bigEndian);
for(let i = 0; i < length; i++) {
//开始的两个字节(‘EEEE’)表示这个IFD所包含的目录实体数量。然后紧跟着就是实体对象(每个实体12个字节)
let offset = dirStart + i * 12 + 2;
if(dataView.getUint16(offset, !bigEndian) === tag) {
// 此处只获取Orientation的值,需要偏移8位
offset += 8;
return dataView.getUint16(offset, !bigEndian);
}
}
}
}
5、最后
1、js二进制操作的主要场景有:文件的切割、下载、内容获取,内容处理等。
2、blob相对于ArrayBuffer的可操作性还是比较小的。
3、base64、blob、ArrayBuffer的互相转换。
遇见好几次exif相关的问题了一直用exifjs。想着深究一下原理,没想到Exif标准真烧脑。。。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!