最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 从Exifjs的原理入手,搞定js二进制数据的操作

    正文概述 掘金(酢排骨)   2021-05-16   881

    前言

    上一章我们大概了解如何通过js库来获取Exif信息。那么这些库是怎么实现的呢。我们用比较流行的Exifjs来做分析和学习。
    通过学习Exifjs,我们大概可以发现,我们需要了解以下知识:
    1、Exif标识
    2、base64的由来和作用,以及常用api
    3、js如何操作二进制流,这也是js实现读取exif信息的核心

    知识点梳理

    1、Exif信息标识

    当照片拍摄的时候,会把Exif参数信息存放到jpeg格式文件的原始数据内部。
    通过查看Exifjs源码,会发现它有一个映射关系,如下图:
    从Exifjs的原理入手,搞定js二进制数据的操作
    那么这个映射关系是如何来的呢,通过查看Exif官方标准,发现exifjs的映射表与下图的tagId、tagName的映射关系相同。
    知道这些关系以后,后面我们会根据tagId来进行exif信息的操作。
    从Exifjs的原理入手,搞定js二进制数据的操作

    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对应的数据返回如下:
    从Exifjs的原理入手,搞定js二进制数据的操作

    可见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方法。
    从Exifjs的原理入手,搞定js二进制数据的操作

    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]
    从Exifjs的原理入手,搞定js二进制数据的操作
    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标准真烧脑。。。
    从Exifjs的原理入手,搞定js二进制数据的操作


    起源地下载网 » 从Exifjs的原理入手,搞定js二进制数据的操作

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元