浏览器中的二进制数据处理汇总

印象中第一次在前端中处理二进制是2017年JavaScript 解码 GIF 格式,提取GIF 图片中的每一帧图像数据。后面做了浏览器端二进制查看器,浏览器端图片处理,浏览器端图片转PDF,有必要对浏览器中的二进制数据处理汇总一下。

I. 读取本地文件

浏览器通常通过文件上传(input type="file"),文件拖放(drag and drop)来读取本地文件。

input.files[0]
event.dataTransfer.files[0]

处理本地文件中的数据,File 继承了 Blob

async function loadfile (that) {
  const file = that.files[0]
  console.log({ file: file })
  console.log('File.__proto__ === Blob', File.__proto__ === Blob) // File 接口基于 Blob,继承了 blob 的功能并将其扩展以支持用户系统上的文件。

  const reader = new FileReader()
  reader.readAsText(file, 'ascii') // 开始读取指定的Blob中的内容,载入内存。一旦完成,result属性中将包含一个字符串以表示所读取的文件内容, 默认编码utf-8。
  await new Promise(resolve => reader.onload = resolve)
  const raw = reader.result
  console.log('rawLength:', raw.length)
  console.log('头30个字符', raw.slice(0, 30))
  let hexdata = ''
  for (let char of raw.slice(0, 20)) {
    hexdata += char.charCodeAt().toString(16) + '-'
  }
  console.log('头20个字节', hexdata)

  reader.readAsArrayBuffer(file) // 开始读取指定的Blob中的内容。result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象
  await new Promise(resolve => reader.onload = resolve)
  const arraybuffer = reader.result
  console.log({ arraybuffer: arraybuffer })
  console.log('ArrayBuffer是一个二进制数据缓冲区,它表示固定长度的原始二进制数据,通常通过TypedArray和DataView对象进行访问和操作。')
  const typedArr = new Uint8Array(arraybuffer) // Uint8Array是一个TypedArray类型, typedArr.buffer 可以转化成ArrayBuffer
  console.log(typedArr)
  console.log('头20个字节', Array.from(typedArr.slice(0, 20)).map((byte) => byte.toString(16)))

  const dataview = new DataView(arraybuffer, 0); // dataview.buffer 可以转成ArrayBuffer
  console.log('dataview', dataview)
  console.log(dataview.getUint16(0, false).toString(16))
  console.log(dataview.getUint16(1, false).toString(16))

  reader.readAsDataURL(file) // 开始读取指定的Blob中的内容。result属性中将包含一个data: URL 格式的 Base64 字符串以表示所读取文件的内容
  await new Promise(resolve => reader.onload = resolve)
  var dataURL = reader.result; // data:image/jpeg;base64,/9j...
  console.log(dataURL.slice(0, 30))
}
有很多方法可以提取文件的二进制原始数据,还可以转化为Data URL 或 Blob URL。

File 继承 Blob,readAsArrayBuffer 也可以用来读 blob 数据,转化成 ArrayBuffer 等数据。

Blob to ArrayBuffer

const reader = new FileReader();
reader.readAsArrayBuffer(blob)
await new Promise(resolve => reader.onload = resolve)
arraybuffer = reader.result

II. HTTP 请求的二进制流

const response = await fetch('https://markbuild.com/wp-content/uploads/2023/03/blobURL.jpg', { responseType: 'arraybuffer' })
const arraybuffer = await response.arrayBuffer();

responseType 的值有:”arraybuffer” “blob” “document” “json” “text”

III. Data URL

浏览器端处理的文件,通常需要在浏览器端构造包含文件数据的链接,以 Data URL 或 Blob URL的形式下载。
Data URL 即前缀为 data: 协议的 URL。

Data URL 由四个部分组成:前缀 (data:)、指示数据类型的 MIME 类型、如果非文本则为可选的 base64 标记、数据本身。

因为我们是处理非文本的二制流,所以用的是 base64 数据。也就是将二进制文件流转化为 Base64,以Base64 长字符串的形式展示 。

以前也有下载注册表的需求,就是一个文本类型的Data URL: downloadlink.href = “data:text/reg;charset=UTF-8,”+data

几种不同数据转化成 DataURL

dataURL = canvas.toDataURL('image/jpeg') // Canvas 转化成DataURL
dataURL = canvas.toDataURL('image/png')

reader.readAsDataURL(file)
await new Promise(resolve => reader.onload = resolve)
dataURL = reader.result

dataURL = "data:image/gif;base64,"+ window.btoa(data)

