前言
Canvas API 是现代浏览器提供的强大图形处理接口,它不仅可以用于绘制图形、创建动画,还是前端图片处理的核心技术。从图片格式转换到滤镜效果,从裁剪缩放到水印添加,Canvas API 都能胜任。
本文将深入讲解 Canvas API 在图片处理中的应用,包括图片加载、格式转换、质量控制、浏览器兼容性等关键知识。
一、Canvas 基础
1.1 什么是 Canvas?
Canvas(画布)是 HTML5 引入的 <canvas> 元素,它提供了一个位图绘图表面。通过 JavaScript 获取其 2D 渲染上下文(CanvasRenderingContext2D),就可以在上面绘制各种图形、文字和图片。
<!-- 基本 Canvas 元素 -->
<canvas id="myCanvas" width="800" height="600">
您的浏览器不支持 Canvas
</canvas>
// 获取 2D 渲染上下文
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
1.2 Canvas 的核心特性
| 特性 | 说明 |
|---|---|
| 像素级操作 | 可以读取和修改每个像素的颜色值 |
| 2D 渲染 | 支持矩形、路径、文字、图片绘制 |
| 变换支持 | 平移、旋转、缩放 |
| 离屏渲染 | 可以不附加到 DOM 中使用 |
| 导出能力 | 可导出为 PNG、JPG、WebP、BMP 格式 |
二、图片加载与绘制
2.1 基本图片绘制
Canvas 提供 drawImage 方法将图片绘制到画布上:
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
// 创建 Image 对象
var img = new Image();
img.crossOrigin = 'anonymous'; // 如果需要跨域使用
img.onload = function() {
// 设置 Canvas 尺寸
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
// 基本绘制:将图片完整绘制到 Canvas
ctx.drawImage(img, 0, 0);
// 带缩放的绘制
// drawImage(image, dx, dy, dWidth, dHeight)
ctx.drawImage(img, 0, 0, canvas.width / 2, canvas.height / 2);
// 裁剪绘制:从源图裁剪一个区域绘制到 Canvas
// drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
ctx.drawImage(img,
100, 100, 200, 200, // 源图裁剪区域
0, 0, 200, 200 // Canvas 绘制区域
);
};
img.src = 'example.jpg';
2.2 不同图片源的加载方式
| 图片源 | 加载方式 | 适用场景 |
|---|---|---|
| URL | img.src = 'url' | 网络图片 |
| Data URL | img.src = 'data:image/...' | Base64 编码图片 |
| Blob URL | img.src = URL.createObjectURL(blob) | File/Blob 对象 |
| File 对象 | 先通过 FileReader 转为 Data URL | 用户上传的图片 |
// 方式一:从 URL 加载
function loadFromURL(url) {
return new Promise(function(resolve, reject) {
var img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function() { resolve(img); };
img.onerror = function() { reject(new Error('图片加载失败')); };
img.src = url;
});
}
// 方式二:从 File 对象加载(用户上传)
function loadFromFile(file) {
return new Promise(function(resolve, reject) {
var reader = new FileReader();
reader.onload = function(e) {
var img = new Image();
img.onload = function() { resolve(img); };
img.onerror = function() { reject(new Error('图片解析失败')); };
img.src = e.target.result;
};
reader.onerror = function() { reject(new Error('文件读取失败')); };
reader.readAsDataURL(file);
});
}
// 方式三:从 Blob 加载
function loadFromBlob(blob) {
return new Promise(function(resolve, reject) {
var url = URL.createObjectURL(blob);
var img = new Image();
img.onload = function() {
URL.revokeObjectURL(url); // 加载完成后释放
resolve(img);
};
img.onerror = function() {
URL.revokeObjectURL(url);
reject(new Error('图片加载失败'));
};
img.src = url;
});
}
三、图片格式转换核心
3.1 toBlob() 方法
toBlob() 是 Canvas 格式转换的核心方法,它将 Canvas 内容导出为指定格式的 Blob 对象:
canvas.toBlob(callback, type, quality);
参数详解:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| callback | Function | 是 | 回调函数,接收 Blob 参数 |
| type | String | 否 | MIME 类型,默认 'image/png' |
| quality | Number | 否 | 质量 0-1,仅对 JPG/WebP 有效 |
// 转换为 PNG(无损,不支持质量参数)
canvas.toBlob(function(pngBlob) {
console.log('PNG 大小:', pngBlob.size);
console.log('MIME 类型:', pngBlob.type); // 'image/png'
}, 'image/png');
// 转换为 JPG(有损,支持质量参数)
canvas.toBlob(function(jpgBlob) {
console.log('JPG 大小:', jpgBlob.size);
console.log('MIME 类型:', jpgBlob.type); // 'image/jpeg'
}, 'image/jpeg', 0.85);
// 转换为 WebP(有损/无损,支持质量参数)
canvas.toBlob(function(webpBlob) {
console.log('WebP 大小:', webpBlob.size);
console.log('MIME 类型:', webpBlob.type); // 'image/webp'
}, 'image/webp', 0.80);
// 转换为 BMP
canvas.toBlob(function(bmpBlob) {
console.log('BMP 大小:', bmpBlob.size);
console.log('MIME 类型:', bmpBlob.type); // 'image/bmp'
}, 'image/bmp');
3.2 toDataURL() 方法
toDataURL() 将 Canvas 内容导出为 Data URL(Base64 编码的字符串):
var dataURL = canvas.toDataURL(type, quality);
// 导出为 PNG Data URL
var pngDataUrl = canvas.toDataURL('image/png');
// 结果类似:data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...
// 导出为 JPG Data URL
var jpgDataUrl = canvas.toDataURL('image/jpeg', 0.8);
// 结果类似:data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ...
// 可以直接用于 <img> 标签
document.getElementById('preview').src = pngDataUrl;
3.3 toBlob vs toDataURL 对比
| 特性 | toBlob | toDataURL |
|---|---|---|
| 返回类型 | Blob 对象 | 字符串(Data URL) |
| 执行方式 | 异步(回调) | 同步(直接返回) |
| 内存效率 | 高(不创建大字符串) | 低(Base64 增大约 33%) |
| 大图片表现 | 不会阻塞 UI | 可能导致 UI 卡顿 |
| 下载方式 | URL.createObjectURL(blob) | 直接赋值给 <a> 的 href |
| 推荐使用 | 大图片、批量处理 | 小图片、需要嵌入 HTML |
性能对比示例:
// 处理一张 4000x3000 的图片
// toBlob: ~200ms,内存峰值 ~50MB
// toDataURL: ~350ms,内存峰值 ~200MB(Base64 字符串占大量内存)
// 推荐:大图片使用 toBlob
canvas.toBlob(function(blob) {
var url = URL.createObjectURL(blob);
// 使用 url 进行下载或预览
}, 'image/jpeg', 0.85);
// 不推荐:大图片使用 toDataURL
// var dataUrl = canvas.toDataURL('image/jpeg', 0.85);
// 这会在内存中创建一个非常大的字符串
四、透明背景处理
4.1 问题描述
当将带有透明背景的图片(如 PNG)转换为不支持透明度的格式(如 JPG)时,透明区域会变成黑色或不可预测的颜色。
4.2 解决方案
function convertWithBackground(img, targetFormat, quality, bgColor) {
bgColor = bgColor || '#FFFFFF'; // 默认白色背景
var canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
var ctx = canvas.getContext('2d');
// 对于不支持透明的格式,先填充背景色
if (targetFormat === 'image/jpeg' || targetFormat === 'image/bmp') {
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// 绘制原图
ctx.drawImage(img, 0, 0);
// 导出
return new Promise(function(resolve) {
if (targetFormat === 'image/jpeg' || targetFormat === 'image/webp') {
canvas.toBlob(resolve, targetFormat, quality);
} else {
canvas.toBlob(resolve, targetFormat);
}
});
}
// 使用示例
convertWithBackground(img, 'image/jpeg', 0.85, '#FFFFFF')
.then(function(blob) {
// blob 是 JPG 格式,透明区域已被填充为白色
});
4.3 常用背景色选择
| 背景色 | 色值 | 适用场景 |
|---|---|---|
| 白色 | #FFFFFF | 产品图、证件照 |
| 黑色 | #000000 | 艺术照片、暗色调 |
| 灰色 | #F5F5F5 | 通用背景 |
| 自定义 | 任意色值 | 匹配网站主题色 |
五、图片缩放与裁剪
5.1 等比例缩放
function resizeImage(img, maxWidth, maxHeight) {
var width = img.naturalWidth;
var height = img.naturalHeight;
// 计算缩放比例
var ratio = Math.min(maxWidth / width, maxHeight / height);
// 如果图片小于目标尺寸,不放大
if (ratio >= 1) {
ratio = 1;
}
var newWidth = Math.round(width * ratio);
var newHeight = Math.round(height * ratio);
var canvas = document.createElement('canvas');
canvas.width = newWidth;
canvas.height = newHeight;
var ctx = canvas.getContext('2d');
// 设置高质量缩放
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, 0, 0, newWidth, newHeight);
return canvas;
}
5.2 裁剪为固定比例
function cropToRatio(img, targetWidth, targetHeight) {
var canvas = document.createElement('canvas');
canvas.width = targetWidth;
canvas.height = targetHeight;
var ctx = canvas.getContext('2d');
// 计算裁剪区域(居中裁剪)
var srcRatio = img.naturalWidth / img.naturalHeight;
var targetRatio = targetWidth / targetHeight;
var sx, sy, sw, sh;
if (srcRatio > targetRatio) {
// 原图更宽,裁剪左右
sh = img.naturalHeight;
sw = sh * targetRatio;
sx = (img.naturalWidth - sw) / 2;
sy = 0;
} else {
// 原图更高,裁剪上下
sw = img.naturalWidth;
sh = sw / targetRatio;
sx = 0;
sy = (img.naturalHeight - sh) / 2;
}
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, targetWidth, targetHeight);
return canvas;
}
六、像素级操作
6.1 读取像素数据
// 获取 Canvas 上指定区域的像素数据
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// imageData 包含以下属性:
// imageData.data - Uint8ClampedArray,每 4 个值表示一个像素(R, G, B, A)
// imageData.width - 区域宽度
// imageData.height - 区域高度
// 读取第一个像素的颜色
var r = imageData.data[0]; // 红色 (0-255)
var g = imageData.data[1]; // 绿色 (0-255)
var b = imageData.data[2]; // 蓝色 (0-255)
var a = imageData.data[3]; // Alpha (0-255)
6.2 灰度滤镜
function applyGrayscale(canvas) {
var ctx = canvas.getContext('2d');
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
// 使用加权平均计算灰度值
var gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
data[i] = gray; // R
data[i + 1] = gray; // G
data[i + 2] = gray; // B
// data[i + 3] 是 Alpha,不修改
}
ctx.putImageData(imageData, 0, 0);
}
6.3 亮度调整
function adjustBrightness(canvas, factor) {
// factor > 1 变亮,< 1 变暗
var ctx = canvas.getContext('2d');
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
data[i] = Math.min(255, data[i] * factor); // R
data[i + 1] = Math.min(255, data[i + 1] * factor); // G
data[i + 2] = Math.min(255, data[i + 2] * factor); // B
}
ctx.putImageData(imageData, 0, 0);
}
6.4 图片反转
function invertColors(canvas) {
var ctx = canvas.getContext('2d');
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // R
data[i + 1] = 255 - data[i + 1]; // G
data[i + 2] = 255 - data[i + 2]; // B
}
ctx.putImageData(imageData, 0, 0);
}
七、浏览器兼容性
7.1 Canvas API 兼容性
| API | Chrome | Firefox | Safari | Edge | IE |
|---|---|---|---|---|---|
| getContext('2d') | 4+ | 3.6+ | 3.1+ | 12+ | 9+ |
| drawImage | 4+ | 3.6+ | 3.1+ | 12+ | 9+ |
| toBlob | 50+ | 18+ | 11+ | 79+ | 10+* |
| toDataURL | 4+ | 3.6+ | 3.1+ | 12+ | 9+ |
| getImageData | 4+ | 3.6+ | 3.1+ | 12+ | 9+ |
| imageSmoothingQuality | 46+ | 未支持 | 未支持 | 79+ | 未支持 |
*IE 10 的 toBlob 需要添加 msToBlob 前缀。
7.2 toBlob 兼容性处理
// toBlob polyfill
if (!HTMLCanvasElement.prototype.toBlob) {
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
value: function(callback, type, quality) {
var dataURL = this.toDataURL(type, quality);
var byteString = atob(dataURL.split(',')[1]);
var mimeString = dataURL.split(',')[0].split(':')[1].split(';')[0];
var ab = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(ab);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
var blob = new Blob([ab], { type: mimeString });
callback(blob);
}
});
}
7.3 格式支持检测
// 检测浏览器支持的输出格式
function getSupportedFormats() {
var canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
var formats = {};
var mimeTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/bmp'];
mimeTypes.forEach(function(mime) {
canvas.toBlob(function(blob) {
formats[mime] = blob && blob.type === mime;
}, mime, 0.5);
});
return formats;
}
7.4 高质量缩放兼容
imageSmoothingQuality 属性在某些浏览器中不受支持,可以通过多次缩放来模拟高质量缩放:
// 高质量缩放兼容方案
function highQualityResize(sourceCanvas, targetWidth, targetHeight) {
var ctx = sourceCanvas.getContext('2d');
// 尝试使用高质量缩放
if ('imageSmoothingQuality' in ctx) {
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
}
var destCanvas = document.createElement('canvas');
destCanvas.width = targetWidth;
destCanvas.height = targetHeight;
var destCtx = destCanvas.getContext('2d');
// 如果缩小比例超过 2 倍,分步缩放以获得更好的质量
var ratioX = sourceCanvas.width / targetWidth;
var ratioY = sourceCanvas.height / targetHeight;
var maxRatio = Math.max(ratioX, ratioY);
if (maxRatio > 2) {
// 分步缩小
var currentCanvas = sourceCanvas;
while (maxRatio > 2) {
var stepCanvas = document.createElement('canvas');
stepCanvas.width = Math.round(currentCanvas.width / 2);
stepCanvas.height = Math.round(currentCanvas.height / 2);
var stepCtx = stepCanvas.getContext('2d');
stepCtx.imageSmoothingEnabled = true;
stepCtx.drawImage(currentCanvas, 0, 0, stepCanvas.width, stepCanvas.height);
currentCanvas = stepCanvas;
maxRatio = maxRatio / 2;
}
destCtx.drawImage(currentCanvas, 0, 0, targetWidth, targetHeight);
} else {
destCtx.drawImage(sourceCanvas, 0, 0, targetWidth, targetHeight);
}
return destCanvas;
}
八、OffscreenCanvas 与 Web Workers
8.1 OffscreenCanvas
OffscreenCanvas 允许在 Web Worker 中使用 Canvas,避免阻塞主线程:
// 主线程
var canvas = document.getElementById('myCanvas');
var offscreen = canvas.transferControlToOffscreen();
var worker = new Worker('image-worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);
// image-worker.js(Web Worker)
self.onmessage = function(e) {
var canvas = e.data.canvas;
var ctx = canvas.getContext('2d');
// 在 Worker 中进行图片处理
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 处理完成后通知主线程
self.postMessage('done');
};
8.2 Worker 中的格式转换
// image-worker.js
self.onmessage = async function(e) {
var imageData = e.data.imageData;
var width = e.data.width;
var height = e.data.height;
var format = e.data.format;
var quality = e.data.quality;
var canvas = new OffscreenCanvas(width, height);
var ctx = canvas.getContext('2d');
// 创建 ImageData 并绘制
var imgData = new ImageData(imageData, width, height);
ctx.putImageData(imgData, 0, 0);
// 转换格式
var blob = await canvas.convertToBlob({
type: format,
quality: quality
});
self.postMessage(blob, [blob]);
};
注意:OffscreenCanvas 和 convertToBlob 的浏览器支持仍在扩展中,使用前需要检测。
九、完整转换工具示例
9.1 封装为工具类
// ImageConverter - 前端图片转换工具类
var ImageConverter = (function() {
// 支持的格式
var SUPPORTED_FORMATS = {
'image/png': { ext: 'png', lossy: false },
'image/jpeg': { ext: 'jpg', lossy: true },
'image/webp': { ext: 'webp', lossy: true },
'image/bmp': { ext: 'bmp', lossy: false }
};
// 转换单张图片
function convert(imageElement, targetFormat, quality) {
return new Promise(function(resolve, reject) {
var formatInfo = SUPPORTED_FORMATS[targetFormat];
if (!formatInfo) {
reject(new Error('不支持的目标格式: ' + targetFormat));
return;
}
var canvas = document.createElement('canvas');
canvas.width = imageElement.naturalWidth;
canvas.height = imageElement.naturalHeight;
var ctx = canvas.getContext('2d');
// 处理透明背景
if (!targetFormat.includes('png') && !targetFormat.includes('webp')) {
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
ctx.drawImage(imageElement, 0, 0);
var callback = function(blob) {
if (blob) {
resolve({
blob: blob,
size: blob.size,
url: URL.createObjectURL(blob),
format: targetFormat,
ext: formatInfo.ext,
originalSize: 0 // 由调用者设置
});
} else {
reject(new Error('转换失败'));
}
};
if (formatInfo.lossy && quality !== undefined) {
canvas.toBlob(callback, targetFormat, quality);
} else {
canvas.toBlob(callback, targetFormat);
}
});
}
// 检测格式支持
function isFormatSupported(mimeType) {
return new Promise(function(resolve) {
var canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
canvas.toBlob(function(blob) {
resolve(!!blob && blob.type === mimeType);
}, mimeType);
});
}
return {
convert: convert,
isFormatSupported: isFormatSupported,
SUPPORTED_FORMATS: SUPPORTED_FORMATS
};
})();
十、总结
Canvas API 为前端图片处理提供了强大的能力。理解以下核心概念是使用 Canvas 进行图片处理的关键:
核心要点:
drawImage()将图片绘制到 CanvastoBlob()以异步方式导出为各种格式(推荐)toDataURL()以同步方式导出为 Base64 字符串- 质量参数仅对 JPG 和 WebP 有效
- 处理透明背景时需要先填充背景色
- 注意大图片的内存管理
- 使用 polyfill 处理兼容性问题
性能建议:
- 大图片优先使用
toBlob()而非toDataURL() - 使用
OffscreenCanvas和 Web Worker 避免阻塞主线程 - 分步缩放比一步大幅缩放质量更好
- 及时释放 Object URL 和 Blob 引用