# 前端二进制文件处理

# 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,用来操作文件。比如如下的对象

  1. File对象:负责处理那些以文件形式存在的二进制数据,也就是操作本地文件;
  2. FileList对象:File对象的网页表单接口;
  3. FileReader对象:负责将二进制数据读入内存内容;
  4. 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对象有两个只读属性:

  1. size:二进制数据的大小,单位为字节。
  2. 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 已**废弃**, 不建议使用。使用 encodeURIComponentencodeURI代替。

encodeURIencodeURIComponent方法都可以对URI(通用资源标识符)进行编码,以便发送给浏览器。 但它们编码的范围有所不用。

  1. encodeURI()方法不会对下列字符编码:ASCII字母、数字、~!@#$&*()=:/,;?+'
  2. encodeURIComponent()方法不会对下列字符编码:ASCII字母、数字、~!*()'

使用场景

  1. 整个URL 进行编码就使用encodeURI()
  2. 需要对URL中的参数 或者 URL后面的一部分 进行编码就使用encodeURIComponent()

对应的有decodeURIdecodeURIComponent, 这个就不过多的介绍了。

# btoa、atob

btoaString (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的全局类型以及构造函数。 但是又许多其他细化的全局构造函数,比如Uint8ArrayInt8Array

# 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

上次更新: 1/22/2025, 9:39:13 AM