本文阿宝哥将按照如下的流程来介绍前端如何进行图片处理,而后穿插介绍二进制、Blob、Blob URL、Base6四、Data URL、ArrayBuffer、TypedArray、DataView 和图片压缩相关的知识点。javascript
阅读完本文,小伙伴们将能轻松看懂如下转换关系图:html
还在犹豫什么?跟上阿宝哥的脚步,让咱们一块儿来玩转前端二进制。请小伙伴们原谅阿宝哥的 “自恋”,在后面的示例中,咱们将使用阿宝哥的我的头像做为演示素材。前端
阅读阿宝哥近期热门文章(感谢掘友的鼓励与支持🌹🌹🌹):java
好的,如今咱们开始来进入第一个环节:「选择本地图片 -> 图片预览」。node
在支持 FileReader API 的浏览器中,咱们也能够利用该 API 方便实现图片本地预览功能。git
(图片来源:https://caniuse.com/#search=filereader)github
由上图可知,该 API 兼容性较好,咱们能够放心使用。这里阿宝哥就不展开详细介绍 FileReader API,咱们直接来看一下利用它如何实现本地图片预览,具体代码以下:web
<!DOCTYPE html>
<html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>图片本地预览示例</title> </head> <body> <h3>阿宝哥:图片本地预览示例</h3> <input type="file" accept="image/*" onchange="loadFile(event)" /> <img id="previewContainer" /> <script> const loadFile = function (event) { const reader = new FileReader(); reader.onload = function () { const output = document.querySelector("#previewContainer"); output.src = reader.result; }; reader.readAsDataURL(event.target.files[0]); }; </script> </body> </html> 复制代码
在以上示例中,咱们为 file
类型的输入框绑定 onchange
事件处理函数 loadFile
,在该函数中,咱们建立了一个 FileReader 对象并为该对象绑定 onload
相应的事件处理函数,而后调用 FileReader 对象的 readAsDataURL()
方法,把本地图片对应的 File 对象转换为 Data URL。算法
❝其实对于 FileReader 对象来讲,除了支持把 File/Blob 对象转换为 Data URL 以外,它还提供了
❞readAsArrayBuffer()
和readAsText()
方法,用于把 File/Blob 对象转换为其它的数据格式。typescript
当文件读取完成后,会触发绑定的 onload
事件处理函数,在该处理函数内部会把获取 Data URL 数据赋给 img
元素的 src
属性,从而实现图片本地预览。
经过使用 Chrome 开发者工具,咱们能够在 Elements
面板中看到 Data URL 的 「“芳容”」:
在图中右侧的绿色框中,咱们能够清楚的看到 img
元素 src
属性值是一串很是 「奇怪」 的字符串:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAhAAAAIwCAYAAADXrFK...
复制代码
其实,这串奇怪的字符串被称为 Data URL,它由四个部分组成:前缀(data:
)、指示数据类型的 MIME 类型、若是非文本则为可选的 base64
标记、数据自己:
data:[<mediatype>][;base64],<data>
复制代码
mediatype
是个 MIME 类型的字符串,好比 "image/png
" 表示 PNG 图像文件。若是被省略,则默认值为 text/plain;charset=US-ASCII
。若是数据是文本类型,你能够直接将文本嵌入(根据文档类型,使用合适的实体字符或转义字符)。若是是二进制数据,你能够将数据进行 base64 编码以后再进行嵌入。
❝MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型,是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。
常见的 MIME 类型有:超文本标记语言文本 .html text/html、PNG 图像 .png image/png、普通文本 .txt text/plain 等。
❞
在 Web 项目开发过程当中,为了减小 HTTP 请求的数量,对应一些较小的图标,咱们一般会考虑使用 Data URL 的形式内嵌到 HTML 或 CSS 文件中。 「但须要注意的是:若是图片较大,图片的色彩层次比较丰富,则不适合使用这种方式,由于该图片通过 base64 编码后的字符串很是大,会明显增大 HTML 页面的大小,从而影响加载速度。」
在 Data URL 中,数据是很重要的一部分,它使用 base64 编码的字符串来表示。所以要掌握 Data URL,咱们还得了解一下 Base64。
「Base64」 是一种基于 64 个可打印字符来表示二进制数据的表示方法。因为 「2⁶ = 64」 ,因此每 6 个比特为一个单元,对应某个可打印字符。3 个字节有 24 个比特,对应于 4 个 base64 单元,即 3 个字节可由 4 个可打印字符来表示。相应的转换过程以下图所示:
「Base64 经常使用于在处理文本数据的场合,表示、传输、存储一些二进制数据,包括 MIME 的电子邮件及 XML 的一些复杂数据。」 在 MIME 格式的电子邮件中,base64 能够用来将二进制的字节序列数据编码成 ASCII 字符序列构成的文本。使用时,在传输编码方式中指定 base64。使用的字符包括大小写拉丁字母各 26 个、数字 10 个、加号 + 和斜杠 /,共 64 个字符,等号 = 用来做为后缀用途。
Base64 相应的索引表以下:
了解完上述的知识,咱们以编码 Man
为例,来直观的感觉一下编码过程。Man
由 M、a 和 n 这 3 个字符组成,它们对应的 ASCII 码为 7七、97 和 110。
接着咱们以每 6 个比特为一个单元,进行 base64 编码操做,具体以下图所示:
由图可知,Man
(3 字节)编码的结果为 TWFu
(4 字节),很明显通过 base64 编码后体积会增长 1/3。Man
这个字符串的长度恰好是 3,咱们能够用 4 个 base64 单元来表示。但若是待编码的字符串长度不是 3 的整数倍时,应该如何处理呢?
「若是要编码的字节数不能被 3 整除,最后会多出 1 个或 2 个字节,那么可使用下面的方法进行处理:先使用 0 字节值在末尾补足,使其可以被 3 整除,而后再进行 base64 的编码。」
以编码字符 A 为例,其所占的字节数为 1,不能被 3 整除,须要补 2 个字节,具体以下图所示:
由上图可知,字符 A 通过 base64 编码后的结果是 QQ==
,该结果后面的两个 =
表明补足的字节数。而最后个 1 个 base64 字节块有 4 位是 0 值。
接着咱们来看另外一个示例,假设需编码的字符串为 BC
,其所占字节数为 2,不能被 3 整除,须要补 1 个字节,具体以下图所示:
由上图可知,字符串 BC 通过 base64 编码后的结果是 QkM=
,该结果后面的 1 个 =
表明补足的字节数。而最后个 1 个 base64 字节块有 2 位是 0 值。
在 JavaScript 中,有两个函数被分别用来处理解码和编码 base64 字符串:
const name = 'Semlinker';
const encodedName = btoa(name); console.log(encodedName); // U2VtbGlua2Vy 复制代码
const encodedName = 'U2VtbGlua2Vy';
const name = atob(encodedName); console.log(name); // Semlinker 复制代码
对于 atob 和 btoa 这两个方法来讲,其中的 a 表明 ASCII,而 b 表明 Blob,即二进制。所以 atob 表示 ASCII 到二进制,对应的是解码操做。而 btoa 表示二进制到 ASCII,对应的是编码操做。在了解方法中 a 和 b 分别表明的意义以后,在之后的工做中,咱们就不会用错了。
相信看到这里,小伙伴们对 base64 已经有必定的了解。须要注意的是 base64 只是一种数据编码方式,目的是为了保障数据的安全传输。但标准的 base64 编码无需额外的信息,便可以进行解码,是彻底可逆的。所以在涉及传输私密数据时,并不能直接使用 base64 编码,而是要使用专门的对称或非对称加密算法。
除了能够从本地获取图片以外,咱们也可使用 fetch API 从网络上获取图片,而后在进行图片预览。固然对于网络上可正常访问的图片地址,咱们能够直接把地址赋给 img
元素,并不须要经过 fetch API 绕一大圈。若在显示图片时,你须要对图片进行特殊处理,好比解密图片数据时,你就能够考虑在 Web Worker 中使用 fetch API 获取图片数据并进行解密操做。
简单起见,咱们不考虑特殊的场景。首先咱们先来看一下 fetch API 的兼容性:
(图片来源:https://caniuse.com/#search=fetch)
而后咱们使用 fetch API 从 Github 上获取阿宝哥的头像,具体代码以下所示:
<!DOCTYPE html>
<html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>获取远程图片预览示例</title> </head> <body> <h3>阿宝哥:获取远程图片预览示例</h3> <img id="previewContainer" style="width: 50%;"/> <script> const image = document.querySelector("#previewContainer"); fetch("https://avatars3.githubusercontent.com/u/4220799") .then((response) => response.blob()) .then((blob) => { const objectURL = URL.createObjectURL(blob); image.src = objectURL; }); </script> </body> </html> 复制代码
在以上示例中,咱们经过 fetch API 从 Github 上下载阿宝哥的头像,当请求成功后,把响应对象(Response)转换为 Blob 对象,而后使用 URL.createObjectURL
方法,建立 Object URL 并把它赋给 img
元素的 src
属性,从而实现图片的显示。
经过使用 Chrome 开发者工具,咱们能够在 Elements
面板中看到 Object URL 的 「“芳容”」:
在图中右侧的绿色框中,咱们能够清楚的看到 img
元素 src
属性值是一串很是 「特殊」 的字符串:
blob:null/ab24c171-1c5f-4de1-a44e-568bc1f77d7b
复制代码
以上的特殊字符串,咱们称之为 「Object URL」,相比前面介绍的 Data URL,它简洁不少。接下来咱们来认识一下 Object URL 这种协议。
Object URL 是一种伪协议,也被称为 Blob URL。它容许 Blob 或 File 对象用做图像,下载二进制数据连接等的 URL 源。在浏览器中,咱们使用 URL.createObjectURL
方法来建立 Blob URL,该方法接收一个 Blob
对象,并为其建立一个惟一的 URL,其形式为 blob:<origin>/<uuid>
,对应的示例以下:
blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641
复制代码
浏览器内部为每一个经过 URL.createObjectURL
生成的 URL 存储了一个 「URL → Blob」 映射。所以,此类 URL 较短,但能够访问 Blob
。生成的 URL 仅在当前文档打开的状态下才有效。但若是你访问的 Blob URL 再也不存在,则会从浏览器中收到 404 错误。
上述的 Blob URL 看似很不错,但实际上它也有反作用。虽然存储了 URL → Blob 的映射,但 Blob 自己仍驻留在内存中,浏览器没法释放它。映射在文档卸载时自动清除,所以 Blob 对象随后被释放。可是,若是应用程序寿命很长,那不会很快发生。所以,若是咱们建立一个 Blob URL,即便再也不须要该 Blob,它也会存在内存中。
针对这个问题,咱们能够调用 URL.revokeObjectURL(url)
方法,从内部映射中删除引用,从而容许删除 Blob(若是没有其余引用),并释放内存。
既然讲到了 Blob URL,不得不提 Blob。那么什么是 Blob 呢?咱们继续往下看。
Blob(Binary Large Object)表示二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个单一个体的集合。Blob 一般是影像、声音或多媒体文件。「在 JavaScript 中 Blob 类型的对象表示不可变的相似文件对象的原始数据。」 为了更直观的感觉 Blob 对象,咱们先来使用 Blob 构造函数,建立一个 myBlob 对象,具体以下图所示:
如你所见,myBlob 对象含有两个属性:size 和 type。其中 size
属性用于表示数据的大小(以字节为单位),type
是 MIME 类型的字符串。Blob 表示的不必定是 JavaScript 原生格式的数据。好比 File
接口基于 Blob
,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。
Blob
由一个可选的字符串 type
(一般是 MIME 类型)和 blobParts
组成:
❝MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型,是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。
常见的 MIME 类型有:超文本标记语言文本 .html text/html、PNG图像 .png image/png、普通文本 .txt text/plain 等。
❞
Blob 构造函数的语法为:
var aBlob = new Blob(blobParts, options);
复制代码
相关的参数说明以下:
""
,它表明了将会被放入到 blob 中的数组内容的 MIME 类型。
"transparent"
,用于指定包含行结束符
\n
的字符串如何被写入。 它是如下两个值中的一个:
"native"
,表明行结束符会被更改成适合宿主操做系统文件系统的换行符,或者
"transparent"
,表明会保持 blob 中保存的结束符不变。
「示例一:从字符串建立 Blob」
let myBlobParts = ['<html><h2>Hello Semlinker</h2></html>']; // an array consisting of a single DOMString
let myBlob = new Blob(myBlobParts, {type : 'text/html', endings: "transparent"}); // the blob console.log(myBlob.size + " bytes size"); // Output: 37 bytes size console.log(myBlob.type + " is the type"); // Output: text/html is the type 复制代码
「示例二:从类型化数组和字符串建立 Blob」
let hello = new Uint8Array([72, 101, 108, 108, 111]); // 二进制格式的 "hello"
let blob = new Blob([hello, ' ', 'semlinker'], {type: 'text/plain'}); 复制代码
介绍完 Blob 构造函数,接下来咱们来分别介绍 Blob 类的属性和方法:
前面咱们已经知道 Blob 对象包含两个属性:
Blob
对象中所包含数据的大小(以字节为单位)。
Blob
对象所包含数据的 MIME 类型。若是类型未知,则该值为空字符串。
ReadableStream
。
USVString
。
ArrayBuffer
。
这里咱们须要注意的是,「Blob
对象是不可改变的」。咱们不能直接在一个 Blob 中更改数据,可是咱们能够对一个 Blob 进行分割,从其中建立新的 Blob 对象,将它们混合到一个新的 Blob 中。这种行为相似于 JavaScript 字符串:咱们没法更改字符串中的字符,但能够建立新的更正后的字符串。
对于 fetch API 的 Response 对象来讲,该对象除了提供 blob()
方法以外,还提供了 json()
、 text()
、formData()
和 arrayBuffer()
等方法,用于把响应转换为不一样的数据格式。
在先后端分离的项目中,你们用得比较多的应该就是 json()
方法,而其它方法可能用得相对比较少。对于前面的示例,咱们把响应对象转换为 ArrayBuffer
对象,一样能够正常显示从网络下载的图像,具体的代码以下所示:
<h3>阿宝哥:获取远程图片预览示例</h3>
<img id="previewContainer" style="width: 50%;"/> <script> const image = document.querySelector("#previewContainer"); fetch("https://avatars3.githubusercontent.com/u/4220799") .then((response) => response.arrayBuffer()) .then((buffer) => { const blob = new Blob([buffer]); const objectURL = URL.createObjectURL(blob); image.src = objectURL; }); </script> 复制代码
在以上代码中,咱们先把响应对象转换为 ArrayBuffer
对象,而后经过调用 Blob 构造函数,把 ArrayBuffer
对象转换为 Blob 对象,再利用 createObjectURL()
方法建立 Object URL,最终实现图片预览。
相信有些小伙伴对 ArrayBuffer 还不太熟悉,下面阿宝哥就带你们一块儿来揭开它的神秘 「“面纱”」。
ArrayBuffer 对象用来表示「通用的、固定长度的」原始二进制数据缓冲区。「ArrayBuffer 不能直接操做,而是要经过类型数组对象 或 DataView
对象来操做」,它们会将缓冲区中的数据表示为特定的格式,并经过这些格式来读写缓冲区的内容。
❝ArrayBuffer 简单说是一片内存,可是你不能直接用它。这就比如你在 C 里面,malloc 一片内存出来,你也会把它转换成 unsigned_int32 或者 int16 这些你须要的实际类型的数组/指针来用。
这就是 JS 里的 TypedArray 的做用,那些 Uint32Array 也好,Int16Array 也好,都是给 ArrayBuffer 提供了一个 “View”,MDN 上的原话叫作 “Multiple views on the same data”,对它们进行下标读写,最终都会反应到它所创建在的 ArrayBuffer 之上。
来源:https://www.zhihu.com/question/30401979
❞
「语法」
new ArrayBuffer(length)
复制代码
Number.MAX_SAFE_INTEGER
(>= 2 ** 53)或为负数,则抛出一个
RangeError
异常。
「示例」
下面的例子建立了一个 8 字节的缓冲区,并使用一个 Int32Array
来引用它:
let buffer = new ArrayBuffer(8);
let view = new Int32Array(buffer); 复制代码
从 ECMAScript 2015 开始,ArrayBuffer
对象须要用 new
运算符建立。若是调用构造函数时没有使用 new
,将会抛出 TypeError
异常。好比执行该语句 let ab = ArrayBuffer(10)
将会抛出如下异常:
VM109:1 Uncaught TypeError: Constructor ArrayBuffer requires 'new'
at ArrayBuffer (<anonymous>) at <anonymous>:1:10 复制代码
对于一些经常使用的 Web API,如 FileReader API 和 Fetch API 底层也是支持 ArrayBuffer,这里咱们以 FileReader API 为例,看一下如何把 File 对象读取为 ArrayBuffer 对象:
const reader = new FileReader();
reader.onload = function(e) { let arrayBuffer = reader.result; } reader.readAsArrayBuffer(file); 复制代码
Uint8Array 数组类型表示一个 8 位无符号整型数组,建立时内容被初始化为 0。建立完后,能够以「对象的方式或使用数组下标索引的方式」引用数组中的元素。
「语法」
new Uint8Array(); // ES2017 最新语法
new Uint8Array(length); // 建立初始化为0的,包含length个元素的无符号整型数组 new Uint8Array(typedArray); new Uint8Array(object); new Uint8Array(buffer [, byteOffset [, length]]); 复制代码
「示例」
// new Uint8Array(length);
var uint8 = new Uint8Array(2); uint8[0] = 42; console.log(uint8[0]); // 42 console.log(uint8.length); // 2 console.log(uint8.BYTES_PER_ELEMENT); // 1 // new TypedArray(object); var arr = new Uint8Array([21,31]); console.log(arr[1]); // 31 // new Uint8Array(typedArray); var x = new Uint8Array([21, 31]); var y = new Uint8Array(x); console.log(y[0]); // 21 // new Uint8Array(buffer [, byteOffset [, length]]); var buffer = new ArrayBuffer(8); var z = new Uint8Array(buffer, 1, 4); 复制代码
ArrayBuffer 自己只是一行 0 和 1 串。 ArrayBuffer 不知道该数组中第一个元素和第二个元素之间的分隔位置。
(图片来源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers)
为了提供上下文,实际上要将其分解为多个盒子,咱们须要将其包装在所谓的视图中。可使用类型数组添加这些数据视图,而且你可使用许多不一样类型的类型数组。
例如,你能够有一个 Int8 类型的数组,它将把这个数组分红 8-bit 的字节数组。
(图片来源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers)
或者你也能够有一个无符号 Int16 数组,它会把数组分红 16-bit 的字节数组,而且把它看成无符号整数来处理。
(图片来源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers)
你甚至能够在同一基本缓冲区上拥有多个视图。对于相同的操做,不一样的视图会给出不一样的结果。
例如,若是咱们从这个 ArrayBuffer 的 Int8 视图中获取 0 & 1 元素的值(-19 & 100),它将给咱们与 Uint16 视图中元素 0 (25837)不一样的值,即便它们包含彻底相同的位。
(图片来源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers)
这样,ArrayBuffer 基本上就像原始内存同样。它模拟了使用 C 之类的语言进行的直接内存访问。「你可能想知道为何咱们不让程序直接访问内存,而是添加了这种抽象层,由于直接访问内存将致使一些安全漏洞」。
目前,咱们已经了解 Blob 与 ArrayBuffer,那么它们以前有什么区别呢?
「ArrayBuffer」 对象用于表示通用的,固定长度的原始二进制数据缓冲区。你不能直接操纵 ArrayBuffer 的内容,而是须要建立一个类型化数组对象或 DataView 对象,该对象以特定格式表示缓冲区,并使用该对象读取和写入缓冲区的内容。
「Blob」 类型的对象表示不可变的相似文件对象的原始数据。Blob 表示的不必定是 JavaScript 原生格式的数据。File 接口基于 Blob,继承了Blob 功能并将其扩展为支持用户系统上的文件。
window.URL.createObjectURL()
。可是,你可能仍须要 FileReader 之类的 File API 才能与 Blob 一块儿使用。
readAsArrayBuffer()
方法,能够把 Blob 对象转换为 ArrayBuffer 对象;
new Blob([new Uint8Array(data]);
,能够把 ArrayBuffer 对象转换为 Blob 对象。
为了便于你们理解转换过程,阿宝哥简单举个相互转换的示例:
var blob = new Blob(["\x01\x02\x03\x04"]),
fileReader = new FileReader(), array; fileReader.onload = function() { array = this.result; console.log("Array contains", array.byteLength, "bytes."); }; fileReader.readAsArrayBuffer(blob); 复制代码
var array = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
var blob = new Blob([array]); 复制代码
DataView 视图是一个能够从二进制 ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不一样平台的字节序问题。
❝字节顺序,又称端序或尾序(英语:Endianness),在计算机科学领域中,指存储器中或在数字通讯链路中,组成多字节的字的字节的排列顺序。
字节的排列方式有两个通用规则。例如,一个多位的整数,按照存储地址从低到高排序的字节中,若是该整数的最低有效字节(相似于最低有效位)在最高有效字节的前面,则称小端序;反之则称大端序。在网络应用中,字节序是一个必须被考虑的因素,由于不一样机器类型可能采用不一样标准的字节序,因此均按照网络标准转化。
例如假设上述变量
❞x
类型为int
,位于地址0x100
处,它的值为0x01234567
,地址范围为0x100~0x103
字节,其内部排列顺序依赖于机器的类型。大端法从首位开始将是:0x100: 01, 0x101: 23,..
。而小端法将是:0x100: 67, 0x101: 45,..
。
new DataView(buffer [, byteOffset [, byteLength]])
复制代码
相关的参数说明以下:
「DataView 返回值」
使用 new 调用 DataView 构造函数后,会返回一个表示指定数据缓存区的新 DataView
对象。你能够把返回的对象想象成一个二进制字节缓存区 array buffer 的 “解释器” —— 它知道如何在读取或写入时正确地转换字节码。这意味着它能在二进制层面处理整数与浮点转化、字节顺序等其余有关的细节问题。
「DataView 使用示例」
const buffer = new ArrayBuffer(16);
// Create a couple of views const view1 = new DataView(buffer); const view2 = new DataView(buffer, 12, 4); //from byte 12 for the next 4 bytes view1.setInt8(12, 42); // put 42 in slot 12 console.log(view2.getInt8(0)); // expected output: 42 复制代码
全部 DataView 实例都继承自 DataView.prototype,而且容许向 DataView 对象中添加额外属性。
DataView 对象提供了 getInt8()、getUint8()、setInt8() 和 setUint8() 等方法来操做数据。具体每一个方法的使用,咱们就不详细介绍。这里咱们来看个简单的例子:
const buffer = new ArrayBuffer(16);
const view = new DataView(buffer, 0); view.setInt8(1, 68); view.getInt8(1); // 68 复制代码
介绍完 ArrayBuffer、TypedArray 和 DataView 的相关知识,阿宝哥用一张图来总结一下它们之间的关系。
好的,下面咱们立刻进入下一个环节。
要对图片进行灰度化处理,咱们就须要操做图片像素数据。那么问题来了,咱们应该如何获取图片的像素数据呢?
针对上述问题,咱们能够利用 CanvasRenderingContext2D 提供的 getImageData
来获取图片像素数据,其中 getImageData() 返回一个 ImageData 对象,用来描述 canvas 区域隐含的像素数据,这个区域经过矩形表示,起始点为(sx, sy)、宽为 sw、高为 sh。其中 getImageData
方法的语法以下:
ctx.getImageData(sx, sy, sw, sh);
复制代码
相应的参数说明以下:
在获取到图片的像素数据以后,咱们就能够对获取的像素数据进行处理,好比进行灰度化或反色处理。当完成处理后,若要在页面上显示处理效果,则咱们须要利用 CanvasRenderingContext2D 提供的另外一个 API —— putImageData
。
该 API 是 Canvas 2D API 将数据从已有的 ImageData 对象绘制到位图的方法。 若是提供了一个绘制过的矩形,则只绘制该矩形的像素。此方法不受画布转换矩阵的影响。putImageData 方法的语法以下:
void ctx.putImageData(imagedata, dx, dy);
void ctx.putImageData(imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight); 复制代码
相应的参数说明以下:
ImageData
,包含像素值的数组对象。
介绍完 getImageData()
和 putImageData()
方法,下面咱们来看一下具体如何利用它们实现图片灰度化:
<!DOCTYPE html>
<html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>获取远程图片并灰度化</title> </head> <body> <h3>阿宝哥:获取远程图片并灰度化示例</h3> <div> <button id="grayscalebtn">灰度化</button> <div style="display: flex;"> <div style="flex: 50%;"> <p>预览容器</p> <img id="previewContainer" width="230" height="230" style="border: 2px dashed blue;" /> </div> <div style="flex: 50%;"> <p>Canvas容器</p> <canvas id="canvas" width="230" height="230" style="border: 2px dashed grey;" ></canvas> </div> </div> </div> <script> const image = document.querySelector("#previewContainer"); const canvas = document.querySelector("#canvas"); fetch("https://avatars3.githubusercontent.com/u/4220799") .then((response) => response.blob()) .then((blob) => { const objectURL = URL.createObjectURL(blob); image.src = objectURL; image.onload = () => { draw(); }; }); function draw() { const ctx = canvas.getContext("2d"); ctx.drawImage(image, 0, 0, 230, 230); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; const grayscale = function () { for (let i = 0; i < data.length; i += 4) { const avg = (data[i] + data[i + 1] + data[i + 2]) / 3; data[i] = avg; // red data[i + 1] = avg; // green data[i + 2] = avg; // blue } ctx.putImageData(imageData, 0, 0); }; const grayscalebtn = document.querySelector("#grayscalebtn"); grayscalebtn.addEventListener("click", grayscale); } </script> </body> </html> 复制代码
在以上示例中,咱们先从 Github 上下载阿宝哥的头像,而后先进行本地预览,接着调用 draw()
方法把获取的图像绘制到页面的 Canvas 容器中,同时为灰度化按钮绑定监听事件。
当用户点击灰度化按钮时,将会触发灰度化处理函数,在该函数内部会对经过 ctx.getImageData()
方法获取的图片像素进行灰度化处理,处理完成后再经过 ctx.putImageData()
方法把处理过的像素数据更新到 Canvas 上。
以上代码成功运行后,最终的灰度化效果以下图所示:
在一些场合中,咱们但愿在上传本地图片时,先对图片进行必定的压缩,而后再提交到服务器,从而减小传输的数据量。在前端要实现图片压缩,咱们能够利用 Canvas 对象提供的 toDataURL()
方法,该方法接收 type
和 encoderOptions
两个可选参数。
其中 type
表示图片格式,默认为 image/png
。而 encoderOptions
用于表示图片的质量,在指定图片格式为 image/jpeg
或 image/webp
的状况下,能够从 0 到 1 的区间内选择图片的质量。若是超出取值范围,将会使用默认值 0.92
,其余参数会被忽略。
下面咱们来看一下如何对前面已进行灰度化处理的图片进行压缩。
<button id="compressbtn">图片压缩</button>
<div style="display: flex;"> <div style="flex: 33.3%;"> <p>预览容器</p> <img id="previewContainer" width="230" height="230" style="border: 2px dashed blue;" /> </div> <div style="flex: 33.3%;"> <p>Canvas容器</p> <canvas id="canvas" width="230" height="230" style="border: 2px dashed grey;"> </canvas> </div> <div style="flex: 33.3%;"> <p>压缩预览容器</p> <img id="compressPrevContainer" width="230" height="230" style="border: 2px dashed green;" /> </div> </div> <script> const compressbtn = document.querySelector("#compressbtn"); const compressImage = document.querySelector("#compressPrevContainer"); compressbtn.addEventListener("click", compress); function compress(quality = 80, mimeType = "image/webp") { const imageDataURL = canvas.toDataURL(mimeType, quality / 100); compressImage.src = imageDataURL; } </script> 复制代码
在以上代码中,咱们设默认的图片质量是 「0.8」,而图片类型是 「image/webp」 类型。当用户点击压缩按钮时,则会调用 Canvas 对象的 toDataURL()
方法实现图片压缩。 以上代码成功运行后,最终的处理效果以下图所示:
其实 Canvas 对象除了提供 toDataURL()
方法以外,它还提供了一个 toBlob()
方法,该方法的语法以下:
canvas.toBlob(callback, mimeType, qualityArgument)
复制代码
和 toDataURL()
方法相比,toBlob()
方法是异步的,所以多了个 callback
参数,这个 callback
回调方法默认的第一个参数就是转换好的 blob
文件信息。
在获取压缩后图片对应的 Data URL 数据以后,能够把该数据直接提交到服务器。针对这种情形,服务端须要作一些相关处理,才能正常保存上传的图片,这里以 Express 为例,具体处理代码以下:
const app = require('express')();
app.post('/upload', function(req, res){ let imgData = req.body.imgData; // 获取POST请求中的base64图片数据 let base64Data = imgData.replace(/^data:image\/\w+;base64,/, ""); let dataBuffer = Buffer.from(base64Data, 'base64'); fs.writeFile("abao.png", dataBuffer, function(err) { if(err){ res.send(err); }else{ res.send("图片上传成功!"); } }); }); 复制代码
然而对于返回的 Data URL 格式的图片数据通常都会比较大,为了进一步减小传输的数据量,咱们能够把它转换为 Blob 对象:
function dataUrlToBlob(base64, mimeType) {
let bytes = window.atob(base64.split(",")[1]); let ab = new ArrayBuffer(bytes.length); let ia = new Uint8Array(ab); for (let i = 0; i < bytes.length; i++) { ia[i] = bytes.charCodeAt(i); } return new Blob([ab], { type: mimeType }); } 复制代码
在转换完成后,咱们就能够压缩后的图片对应的 Blob 对象封装在 FormData 对象中,而后再经过 AJAX 提交到服务器上:
function uploadFile(url, blob) {
let formData = new FormData(); let request = new XMLHttpRequest(); formData.append("imgData", blob); request.open("POST", url, true); request.send(formData); } 复制代码
要查看图片对应的二进制数据,咱们就须要借助一些现成的编辑器,好比 Windows 平台下的 「WinHex」 或 macOS 平台下的 「Synalyze It! Pro」 十六进制编辑器。这里咱们使用 Synalyze It! Pro 这个编辑器,以十六进制的形式来查看阿宝哥头像对应的二进制数据。
「计算机并非经过图片的后缀名来区分不一样的图片类型,而是经过 “魔数”(Magic Number)来区分。」 对于某一些类型的文件,起始的几个字节内容都是固定的,根据这几个字节的内容就能够判断文件的类型。
常见图片类型对应的魔数以下表所示:
文件类型 | 文件后缀 | 魔数 |
---|---|---|
JPEG | jpg/jpeg | 0xFFD8FF |
PNG | png | 0x89504E47 |
GIF | gif | 0x47494638(GIF8) |
BMP | bmp | 0x424D |
这里咱们以阿宝哥的头像(abao.png)为例,验证一下该图片的类型是否正确:
在平常开发过程当中,若是遇到检测图片类型的场景,咱们能够直接利用一些现成的第三方库。好比,你想要判断一张图片是否为 PNG 类型,这时你可使用 is-png 这个库,它同时支持浏览器和 Node.js,使用示例以下:
「Node.js」
// npm install read-chunk
const readChunk = require('read-chunk'); const isPng = require('is-png'); const buffer = readChunk.sync('unicorn.png', 0, 8); isPng(buffer); //=> true 复制代码
「Browser」
(async () => {
const response = await fetch('unicorn.png'); const buffer = await response.arrayBuffer(); isPng(new Uint8Array(buffer)); //=> true })(); 复制代码
图片的尺寸、位深度、色彩类型和压缩算法都会存储在文件的二进制数据中,咱们继续以阿宝哥的头像(abao.png)为例,来了解一下实际的状况:
❝528(十进制) => 0x0210(十六进制)
560(十进制)=> 0x0230(十六进制)
❞
所以若是想要获取图片的尺寸,咱们就须要依据不一样的图片格式对图片二进制数据进行解析。幸运的是,咱们不须要本身实现该功能,image-size 这个 Node.js 库已经帮咱们实现了获取主流图片类型文件尺寸的功能,使用示例以下:
「同步方式」
var sizeOf = require('image-size');
var dimensions = sizeOf('images/abao.png'); console.log(dimensions.width, dimensions.height); 复制代码
「异步方式」
var sizeOf = require('image-size');
sizeOf('images/abao.png', function (err, dimensions) { console.log(dimensions.width, dimensions.height); }); 复制代码
image-size 这个库功能仍是蛮强大的,除了支持 PNG 格式以外,还支持 BMP、GIF、ICO、JPEG、SVG 和 WebP 等格式。
相信小伙们平时也听过图片解码、音视频解码。解码 PNG 图片就是把一张图片从二进制数据转换成包含像素数据的 ImageData。前面咱们已经讲过,能够利用 CanvasRenderingContext2D 提供的 getImageData()
方法来获取图片像素数据。
那么 getImageData()
方法内部是如何处理的呢?下面咱们来简单介绍一下大体流程,这里咱们以一张 2px * 2px 的图片为例,下图是放大展现的效果:
(图片来源:https://vivaxyblog.github.io/2019/12/07/decode-a-png-image-with-javascript-cn.html)
一样,咱们先使用 「Synalyze It! Pro」 十六进制编辑器打开上面的 「2px * 2px」 的图片:
PNG 图片的像素数据是保存在 「IDAT」 块中,除了 「IDAT」 块以外,还包含其余的数据块,完整的数据块以下所示:
(图片来源:https://dev.gameres.com/Program/Visual/Other/PNGFormat.htm)
在解析像素数据以前,咱们先了解像素数据是如何编码的。每行像素都会先通过过滤函数处理,每行像素的过滤函数能够不一样。而后全部行的像素数据会通过 「deflate」 压缩算法压缩。这里阿宝哥使用 pako 这个库进行解码操做:
const pako = require("pako");
const compressed = new Uint8Array([120, 156, 99, 16, 96, 216, 0, 0, 0, 228, 0, 193]); try { const result = pako.inflate(compressed); console.dir(result); } catch (err) { console.log(err); } 复制代码
在以上代码中,经过调用 pako.inflate()
方法执行解压操做,最终的解压后的像素数据以下:
Uint8Array [ 0, 16, 0, 176 ]
复制代码
获取解压的像素数据以后,还要解码扫描线,而后根据色板中的索引,来还原出图片的像素信息。这里阿宝哥就不详细展开了,感兴趣的小伙伴能够阅读 一步一步解码 PNG 图片 这篇文章。
File 对象是特殊类型的 Blob,且能够用在任意的 Blob 类型的上下文中。因此针对大文件传输的场景,咱们可使用 slice 方法对大文件进行切割,而后分片进行上传,具体示例以下:
const file = new File(["a".repeat(1000000)], "test.txt");
const chunkSize = 40000; const url = "https://httpbin.org/post"; async function chunkedUpload() { for (let start = 0; start < file.size; start += chunkSize) { const chunk = file.slice(start, start + chunkSize + 1); const fd = new FormData(); fd.append("data", chunk); await fetch(url, { method: "post", body: fd }).then((res) => res.text() ); } } 复制代码
在一些场景中,咱们会经过 Canvas 进行图片编辑或使用 jsPDF、sheetjs 等一些第三方库进行文档处理,当文件文件处理完成后,咱们须要把文件下载并保存到本地。针对这些场景,咱们可使用纯前端的方案实现文件下载。
“Talk is cheap”,阿宝哥来举一个简单的 Blob 文件下载的示例:
「index.html」
<!DOCTYPE html>
<html> <head> <meta charset="UTF-8" /> <title>Blob 文件下载示例</title> </head> <body> <button id="downloadBtn">文件下载</button> <script src="index.js"></script> </body> </html> 复制代码
「index.js」
const download = (fileName, blob) => {
const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = fileName; link.click(); link.remove(); URL.revokeObjectURL(link.href); }; const downloadBtn = document.querySelector("#downloadBtn"); downloadBtn.addEventListener("click", (event) => { const fileName = "blob.txt"; const myBlob = new Blob(["一文完全掌握 Blob Web API"], { type: "text/plain" }); download(fileName, myBlob); }); 复制代码
在示例中,咱们经过调用 Blob 的构造函数来建立类型为 「"text/plain"」 的 Blob 对象,而后经过动态建立 a
标签来实现文件的下载。在实际项目开发过程当中,咱们可使用成熟的开源库,好比 FileSaver.js 来实现文件保存功能。
❝建立了一个 「“全栈修仙之路交流群”」 的微信群,想加群的小伙伴,加我微信 "semlinker",备注 「2」。阿里、京东、腾讯的大佬都在群里等你哟。
semlinker/awesome-typescript 1.7K
❞
本文使用 mdnice 排版