在前端领域里,浏览器本身没有压缩图片的能力,目前业界在前端主流的图片压缩方式有两种,尺寸(像素)压缩和质量压缩。
- 尺寸压缩: 通过用
canvas
按压缩比例绘制图片长度和宽度来压缩图片。 - 质量压缩:
canvas
绘制图片后,输出base64
字符串过程,设置输出图片的质量,这里的质量算是可以理解为清晰度。
本篇文章主要利用 canvas
的 尺寸压缩
和 质量压缩
的结合能力来压缩图片。
具体实现的步骤有
- 步骤1 (尺寸压缩): 设置尺寸压缩的参数
-
- 设定待压缩图片最大限制像素为 2000*2000 = 400万像素
- 受限于不同浏览器对canvas总绘制图片像素大小的限制,取个保守的限制尺寸 400万像素
-
- 设定待压缩图片期待压缩过程的切成多个瓦片,每个瓦片的像素为 1000*1000 = 100万像素
- 因为受限于部分浏览器对canvas图片一次性绘制的大小限制,保守设置一次绘制100万像素的瓦片
-
- 步骤2 (尺寸压缩): 计算原始图片与最大限制尺寸(长度/宽度)的比例情况
- 步骤3 (尺寸压缩): 如果原始图片像素长宽比限制像素尺寸大,换算出压缩结果尺寸
- 步骤4 (尺寸压缩): 计算需要拆分的瓦片数量
- 步骤5 (尺寸压缩): 拼接瓦片
- 如果瓦片数量大于1,就需要拼接瓦片
- 按照换算的瓦片位置,把瓦片原图绘制到一个临时tempCanvas里
- 再把临时的tempCanvas根据压缩后换算的长度/宽度,x轴位置和y轴位置,绘制到结果的canvas位置上
- 循环直至瓦片全部压缩比例绘制到结果canvas上
- 如果瓦片数量小于1,即可以安全绘制到结果canvas里
- 如果瓦片数量大于1,就需要拼接瓦片
- 步骤6 (质量压缩): 结果的canvas按照
0~1
的范围内,取压缩比例,输出成图片base64
https://github.com/chenshenhai/canvas-note/blob/master/demo/lib/util/compress.js
// 1.1 设定待压缩图片最大限制像素为 2000*2000 = 400万像素
// 受限于不同浏览器对canvas总绘制图片像素大小的限制,取个保守的限制尺寸 400万像素
const IMG_LIMIT_SIZE = 2000 * 2000;
// 1.2 设定待压缩图片期待压缩过程的切成多个瓦片,每个瓦片的像素为 1000*1000 = 100万像素
// 因为受限于部分浏览器对canvas图片一次性绘制的大小限制,保守设置一次绘制100万像素的瓦片
const PIECE_SIZE = 1000 * 1000;
/**
* 压缩图片
* @param {Image} img
* @param {object} opts
* opts.type: 输出图片类型
* opts.encoderOptions: 压缩比例,范围在[0,1],只在 type='image/jpeg'时候有效
* @return {string} 输出类型
*/
export const compressImage = function(img, opts = { type: 'image/jpeg', encoderOptions: 0.5 }) {
const {type, encoderOptions } = opts;
const w = img.width;
const h = img.height;
let outputW = w;
let outputH = h;
// 获取原始图片尺寸
let imageSize = w * h;
// 2. 计算原始图片与最大限制尺寸(长度/宽度)的比例情况
// 由于是面积换算比例,所以要取开平方才能清晰知道原始像素为最大限制像素的长度/宽度比例
// 例如: 原始图片像素为 8000 * 8000 = 64,000,000 六千四百万像素
// 是最大图片限制 2000 * 2000 像素的 长度/宽度的四倍
let ratio = Math.ceil(Math.sqrt(Math.ceil(imageSize / IMG_LIMIT_SIZE)));
if ( ratio > 1) {
// 如果原始图片像素长宽比限制像素尺寸大
// 就换算出压缩后图片尺寸的长度和宽度
outputW = w / ratio;
outputH = h / ratio;
} else {
// 剩下情况都是比例为1,即无需压缩,原样输出
ratio = 1;
}
let canvas = document.createElement('canvas');
let tempCanvas = document.createElement('canvas');
let context = canvas.getContext('2d');
canvas.width = outputW;
canvas.height = outputH;
context.fillStyle = '#FFFFFF';
context.fillRect(0, 0, canvas.width, canvas.height);
// 计算需要拆分的瓦片数量
const pieceCount = Math.ceil(imageSize / PIECE_SIZE);
if (pieceCount > 1) {
// 如果瓦片数量大于1,就需要进行瓦片绘制到一个临时tempCanvas里
// 再把临时的tempCanvas根据压缩后换算的长度/宽度,x轴位置和y轴位置,绘制到结果的canvas位置上
// 直到所有瓦片按照结果尺寸和瓦片数量拼接完毕
const pieceW = Math.ceil(canvas.width / pieceCount);
const pieceH = Math.ceil(canvas.height / pieceCount);
tempCanvas.width = pieceW;
tempCanvas.height = pieceH;
let tempContext = tempCanvas.getContext('2d');
const sw = pieceW * ratio;
const sh = pieceH * ratio;
const dw = pieceW;
const dh = pieceH;
for(let i = 0; i < pieceCount; i++) {
for(let j = 0; j < pieceCount; j++) {
const sx = i * pieceW * ratio;
const sy = j * pieceH * ratio;
tempContext.drawImage(img, sx, sy, sw, sh, 0, 0, dw, dh);
context.drawImage(tempCanvas, i * pieceW, j * pieceH, dw, dh);
}
}
tempContext.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
tempCanvas.width = 0;
tempCanvas.height = 0;
tempCanvas = null;
} else {
// 如果瓦片数量小于1,即可以安全绘制到结果canvas里
context.drawImage(img, 0, 0, outputW, outputH);
}
// 将结果的canvas输出成base64
// 上述压缩的是尺寸,这里使用 encoderOptions 压缩的是质量,可以理解为清晰度
const base64 = canvas.toDataURL(type, encoderOptions);
context.clearRect(0, 0, canvas.width, canvas.height);
canvas.width = 0;
canvas.height = 0;
canvas = null;
return base64;
}
https://github.com/chenshenhai/canvas-note/blob/master/demo/chapter-03/05/
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>chapter</title>
<link rel="stylesheet" href="./css/index.css">
</head>
<body>
<div class="canvas-list">
<div class="canvas-bg">
<canvas id="canvas-1"></canvas>
</div>
<div class="canvas-bg">
<canvas id="canvas-2"></canvas>
</div>
</div>
<div id="info"></div>
<script type="module" src="./js/index.js"></script>
</body>
</html>
import { getImageBySrc } from './../../../lib/util/file.js';
import { compressImage } from './../../../lib/util/compress.js';
(async function() {
// 绘制原始图像
const canvas1 = document.getElementById('canvas-1');
const ctx1 = canvas1.getContext('2d');
const img = await getImageBySrc('./../../image/pexels-photo-001.jpg');
canvas1.width = img.width;
canvas1.height = img.height;
ctx1.drawImage(img, 0, 0);
// 绘制压缩后图像
const canvas2 = document.getElementById('canvas-2');
const ctx2 = canvas2.getContext('2d');
const compressedImgSrc = compressImage(img);
const timeBefore = new Date().getTime();
const compressedImg = await getImageBySrc(compressedImgSrc);
const timeAfter = new Date().getTime();
canvas2.width = compressedImg.width;
canvas2.height = compressedImg.height;
ctx2.drawImage(compressedImg, 0, 0);
// 前后尺寸结果
const originSize = img.width * img.height;
const compressedSize = compressedImg.width * compressedImg.height;
const infoText = `
原始尺寸大小: ${img.width} * ${img.height} = ${originSize} 像素
<br>
压缩后尺寸大小: ${compressedImg.width} * ${compressedImg.height} = ${compressedSize} 像素
<br/>
压缩过程耗时: ${timeAfter - timeBefore} ms
`
document.getElementById('info').innerHTML = infoText;
// console.log('originSize = ', originSize);
// console.log('compressedSize = ', compressedSize);
})();
无论是质量压缩还是尺寸压缩,在不同浏览器处理的效果都有一定的差距,压缩结果的大小不一定一致。