一篇文章让你彻底学会图片操作

4,480 阅读8分钟

在华为云算法平台中,我们需要大量处理各种图片类型的文件。我们知道,在日常处理图片的操作中,实际上是一个比较复杂的过程,其中涉及的流程包括:选择本地文件预览、网络下载的图片预览、图片压缩等处理、图片上传服务器等等。这个过程穿插着对图片文件的的形态处理,例如:二进制、Blob、Base64、ArrayBuffer等相关知识。接下来我们将以图片处理的一个完整的流程为切入点,来系统了解其本质内容。

1. 如何实现图片预览

实现图片预览其实分为两种场景:

  1. 本地图片上传 ——> 图片预览
  2. 网络下载图片 ——> 图片预览

1.1 如何通过本地上传实现图片预览

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图片预览</title>
</head>
<body>
    <h3>镇哥@吃橙子实例展示</h3>
    <input type="file" onchange="loadFile(event)" />
    <img id="previewContainer" />
</body>
<script>
    function loadFile (event) {
        const reader = new FileReader();
        reader.onload = function () {
            const output = document.querySelector("#previewContainer");
            output.src = reader.result;
        };
        reader.readAsDataURL(event.target.files[0]);
    };
</script>
</html>

给file 类型的 input 输入框绑定 onchange 事件处理函数,在该函数中,我们创建了一个 FileReader 对象并为该对象绑定 onload 相应的事件处理函数,然后调用 FileReader 对象的 readAsDataURL() 方法,把本地图片对应的 File 对象转换为 Data URL。当文件读取完成后,会触发绑定的 onload 事件处理函数,在该处理函数内部会把获取 Data URL 数据赋给 img 元素的 src 属性,从而实现图片本地预览。 image.png 上图中,在src 属性中的这串字符串就是 Base64 编码的字符串。在项目开发中,为了减少 HTTP 请求的数量,对应一些较小的图标,我们通常会考虑使用 Data URL 的形式内嵌到 HTML 或 CSS 文件中。但是如果图片较大,则不适合使用这种方式,因为该图片经过 Base64 编码后的字符串非常大,会明显增大 HTML 页面的大小,从而影响加载速度。Base64 是一种基于64个可打印字符来表示二进制数据的方法,常用于在处理文本数据的场合,表示、传输、存储一些二进制数据,包括 MIME 的电子邮件及 XML 的一些复杂数据。在JavaScript 中,有两个函数被分别用来处理解码和编码 base64 字符串:

  • btoa():该函数能够基于二进制数据 “字符串” 创建一个 base64 编码的 ASCII 字符串。
  • atob(): 该函数能够解码通过 base64 编码的字符串数据。 对于 atob 和 btoa 这两个方法来说,其中的 a 代表 ASCII,而 b 代表 Blob,即二进制。因此 atob 表示 ASCII 到二进制,对应的是解码操作。而 btoa 表示二进制到 ASCII,对应的是编码操作。
const name = 'kobe bryant';
const encodedName = btoa(name);
console.log(encodedName); // a29iZSBicnlhbnQ=
const encodedName = 'a29iZSBicnlhbnQ=';
const name = atob(encodedName);
console.log(name); // kobe bryant

1.1 如何通过网络下载图片实现图片预览

