# 前端二进制文件处理
# Unicode
从unicode说起。
比如我们说的UTF-8
到底是什么意思?
Unicode是将全世界所有的字符包含在一个集合里面, 计算机只要支持整一个集合,就能显示所有的符号, 再也不会乱码了。
目前,Unicode的最新版本是7.0版,一共收入了109449个符号。
但是Unicode 只规定了每个字符的码点,比如就 U + 0000
表示null符号。至于到底需要什么样子的字节序表示这个码点,就涉及到编码方法。
最简单的编码方式就是UTF-32
就是用32位,也就是4个字节表示一个码点。这种编码方式很直观, 但是有个问题就是非常浪费空间。但是我们需要的是一个真正节省空间的编码方法,所以UTF-8诞生了。
UTF-8是一个变长的编码方法,字符长度从1个字节到4个字节不等。越是常用的字符,字节越短,最前面的128个字符,只使用一个字节表示, 与ASCII码完全相同。
UTF-16编码介于UTF-32和UTF-8之间。同时结合了定长和变长两种编码方式的优点。规则就是基本平面(看参考内容)的字符占用2个字节,辅助平面的字符占用4个字节。
于是就有一个问题,当我们遇到两个字节,怎么看出它本身是一个字符,还是需要跟其他两个字节放在一起解读?
说来很巧妙,我也不知道是不是故意的设计,在基本平面内,从U+D800到U+DFFF是一个空段,即这些码点不对应任何字符。因此,这个空段可以用来映射辅助平面的字符。
具体来说,辅助平面的字符位共有2^20个,也就是说,对应这些字符至少需要20个二进制位。UTF-16将这20位拆成两半,前10位映射在U+D800到U+DBFF(空间大小2^10),称为高位(H),后10位映射在U+DC00到U+DFFF(空间大小2^10),称为低位(L)。这意味着,一个辅助平面的字符,被拆成两个基本平面的字符表示。
了解到这里就足够了, 我们只要知道字符需要字符集来规定码点, 用时利用编码方法 来表示字节序, 而方法有很多。具体怎么展示也不用知道的那么清楚。
# 文件和二进制数据的操作
历史上,JavaScript无法处理二进制数据。如果一定要处理的话,只能使用charCodeAt()方法,一个个字节地从文字编码转成二进制数据,还有一种办法是将二进制数据转成Base64编码,再进行处理。这两种方法不仅速度慢,而且容易出错。ECMAScript 5引入了Blob对象,允许直接操作二进制数据。
所有Blob对象是用于操作二进制数据的。Blob对象是一个代表二进制数据的基本对象,在它的基础上,又衍生出一系列相关的API,用来操作文件。比如如下的对象
- File对象:负责处理那些以文件形式存在的二进制数据,也就是操作本地文件;
- FileList对象:File对象的网页表单接口;
- FileReader对象:负责将二进制数据读入内存内容;
- URL对象:用于对二进制数据生成URL。
# Blob对象
Blob(Binary Large Object)对象代表了一段二进制数据,提供了一系列操作接口。其他操作二进制数据的API(比如File对象),都是建立在Blob对象基础上的,继承了它的属性和方法。
生成Blob对象有两种方法:一种是使用Blob构造函数,另一种是对现有的Blob对象使用slice方法切出一部分。
(1)Blob构造函数,接受两个参数。第一个参数是一个包含实际数据的数组,第二个参数是数据的类型,这两个参数都不是必需的。举个例子
const blob = new Blob(['hello, world'], { type: 'text/plain' });
const a = document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.download = 'hello-world.txt';
a.textContent = 'download txt';
document.body.appendChild(a);
// URL.revokeObjectURL(url);
这个就是利用Blob对象,生成可下载文件的例子。
(2)Blob对象的slice方法,将二进制数据按照字节分块,返回一个新的Blob对象。 这个可以用于将大文件分割上传。
(3)Blob对象有两个只读属性:
- size:二进制数据的大小,单位为字节。
- type:二进制数据的MIME类型,全部为小写,如果类型未知,则该值为空字符串。
在Ajax操作中,如果xhr.responseType设为blob,接收的就是二进制数据。
# FileList对象
FileList
对象针对表单的file控件。当用户通过file控件选取文件后,这个控件的files属性值就是FileList对象。它在结构上类似于数组,包含用户选取的多个文件。
<input type="file" id="input" onchange="console.log(this.files.length)" multiple />
采用拖拽的方式也可以获取到FileList对象
const dropZone = document.querySelector('#wrapper');
dropZone.addEventListener(
'dragenter',
function (e) {
e.preventDefault();
e.stopPropagation();
},
false
);
dropZone.addEventListener(
'dragover',
function (e) {
e.preventDefault();
e.stopPropagation();
},
false
);
dropZone.addEventListener(
'dragleave',
function (e) {
e.preventDefault();
e.stopPropagation();
},
false
);
dropZone.addEventListener(
'drop',
function (e) {
e.preventDefault();
e.stopPropagation();
console.log(e.dataTransfer.files);
const file = e.dataTransfer.files[0];
console.log(file);
},
false
);
# File API
File API提供File
对象,它是FileList
对象的成员,包含了文件的一些元信息,比如文件名、上次改动时间、文件大小和文件类型。
{
lastModified: 1629772183149
lastModifiedDate: Tue Aug 24 2021 10:29:43 GMT+0800 (中国标准时间) {}
name: "0.png"
size: 7560
type: "image/png"
webkitRelativePath: ""
}
name: 文件名,该属性只读。
size: 文件大小,单位为字节,该属性只读。
type: 文件的MIME类型,如果分辨不出类型,则为空字符串,该属性只读。
lastModified: 文件的上次修改时间,格式为时间戳。
lastModifiedDate:文件的上次修改时间,格式为Date
对象实例。
# FileReader API
FileReader API用于读取文件,即把文件内容读入内存。它的参数是File
对象或Blob
对象。
对于不同类型的文件,FileReader提供不同的方法读取文件。
readAsBinaryString(Blob|File)
:返回二进制字符串,该字符串每个字节包含一个0到255之间的整数。readAsText(Blob|File, opt_encoding)
:返回文本字符串。默认情况下,文本编码格式是’UTF-8’,可以通过可选的格式参数,指定其他编码格式的文本。readAsDataURL(Blob|File)
:返回结果包含一个data:
URL格式的字符串(base64编码)以表示所读取文件的内容。它的作用基本上是将文件数据进行Base64编码。你可以将返回值设为图像的src
属性。readAsArrayBuffer(Blob|File)
:返回一个ArrayBuffer对象。即固定长度的二进制缓存数据。在文件操作时(比如将JPEG图像转为PNG图像),这个方法非常方便。abort()
: 用于中止文件上传。
const blob = new Blob(['hello, world'], { type: 'text/plain' });
const reader = new FileReader();
// const result =
// console.log(result);
reader.onloadend = function () {
console.log(reader.result);
console.log(reader.readyState); // 2
console.log(FileReader.DONE); // 2
};
// reader.readAsBinaryString(blob); // hello, world
// reader.readAsText(blob, 'UTF-8'); // hello, world
// reader.readAsDataURL(blob); // data:text/plain;base64,aGVsbG8sIHdvcmxk
reader.readAsArrayBuffer(blob); // ArrayBuffer
console.log(reader.readyState); // 1
console.log(FileReader.LOADING); // 1
// 读取上传的图片预览
const fileInput = document.querySelector('#fileInput');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
if (file.type.indexOf('image') > -1) {
const reader = new FileReader();
reader.onload = function () {
const dataURL = reader.result;
const img = new Image();
img.src = dataURL;
document.body.appendChild(img);
};
reader.readAsDataURL(file);
}
}
});
FileReader对象采用异步方式读取文件,可以为一系列事件指定回调函数。
- onabort方法:读取中断或调用reader.abort()方法时触发。
- onerror方法:读取出错时触发。
- onload方法:读取成功后触发。
- onloadend方法:读取完成后触发,不管是否成功。触发顺序排在 onload 或 onerror 后面。
- onloadstart方法:读取将要开始时触发。
- onprogress方法:读取过程中周期性触发。
可以利用FileReader获取剪切板中的图片
document.onpaste = function (e) {
e.preventDefault();
if (e.clipboardData && e.clipboardData.items) {
// pasted image
for (var i = 0, items = e.clipboardData.items; i < items.length; i++) {
if (items[i].kind === 'file' && items[i].type.match(/^image/)) {
readFile(items[i].getAsFile(), dataUrl => {
const img = new Image();
img.src = dataUrl;
document.body.appendChild(img);
});
break;
}
}
}
return false;
};
function readFile(file, applyDataUrlTo) {
var reader = new FileReader();
reader.onload = function (e) {
applyDataUrlTo(reader.result);
};
reader.readAsDataURL(file);
}
# window.URL对象
URL对象用于生成指向File对象或Blob对象的URL。
var objectURL = window.URL.createObjectURL(object);
参数object
:用于创建 URL 的 File
(opens new window) 对象、Blob
(opens new window) 对象或者 MediaSource
(opens new window) 对象。
上面的代码会对二进制数据生成一个URL,类似于“blob:http%3A//test.com/666e6730-f45c-47c1-8012-ccc706f17191”。这个URL可以放置于任何通常可以放置URL的地方,比如img标签的src属性,a标签的href属性。需要注意的是,即使是同样的二进制数据,每调用一次URL.createObjectURL方法,就会得到一个不一样的URL。
这个URL的存在时间,等同于网页的存在时间,一旦网页刷新或卸载,这个URL就失效。除此之外,也可以手动调用URL.revokeObjectURL
方法,使URL失效。
window.URL.revokeObjectURL(objectURL);
下面是利用URL对象把图片文件插入到文档的例子
const fileInput = document.querySelector('#fileInput');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
if (file.type.indexOf('image') > -1) {
const img = new Image();
img.src = window.URL.createObjectURL(file);
img.onload = function () {
window.URL.revokeObjectURL(this.src);
};
// <img src="blob:null/f7964f31-d4dd-4b90-ba85-fa03ada5b9e9">
document.body.appendChild(img);
}
}
});
下面是一个本机视频上传预览的例子
const fileInput = document.querySelector('#fileInput');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
if (file.type.indexOf('video') > -1) {
const video = document.createElement('video');
video.setAttribute('controls', 'controls');
video.style.width = '200px';
const videoUrl = window.URL.createObjectURL(file);
video.src = videoUrl;
video.play();
document.body.appendChild(video);
}
}
});
# Canvas相关
比较常见的把canvas绘制的内容生成图片上传保存,比如一些在线签字案例。
Canvas API主要聚焦于2D图形。而同样使用<canvas>
元素的 WebGL API (opens new window) 则用于绘制硬件加速的2D和3D图形。
DataURL:在FileReader里面也有讲到,DataURL是一个包含data:
URL格式的字符串(base64编码),用以表示所读取文件的内容。一般意思就是把文件数据进行Base64编码的格式。
下面是两个DataURL的实例内容
1、这是一个文本的,内容是hello, world
data:text/plain;base64,aGVsbG8sIHdvcmxk
2、下面是一个图片的,是一张480字节的图片
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAKCAYAAABi8KSDAAABP2lDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSCwoyGFhYGDIzSspCnJ3UoiIjFJgf8rAycDPwMMgxKCRmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsgsnfjTOtf5ntqe+OGXeVut4jimehTAlZJanAyk/wBxYnJBUQkDA2MCkK1cXlIAYrcA2SJFQEcB2TNA7HQIew2InQRhHwCrCQlyBrKvANkCyRmJKUD2EyBbJwlJPB2JDbUXBNg9AhSMTMoJuJQMUJJaUQKinfMLKosy0zNKFByBIZSq4JmXrKejYGRgZMjAAApviOrPN8DhyCjGgRArPsPAYH8OyMhGiEVuZ2BY/YGBgWcjQkwzGBgETgwMxwQKEosS4Q5g/MZSnGZsBGFzA/WxTvv//3M40MuaDAx/r////3v7//9/lzEwMN9iYDjwDQDhjV9N7fW04wAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAC6ADAAQAAAABAAAACgAAAACfFlGMAAAAHUlEQVQYGWP8/uPHfwYiAROR6sDKRhUjhxbtQgMA5DED+hgegtkAAAAASUVORK5CYII=
可以看到基本的格式就是 data:mime;base64,xxxxx
DataURL可以转换为Blob,用如下的函数可以进行转换
function dataURLtoBlob(dataurl) {
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
}
canvas.toDataURL
canvas.toDataURL(type, encoderOptions);
方法返回一个包含图片展示的DataURL。可以使用 type
参数指示类型,默认为PNG
格式。图片的分辨率为96dpi。
type
可选图片格式,默认为
image/png
encoderOptions
可选在指定图片格式为
image/jpeg 或
image/webp的情况下,可以从 0 到 1 的区间内选择图片的质量
。如果超出取值范围,将会使用默认值0.92
。其他参数会被忽略。
如下示例
const imgDataURL = canvasElement.toDataURL('image/png');
const blob = dataURLtoBlob(imgDataURL);
const formData = new FormData();
formData.append('file', blob, 'path.png');
利用 dataURLtoBlob
可以把base64的内容转换为Blob对象。Blob对象可以添加到FormData对象作为请求体传递给后端,完成图片文件上传**。如果使用formData.append
blob数据, 最好设置第三个参数,一般是文件名。**
还有个辅助函数,介绍一下,比如可以把base64后的图片数据进行旋转。
rotateBase64Img(src, edg, callback)
src为base64之后的图片数据, edg是角度,比如-90
度既是逆时针旋转90度, callback回调函数的参数为旋转之后的base64图片数据
function rotateBase64Img(src, edg, callback) {
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var imgW; //图片宽度
var imgH; //图片高度
var size; //canvas初始大小
if (edg % 90 != 0) {
console.error('旋转角度必须是90的倍数!');
throw '旋转角度必须是90的倍数!';
}
edg < 0 && (edg = (edg % 360) + 360);
const quadrant = (edg / 90) % 4; //旋转象限
const cutCoor = { sx: 0, sy: 0, ex: 0, ey: 0 }; //裁剪坐标
var image = new Image();
image.crossOrigin = 'anonymous';
image.src = src;
image.onload = function() {
imgW = image.width;
imgH = image.height;
size = imgW > imgH ? imgW : imgH;
canvas.width = size * 2;
canvas.height = size * 2;
switch (quadrant) {
case 0:
cutCoor.sx = size;
cutCoor.sy = size;
cutCoor.ex = size + imgW;
cutCoor.ey = size + imgH;
break;
case 1:
cutCoor.sx = size - imgH;
cutCoor.sy = size;
cutCoor.ex = size;
cutCoor.ey = size + imgW;
break;
case 2:
cutCoor.sx = size - imgW;
cutCoor.sy = size - imgH;
cutCoor.ex = size;
cutCoor.ey = size;
break;
case 3:
cutCoor.sx = size;
cutCoor.sy = size - imgW;
cutCoor.ex = size + imgH;
cutCoor.ey = size + imgW;
break;
}
ctx.translate(size, size);
ctx.rotate((edg * Math.PI) / 180);
ctx.drawImage(image, 0, 0);
var imgData = ctx.getImageData(
cutCoor.sx,
cutCoor.sy,
cutCoor.ex,
cutCoor.ey
);
if (quadrant % 2 == 0) {
canvas.width = imgW;
canvas.height = imgH;
} else {
canvas.width = imgH;
canvas.height = imgW;
}
ctx.putImageData(imgData, 0, 0);
callback(canvas.toDataURL('image/png'));
};
}
# escape、encodeURIComponent、encodeURI
escape 已**废弃**, 不建议使用。使用 encodeURIComponent
、encodeURI
代替。
encodeURI
和encodeURIComponent
方法都可以对URI(通用资源标识符)进行编码,以便发送给浏览器。
但它们编码的范围有所不用。
- encodeURI()方法不会对下列字符编码:ASCII字母、数字、~!@#$&*()=:/,;?+'
- encodeURIComponent()方法不会对下列字符编码:ASCII字母、数字、~!*()'
使用场景
- 对 整个URL 进行编码就使用encodeURI()
- 需要对URL中的参数 或者 URL后面的一部分 进行编码就使用encodeURIComponent()
对应的有decodeURI
、decodeURIComponent
, 这个就不过多的介绍了。
# btoa、atob
btoa
从 String
(opens new window) 对象中创建一个 base-64 编码的 ASCII 字符串,其中字符串中的每个字符都被视为一个二进制数据字节。binary to ascii
atob
对经过 base-64 编码的字符串进行解码。ascii to binary
这里的b指二进制数据字节
let encodedData = window.btoa('Hello, world'); // 编码
let decodedData = window.atob(encodedData); // 解码
更多细节可以查看 https://github.com/dankogai/js-base64
下面两个函数也可以进行base64的编解码操作, 返回的都是String字符串格式。
let base64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let base64DecodeChars = new Array(
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
-1, 0, 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, -1, -1, -1, -1, -1,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1);
export function base64encode(str) {
let out, i, len;
let c1, c2, c3;
len = str.length;
i = 0;
out = "";
while(i < len) {
c1 = str.charCodeAt(i++) & 0xff;
if(i == len)
{
out += base64EncodeChars.charAt(c1 >> 2);
out += base64EncodeChars.charAt((c1 & 0x3) << 4);
out += "==";
break;
}
c2 = str.charCodeAt(i++);
if(i == len)
{
out += base64EncodeChars.charAt(c1 >> 2);
out += base64EncodeChars.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4));
out += base64EncodeChars.charAt((c2 & 0xF) << 2);
out += "=";
break;
}
c3 = str.charCodeAt(i++);
out += base64EncodeChars.charAt(c1 >> 2);
out += base64EncodeChars.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4));
out += base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >>6));
out += base64EncodeChars.charAt(c3 & 0x3F);
}
return out;
}
export function base64decode(str) {
let c1, c2, c3, c4;
let i, len, out;
len = str.length;
i = 0;
out = "";
while(i < len) {
/* c1 */
do {
c1 = base64DecodeChars[str.charCodeAt(i++) & 0xff];
} while(i < len && c1 == -1);
if(c1 == -1)
break;
/* c2 */
do {
c2 = base64DecodeChars[str.charCodeAt(i++) & 0xff];
} while(i < len && c2 == -1);
if(c2 == -1)
break;
out += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4));
/* c3 */
do {
c3 = str.charCodeAt(i++) & 0xff;
if(c3 == 61)
return out;
c3 = base64DecodeChars[c3];
} while(i < len && c3 == -1);
if(c3 == -1)
break;
out += String.fromCharCode(((c2 & 0XF) << 4) | ((c3 & 0x3C) >> 2));
/* c4 */
do {
c4 = str.charCodeAt(i++) & 0xff;
if(c4 == 61)
return out;
c4 = base64DecodeChars[c4];
} while(i < len && c4 == -1);
if(c4 == -1)
break;
out += String.fromCharCode(((c3 & 0x03) << 6) | c4);
}
return out;
}
# ArrayBuffer
ArrayBuffer
对象表示一段二进制数据, 用来模拟内存里面的数据。 通过这个对象, js可以读写二进制数据, 这个对象可以看做是内存对象的表达。
这个对象是 ES6 才写入标准的,普通的网页编程用不到它
浏览器原生提供ArrayBuffer()
构造函数,用来生成实例。它接受一个整数作为参数,表示这段二进制数据占用多少个字节。
const buffer = new ArrayBuffer(8)
上面的代码中, buffer
占用8个字节。ArrayBuffer
对象有 byteLength
属性, 用于表示实例占用的内存长度。
console.log(buffer.byteLength); // 8
ArrayBuffer 对象有实例方法slice()
,用来复制一部分内存。它接受两个整数参数,分别表示复制的开始位置(从0开始)和结束位置(复制时不包括结束位置),如果省略第二个参数,则表示一直复制到结束。
var buf1 = new ArrayBuffer(8);
var buf2 = buf1.slice(0);
上面的代码表示复制原来的实例。
# TypedArray
一个类型化数组对象描述了一个底层的**二进制数据缓冲区**(binary data buffer)的一个类数组视图。实际上, 没有名为 TypedArray
的全局类型以及构造函数。 但是又许多其他细化的全局构造函数,比如Uint8Array
、Int8Array
。
# Uint8Array
Uint8Array
表示一个8位无符号整形数组,创建时内容位0,创建完成后, 可以以对象的方式或者数组下标的方式引用数组中的元素。
const array = new Uint8Array(2);
console.log(array.length); // 2
array[0] = 257;
console.log(array[0]); // 1
无符号的只能正确存储0-255的数值,超出这个数值就不能正确存储。Int8Array
作为有符号的, 第一位是符号位。只能存储-128-127的数值。
Uint16Array
Uint32Array
都是同理, 只不过占16位和32位。
不过这些东西暂时没想到有什么特别大的用处, 平时实际用的应该比较少, 下面的 dataURLtoBlob
有用到一点。或者可以用Uint8Array
来存储RGB色彩, 但是也没什么必要。
参考内容
https://www.ruanyifeng.com/blog/2014/12/unicode.html
http://javascript.ruanyifeng.com/htmlapi/file.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypedArray