JavaScript二进制数据处理

under

in tech

Published: 2020-02-28

JavaScript类型化数组

使用JavaScript类型化数组可以方便地操作二进制数据,它分为两层,数据缓存和数据视图,数据缓存(ArrayBuffer)用于储存数据,但不负责数据读写,读写数据需要通过创建数据视图来实现,类型化数组就是数据视图的一种。

数据缓存和数据视图的关系如下:

let buffer = new ArrayBuffer(16);
buffer.length; // 16
let int32View = new Int32Array(buffer);
int32View.length; // 4
let int16View = new Int16Array(buffer);
int16View.length; // 8

很多操作二进制数据的API用到了类型化数组

File和Blob

FileBlob提供了文件操作的接口,文件也可以看作是一块(尚未加载的)二进制数据,需要读出来,因此其操作大多是异步的。FileBlob的区别在于后者提供了文件基本操作,前者在此基础上增加文件的常用属性,如namelastModified

读文件内容的通用方法是创建一个FileReader对象,它提供了将文件读取为ArrayBuffer、dataURL、文本或者Binary String的API。

比较简单的应用可以使用Blob.arrayBuffer()的内置方法读取二进制数据,它返回一个Promise,不提供读取进度的反馈信息。UTF8编码的文本文件可以使用Blob.text()读取其内容,如果文本文件不是UTF8编码,则需要使用FileReader

URL.createObjectURL()可以将文件内容转化为dataURL,这是同步操作,不适用于大文件或者网络带宽比较小的场景。

String

String类型不适合处理二进制数据,因为String是文本的抽象,即Unicode字符集上元素的集合。文本和适合持久存储的二进制数据之间存在编解码过程,所有文本都可以通过编码转化成二进制数据,但反过来不成立,即对于任一编码,都存在无法解码成文本的二进制数据,因此String类型不适合处理二进制数据。

虽然任何文本编码都不是String和二进制数据之间的双向映射,但可以创建一个这样的映射,因为JavaScript String是用UTF16字符实现的,每个字符理论上可以表示0-65535之间的任意值,而二进制数据的每个字节都可以用0-255之间的任意数来表示,因此String的每个字符和二进制数据的每个字节之间可以建立联系:s.charCodeAt(i) === new Uint8Array(buffer)[i],这就是Binary String,一种与文本无关、专为处理二进制数据而存在的数据类型。

Binary String用了16位来表示8位有效信息,虽然浪费了一倍的空间,但避免了不少问题,例如字节序、二进制数据字节长度为单数、UTF-16代理对(surrogate pair)。

Base64

Base64(RFC 4648)是一种用ASCII字符表示二进制数据的编码格式,共使用了64个可打印的ASCII字符,每个字符可表示6位有效信息,每3个字节的数据被编码成4个ASCII字符,数据大小增加了约1/3。

JavaScript提供了btoa()将Binary String编码为base64,以及atob()将base64解码为Binary String。如果文本只包含ASCII字符,也可以直接把它交给btoa()来编码,因为这种String数据和Binary String恰好是相同的。超出这个范围,天然的一对一关系就不存在了,必须把文本先编码成二进制数据,再将二进制数据编码成base64,这种情况下要确保编码方和解码方使用的字符编码相同,UTF16(效率高)或UTF8(通用)是常见的选择,MDN提出将String编码成base64的多种方案,都是基于UTF16或者UTF8(使用encodeURIComponent()的例子可以认为是基于UTF8)。

dataURI

JavaScript提供了encodeURI()encodeURIComponent()两个函数将String编码成UTF8,但将大部分字节(UTF8每个字符可能用1、2、3、4个字节来表示)进行了转义。这两个函数的区别是,前者不会对URI里的保留字符如/:@?&#进行转义,其输出可以直接作为URI使用,后者会对URI里的特殊字符进行转译,其输出可以嵌入到URI的内容里。

这两个函数可以对所有有效的文本进行编码(如果String数据中存在不完整的UTF16代理对,会抛出异常:URIError: malformed URI sequence),对于二进制数据,可以先将其转化为base64再编码成URI,因此所有数据都可以用URI来表示,其标准化的描述就是dataURI(RFC 2397)。

dataURL的格式如下:

data:[<mediatype>][;base64],<data>

mediatype包括数据的MIME类型和字符编码等其它信息,base64表示使用了base64编码,data部分是经过URI转义的数据。尽管创建一个dataURL不难(前面已经叙述),读取dataURL则可能遇到很多复杂的情况(mediatype)。

mediatype  := [ type "/" subtype ] *( ";" parameter )
parameter  := attribute "=" value

目前看到的最靠谱的方式是用XHR

function dataURLtoBlob(dataUrl, callback) {
    var req = new XMLHttpRequest;

    req.open( 'GET', dataUrl );
    req.responseType = 'arraybuffer'; // Can't use blob directly because of https://crbug.com/412752

    req.onload = function fileLoaded(e) {
        // If you require the blob to have correct mime type
        var mime = this.getResponseHeader('content-type');

        callback(new Blob([this.response], {type: mime}));
    };

    req.send();
}

var url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="

dataURLtoBlob(url, function( blob ) {
    console.log( blob );
});

或者fetch API

var url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="

fetch(url)
.then(res => res.blob())
.then(blob => console.log(blob))

参考