我们可以使用 fetch API(developer.mozilla.org/zh-CN/docs/… 从网络上获取图片,然后在进行图片预览:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图片预览</title>
</head>
<body>
    <h3>镇哥@吃橙子实例展示</h3>
    <img id="previewContainer" style="width: 50%;" />
</body>
<script>
    const image = document.querySelector("#previewContainer");
    fetch("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2F8138b202eb5c505b4e46159b2642477205c6b42c14bb5-FWbB38_fw658&refer=http%3A%2F%2Fhbimg.b0.upaiyun.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1628928976&t=5e9f3b7f70c32cc17f87500920124a75")
        .then((response) => response.blob())
        .then((blob) => {
            const objectURL = URL.createObjectURL(blob);
            image.src = objectURL;
        });
</script>
</html>

我们通过 fetch API 从远程路径取到头像资源,当请求成功后,把响应对象(Response)转换为 Blob 对象,然后使用 URL.createObjectURL 方法,创建 Object URL 并把它赋给 img 元素的 src 属性,从而实现图片的显示。 image.png 浏览器内部为每个通过 URL.createObjectURL 生成的 URL 存储了一个 URL → Blob 映射,Blob 本身仍驻留在内存中,浏览器无法释放它。生成的 URL 仅在当前文档打开的状态下才有效。但如果你访问的 Blob URL不再存在,则会从浏览器中收到 404 错误。这么看来,我们是实际上需要取到应该是 Blob 对象。

1.1 Blob

Blob(Binary Large Object)表示二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个单一个体的集合。Blob 通常是影像、声音或多媒体文件。为了更直观的感受 Blob 对象,我们先来使用 Blob 构造函数,创建一个 myBlob 对象,具体如下图所示: image.png Blob 构造函数的语法为:

const aBlob = new Blob(blobParts, options);

// blobParts:它是一个由 ArrayBuffer,ArrayBufferView,Blob,DOMString 等对象构成的数组。DOMStrings 会被编码为 UTF-8。
// options:一个可选的对象,包含以下两个属性:type —— 默认值为 ""; endings —— 默认值为 "transparent",用于指定包含行结束符 \n 的字符串如何被写入

// 实例代码:从字符串创建Blob
let myBlobParts = ['<html><h2>Hello Semlinker</h2></html>'];
let myBlob = new Blob(myBlobParts, {type : 'text/html', endings: "transparent"});
console.log(myBlob.size);
// Output: 37 
console.log(myBlob.type);
// Output: text/html

Blob 方法有:

  1. slice([start[, end[, contentType]]]):返回一个新的 Blob 对象,包含了源 Blob 对象中指定范围内的数据。
  2. stream():返回一个能读取 blob 内容的 ReadableStream。
  3. text():返回一个 Promise 对象且包含 blob 所有内容的 UTF-8 格式的 USVString。
  4. arrayBuffer():返回一个 Promise 对象且包含 blob 所有内容的二进制格式的 ArrayBuffer。 Blob 对象是不可改变的。我们不能直接在一个 Blob 中更改数据,但是我们可以对一个 Blob 进行分割,从其中创建新的 Blob 对象,将它们混合到一个新的 Blob 中。这种行为类似于 JavaScript 字符串:我们无法更改字符串中的字符,但可以创建新的更正后的字符串。

1.2 ArrayBuffer

对于 fetch API 的 Response 对象来说,该对象除了提供 blob() 方法之外,还提供了 json()、 text() 、formData() 和 arrayBuffer() 等方法,用于把响应转换为不同的数据格式。对于前面的示例,我们把响应对象转换为 ArrayBuffer 对象,同样可以正常显示从网络下载的图像,具体的代码如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图片预览</title>
</head>
<body>
    <h3>镇哥@吃橙子实例展示</h3>
    <img id="previewContainer" style="width: 50%;" />
</body>
<script>
    const image = document.querySelector("#previewContainer");
    fetch(
            "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2F8138b202eb5c505b4e46159b2642477205c6b42c14bb5-FWbB38_fw658&refer=http%3A%2F%2Fhbimg.b0.upaiyun.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1628928976&t=5e9f3b7f70c32cc17f87500920124a75")
        .then((response) => response.arrayBuffer())
        .then((buffer) => {
            const blob = new Blob([buffer]);
            const objectURL = URL.createObjectURL(blob);
            image.src = objectURL;
        });
</script>
</html>

ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。ArrayBuffer 不能直接操作,而是要通过类型数组对象 或 DataView 对象来操作。对于一些常用的 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);       // 可以将File 对象读取为 ArrayBuffer 对象

1.2 ArrayBuffer 和 Blob 的区别

  • ArrayBuffer对象用于表示通用的,固定长度的原始二进制数据缓冲区。你不能直接操纵 ArrayBuffer 的内容,而是需要创建一个类型化数组对象或 DataView 对象,该对象以特定格式表示缓冲区,并使用该对象读取和写入缓冲区的内容。
  • Blob类型的对象表示不可变的类似文件对象的原始数据。Blob 表示的不一定是 JavaScript 原生格式的数据。File 接口基于 Blob,继承了Blob 功能并将其扩展为支持用户系统上的文件。
  • Blob 与 ArrayBuffer 对象之间是可以相互转化的:
  1. 使用 FileReader 的 readAsArrayBuffer() 方法,可以把 Blob 对象转换为 ArrayBuffer 对象;
  2. 使用 Blob 构造函数,如 new Blob([new Uint8Array(data]);,可以把 ArrayBuffer 对象转换为 Blob 对象。
// Blob 转换为 ArrayBuffer

let blob = new Blob(["\x01\x02\x03\x04"]);
let fileReader = new FileReader();
let arrayRes;
fileReader.onload = function() {
  arrayRes = this.result;
  console.log("Array contains", array.byteLength, "bytes.");
};
fileReader.readAsArrayBuffer(blob);
// ArrayBuffer 转 Blob

let array = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
let blob = new Blob([array]);

2 如何通过 canvas 来处理图片效果

某些场景需要对图片效果进行处理,我们就需要操作图片像素数据。那么问题来了,我们应该如何获取图片的像素数据呢?这个时候需要引入 canvas 来处理。如下例子中我们事实现一个图片灰度化处理的效果:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>canvas绘图</title>
</head>
<body>
    <div>
        <h3>灰度化处理</h3>
        <div style="display: flex;">
            <div style="flex: 50%;">
                <p>预览容器</p>
                <img id="previewContainer" src="./img/test.PNG" 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>
</body>
<script>
    window.onload = function () {
        // 绘制 canvas 图片:
        drawCanvas();
    }
    function drawCanvas() {
        const image = document.querySelector('#previewContainer');
        const canvas = document.querySelector('#canvas');
        const ctx = canvas.getContext('2d');
        ctx.drawImage(image, 0, 0, 230, 230); // 绘制

        // 取canvas数据重新绘制灰度化图片:
        // 注意当用到getImageData方法获取图片信息时,会碰到跨域无法获取的情况,浏览器跨域 cros 设置或者将图片放在服务器中
        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);   
        const data = imageData.data;
        for (let i = 0; i < data.length; i += 4) {
            const avg = (data[i] + data[i + 1] + data[i + 2]) / 10;
            data[i] = avg; // red
            data[i + 1] = avg; // green
            data[i + 2] = avg; // blue
        }
        ctx.putImageData(imageData, 0, 0);
    }
