再了解了 Blob、Blob URL、Base64、Data URL、ArrayBuffer、TypedArray、DataView 之间的关系后,我们再来了解一下关于图片灰度化,压缩 和 上传 的知识点。
要对图片进行灰度化处理,就需要操作图片像素数据。那么问题来了,我们应该如何获取图片的像素数据呢?
针对上述问题,我们可以利用 CanvasRenderingContext2D 提供的 getImageData 来获取图片像素数据;其中 getImageData() 返回一个 ImageData 对象,用来描述 canvas 区域隐含的 像素数据 ,这个区域通过矩形表示,起始点为 (sx, sy) 、宽为 sw 、高为 sh 。
语法:
ctx.getImageData(sx, sy, sw, sh);
参数说明:
在获取到图片的像素数据之后,就可以对获取的像素数据进行处理,比如进行灰度化或反色处理。当完成处理后,若要在页面上显示处理效果,则我们需要利用 CanvasRenderingContext2D 提供的另一个 API —— putImageData。
该 API 是 Canvas 2D API 将数据从已有的 ImageData 对象绘制到位图的方法。
语法:
ctx.putImageData(imagedata, dx, dy);
ctx.putImageData(imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight);
参数说明:
利用 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://s.yimg.com/ut/api/res/1.2/wc8Y6ZMRPK6k0ucegJ2J5Q--~B/YXBwaWQ9eXR3bWFsbDtjYz0zMTUzNjAwMDtoPTYwMDtxPTEwMDt3PTYwMA--/https://s.yimg.com/fy/8236/item/p0265120751355-item-ebbbxf4x0800x0800-m.jpg")
.then((response) => response.blob())
.then((blob) => {
const objectURL = URL.createObjectURL(blob);
image.src = objectURL;
image.onload = () => {
draw();
};
});
// 把获取的图像绘制到页面的 Canvas 容器中
function draw() {
const ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, 230, 230);
// 通过 ctx.getImageData() 方法获取的图片,便于在grayscale函数中 图片像素进行灰度化处理
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// 图片灰度化
const grayscale = function (data) {
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() 方法把处理过的像素数据更新到 Canvas 上
ctx.putImageData(imageData, 0, 0);
};
// 按钮绑定监听事件
const grayscalebtn = document.querySelector("#grayscalebtn");
grayscalebtn.addEventListener("click", grayscale);
}
</script>
</body>
</html>
在一些场合中,我们希望在上传本地图片时,先对图片进行一定的压缩,然后再提交到服务器,从而减少传输的数据量。在前端要实现图片压缩,我们可以利用 Canvas 对象提供的 toDataURL() 方法,该方法接收 type 和 encoderOptions 两个可选参数。
语法:
canvas.toDataURL(type, encoderOptions);
参数说明:
示例:
/* 添加压缩后的图片预览容器 */
<div style="flex: 33.3%;">
<p>压缩预览容器</p>
<img id="compressPrevContainer" width="230" height="230" style="border: 2px dashed green;" />
</div>
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;
}
在以上代码中,我们设默认的图片质量是 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){
// 获取POST请求中的base64图片数据
let imgData = req.body.imgData;
let base64Data = imgData.replace(/^data:image\/\w+;base64,/, "");
let dataBuffer = Buffer.from(base64Data, 'base64');
fs.writeFile("melod.jpg", 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 buffer = new ArrayBuffer(bytes.length);
let uin8 = new Uint8Array(buffer);
for (let i = 0; i < bytes.length; i++) {
uin8[i] = bytes.charCodeAt(i);
}
return new Blob([buffer], { 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);
}
计算机并不是通过图片的后缀名来区分不同的图片类型,而是通过 “魔数”(Magic Number)来区分。 对于某一些类型的文件,起始的几个字节内容都是固定的,根据这几个字节的内容就可以判断文件的类型。
常见图片类型对应的魔数如下表所示:
文件类型 | 文件后缀 | 魔数 |
---|---|---|
JPEG | jpg/jpeg | 0xFFD8FF |
PNG | png | 0x89504E47 |
GIF | gif | 0x47494638(GIF8) |
BMP | bmp | 0x424D |
在日常开发过程中,如果遇到检测图片类型的场景,可以直接利用一些现成的第三方库。比如,想要判断一张图片是否为 PNG 类型,这时你可以使用 is-png 这个库,它同时支持浏览器和 Node.js。
图片的尺寸、位深度、色彩类型和压缩算法都会存储在文件的二进制数据中。
如果想要获取图片的尺寸,就需要依据不同的图片格式对图片二进制数据进行解析。可以通过 image-size 这个 Node.js 库实现了获取主流图片类型文件尺寸的功能。
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 等一些第三方库进行文档处理,当文件文件处理完成后,我们需要把文件下载并保存到本地。针对这些场景,我们可以使用纯前端的方案实现文件下载。
一个简单的 Blob 文件下载的示例:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Blob 文件下载示例</title>
</head>
<body>
<button id="downloadBtn">文件下载</button>
<script>
// 下载
const download = (fileName, blob) => {
// 动态创建 a 标签
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";
// 创建下载对象,类型为 text/plain
const myBlob = new Blob(["我是内容我是内容我是内容"], { type: "text/plain" });
download(fileName, myBlob);
});
</script>
</body>
</html>
在示例中,通过调用 Blob 的构造函数来创建类型为 “text/plain” 的 Blob 对象,然后通过动态创建 a 标签来实现文件的下载。在实际项目开发过程中,我们可以使用成熟的开源库,比如 FileSaver.js 来实现文件保存功能。