DataURL 的下载

const downloadlink = document.getElementById('download') 
downloadlink.href = dataurl;
downloadlink.click();

iOS(iPhone) Safari 下 href=”data:” 无法自动下载,因为该浏览器无法处理通过Data URL 下载文件,可以尝试使用Blob URL 替代 Data URL 来解决这个问题,但 Blob URL 在某些老浏览器和设备上不受支持。

DataURL的数据提取

var parts = dataUrl.split(';base64,')
var raw = window.atob(parts[1])

var rawLength = raw.length;
// Unit8Array 转化成 blob
var uInt8Array = new Uint8Array(rawLength);
for (var i = 0; i < rawLength; ++i) {
  uInt8Array[i] = raw.charCodeAt(i);
}
var contentType = parts[0].split(':')[1]
blob = new Blob([uInt8Array], {type: contentType}); 

IV. Blob URL

Blob URL 即前缀为 blob: 协议的 URL。Blob URL 的形式为 blob : <unique-identifier>,其中 <unique-identifier> 是一个随机生成的唯一标识符,用于表示 Blob 对象。

Blob URL 和 Data URL 的主要区别在于,Blob URL 使用 Blob 对象表示数据或文件,而 Data URL 使用 Base64 编码的字符串表示数据或文件。因此,Blob URL 通常用于大文件或二进制数据,而 Data URL 通常用于小文件或文本数据。

几种不同数据转化成Blob URL

// Unit8Array to Blob
const blob = new Blob([uint8Array], {type: 'application/pdf'});

// ArrayBuffer to Blob
const blob = new Blob([arraybuffer], { type: "image/jpg" })
// Blob to Blob URL
const url = URL.createObjectURL(blob);

// canvas to Blob URL
canvas.toBlob(function(blob) {
  var url = URL.createObjectURL(blob);
}, 'image/jpeg', 0.95);

Blob URL数据提取,通过 Fetch API

V. Canvas

有时候需要借助 Canvas 来处理图像,也就是让浏览器本身的特性去帮助我们。毕竟去解码 JPG/PNG/WEBP 等图像会很复杂,解码的代码会很多,解码处理完数据,可能还要编码成对应格式。
通常是将图像数据绘制在 Canvas 上, 然后处理,最后将 Canvas 上的数据转变成 Data URL 以便下载。

const image = new Image();
image.src = url;
await new Promise(resolve => image.onload = resolve);
const canvas = document.createElement('canvas');
canvas.width = option.width;
canvas.height = option.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
// todo 这里可以对画布上的数据进行处理
// 可以生成 Data URL
const dataUrl = canvas.toDataURL('image/jpeg');
// 也可以生成 Blob URL
blob = await new Promise(resolve => {
  canvas.toBlob(blob => resolve(blob), 'image/jpeg', '0.95');
});
const blobUrl = URL.createObjectURL(blob);

下面是Canvas 数据的处理,我们可以修改 Canvas 上的每个像素点的颜色值。

下面是一个将画布图像转化成灰度颜色(红绿蓝三基色亮度一致)处理。

var imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);  
var data=imageData.data;
for(i=0;i<data.length;i+=4) {
  data[i]=data[i+1]=data[i+2]=parseInt(0.2989 * data[i] + 0.5870 * data[i+1] + 0.1140 * data[i+2]);
  data[i+3]=data[i+3];
}
ctx.putImageData(imageData,0,0);

简单的几行代码就可以完成图像的灰度转化,如果是要去解码 JPG,提取每个像素的颜色,转化成新的色彩表,然后又将数据编码成 JPG,代码量是非常大的。Canvas 是在浏览器端处理图像的好工具。

VI. ArrayBuffer TypedArray 和 DataView

ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区,他存的是实际数据而不是指针,无法修改长度,所以比 Array 更省内存。

二进制数组就是在这种背景下诞生的。它很像 C 语言的数组,允许开发者以数组下标的形式,直接操作内存,大大增强了 JavaScript 处理二进制数据的能力,使得开发者有可能通过 JavaScript 与操作系统的原生接口进行二进制通信。

你不能直接操作 ArrayBuffer 中的内容;而要通过Typed ArrayDataView 对象来操作它。

比如你创建一个ArrayBuffer。arrayBuffer = new ArrayBuffer(10),它申请占用了 8 个字节的固定长度空间开销,它的长度不可更改,但你可以基于它通过slice() 来创建另一个ArrayBuffer。

DataView 的constructor 只能是一个ArrayBuffer,TypedArray 可以是数组。