</script>
</html>

ctx.getImageData() 方法获取的图片像素进行灰度化处理,处理完成后再通过 ctx.putImageData() 方法把处理过的像素数据更新到 Canvas 上。

image.png

3 如何通过 canvas 来实现图片压缩处理

在一些场合中,我们希望在上传本地图片时,先对图片进行一定的压缩,然后再提交到服务器,从而减少传输的数据量。在前端要实现图片压缩,我们可以利用 Canvas 对象提供的 toDataURL() 方法,该方法接收 type 和 encoderOptions 两个可选参数:

  • type 表示图片格式,默认为 image/png;
  • encoderOptions 用于表示图片的质量,在指定图片格式为 image/jpeg 或 image/webp 的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92,其他参数会被忽略
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>canvas绘图</title>
</head>

<body>
    <div>
        <button id="compressbtn">图片压缩</button>
        <div style="display: flex;">
            <div style="flex: 33.3%;">
                <p>预览容器</p>
                <img id="previewContainer" src="./img/test.PNG" 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>
    </div>
</body>
<script>
    window.onload = function () {
        // 绘制 canvas 图片:
        drawCanvas();

        // 图片压缩添加事件处理:
        const compressbtn = document.querySelector("#compressbtn");
        const compressImage = document.querySelector("#compressPrevContainer");
        compressbtn.addEventListener("click", compress);
        function compress(quality = 10, mimeType = "image/webp") {
            const imageDataURL = canvas.toDataURL(mimeType, quality / 100);
            compressImage.src = imageDataURL;
        }

    }

    function drawCanvas() {
        const image = document.querySelector('#previewContainer');
        const canvas = document.querySelector('#canvas');
        const ctx = canvas.getContext('2d');
        ctx.drawImage(image, 0, 0, 230, 230); // 绘制

        // 取canvas数据重新绘制灰度化图片:
        // 注意当用到getImageData方法获取图片信息时,会碰到跨域无法获取的情况,浏览器跨域 cros 设置或者将图片放在服务器中
        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); 
        const data = imageData.data;
        for (let i = 0; i < data.length; i += 4) {
            const avg = (data[i] + data[i + 1] + data[i + 2]) / 10;
            data[i] = avg; // red
            data[i + 1] = avg; // green
            data[i + 2] = avg; // blue
        }
        ctx.putImageData(imageData, 0, 0);
    }
</script>
</html>

image.png

4 前端如何处理图片上传

对于返回的 Data Url格式的图片数据(即 base64 编码数据)一般会比较大,为了进一步的减少传输的数据量我们可以把它转换成 Blob 对象:

function dataUrlToBlob(base64, mimeType) {
  let bytes = window.atob(base64.split(",")[1]);   // 解码 base64 数据
  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);
}

5 后台服务如何读取并处理图片

我们来看看Node.js服务端如何处理拿到的 Data Url 数据(即 base64 编码数据):

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("图片上传成功!");
        }
    });
});

6 前端如何实现大文件分片上传和文件下载

File 对象是特殊类型的 Blob,且可以用在任意的 Blob 类型的上下文中。所以针对大文件传输的场景,我们可以使用 slice 方法对大文件进行切割,然后分片进行上传,具体示例如下:

// file 为 File 对象的一个实例
// url 是请求的url

const chunkSize = 40000;
async function chunkedUpload(url, file) {
  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()
      );
  }
}

我们通过调用 Blob 的构造函数来创建类型为 「"text/plain"」 的 Blob 对象,然后通过动态创建 a 标签来实现文件的下载:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文件下载实例</title>
</head>
<body>
    <button id="downloadBtn">文件下载</button>
    <script>
        const downloadBtn = document.querySelector("#downloadBtn");
        downloadBtn.addEventListener("click", (event) => {
            const fileName = "blob.txt";
            const myBlob = new Blob(["这是一个测试文件下载的的实例"], {
                type: "text/plain"
            });
            download(fileName, myBlob);
        });

        function download(fileName, blob){
            const link = document.createElement("a");
            link.href = URL.createObjectURL(blob);
            link.download = fileName;
            link.click();
            link.remove();
            URL.revokeObjectURL(link.href);
        };
    </script>
</body>
</html>