Canvas 2D 图像处理:浏览器端像素操作的原理与实践
Canvas 在图像处理中的角色
浏览器提供了三种图像处理路径:
| 方案 | 性能 | 灵活性 | 适用 |
|---|---|---|---|
| CSS filter | 高(GPU) | 低(预设滤镜) | 简单视觉效果 |
| Canvas 2D | 中 | 高(像素级) | 压缩、裁剪、格式转换 |
| WebGL | 最高 | 高(需 shader) | 复杂滤镜、实时处理 |
工具库的图片工具(压缩、裁剪、格式转换、水印)全部基于 Canvas 2D API。
基本处理流程
// 1. 加载图片
const img = new Image();
img.src = URL.createObjectURL(file);
await img.decode();
// 2. 创建 Canvas 并绘制
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0);
// 3. 导出为目标格式
const blob = await new Promise<Blob>((resolve) =>
canvas.toBlob(resolve, 'image/webp', 0.85) // 质量 85%
);
图片压缩原理
Canvas 压缩本质是重新编码:
原始 PNG (2MB, 无损)
↓ drawImage 到 Canvas
↓ toBlob('image/jpeg', 0.8)
压缩 JPEG (400KB, 有损 80% 质量)
| 格式 | 压缩方式 | 典型压缩率 | 透明度 |
|---|---|---|---|
| JPEG | 有损 DCT | 60-80% | ❌ |
| WebP | 有损/无损 | 70-90% | ✅ |
| PNG | 无损 DEFLATE | 10-30% | ✅ |
| AVIF | 有损/无损 | 80-95% | ✅ |
工具库 图片压缩工具 支持批量 WebP/JPEG 压缩,默认质量 85% 在体积与画质间取得最佳平衡。
像素级操作
通过 ImageData 直接操作 RGBA 像素:
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data; // Uint8ClampedArray, 每 4 字节 = 1 像素
for (let i = 0; i < data.length; i += 4) {
// 灰度化
const avg = (data[i] + data[i+1] + data[i+2]) / 3;
data[i] = data[i+1] = data[i+2] = avg;
}
ctx.putImageData(imageData, 0, 0);
OffscreenCanvas 性能优化
主线程 Canvas 操作会阻塞 UI。OffscreenCanvas 可在 Worker 中运行:
// worker.ts
const offscreen = new OffscreenCanvas(width, height);
const ctx = offscreen.getContext('2d')!;
ctx.drawImage(imageBitmap, 0, 0);
const blob = await offscreen.convertToBlob({ type: 'image/webp', quality: 0.85 });
postMessage({ blob }, [blob]); // Transferable
批量处理 50 张图片时,OffscreenCanvas + Worker 可将主线程阻塞从 10s 降至 0。
常见问题
为什么压缩后图片变模糊?
JPEG/WebP 有损压缩会丢失高频细节。提高质量参数(0.9+)或使用 PNG/WebP 无损模式。
Canvas 有尺寸限制吗?
大多数浏览器限制 Canvas 单边最大 16384px,总面积约 268MP。超过此限制需要分块处理。
如何处理 EXIF 方向信息?
手机拍摄的照片可能含 EXIF Orientation。绘制前需读取 EXIF 并旋转 Canvas,否则图片方向错误。工具库 图片格式转换 会自动处理 EXIF。
合成操作与混合模式
Canvas 2D 支持 globalCompositeOperation 设置图层混合模式,实现丰富效果:
| 混合模式 | 效果 | 适用场景 |
|---|---|---|
source-over |
新图层覆盖旧图层 | 默认绘制 |
destination-over |
新图层置于底层 | 背景填充 |
source-in |
只保留新图层与旧图层重叠部分 | 蒙版效果 |
multiply |
正片叠底(变暗) | 图片叠加 |
screen |
滤色(变亮) | 高光效果 |
xor |
异或(重叠区域透明) | 抠图辅助 |
// 圆形裁剪(头像裁切)
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.clip(); // 裁剪路径
ctx.drawImage(img, 0, 0);
文字水印实现
工具库的批量水印工具基于 Canvas 文字渲染实现:
function addWatermark(
img: HTMLImageElement,
text: string,
options: { opacity?: number; fontSize?: number; rotate?: number }
) {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d')!;
// 先绘制原图
ctx.drawImage(img, 0, 0);
// 设置水印样式
ctx.globalAlpha = options.opacity ?? 0.3;
ctx.font = `${options.fontSize ?? 36}px sans-serif`;
ctx.fillStyle = '#ffffff';
ctx.shadowColor = 'rgba(0,0,0,0.5)';
ctx.shadowBlur = 4;
// 平铺水印
const stepX = img.width / 4;
const stepY = img.height / 3;
ctx.save();
ctx.rotate((options.rotate ?? -20) * Math.PI / 180);
for (let y = -img.height; y < img.height * 2; y += stepY) {
for (let x = -img.width; x < img.width * 2; x += stepX) {
ctx.fillText(text, x, y);
}
}
ctx.restore();
return canvas;
}
批量处理与 Worker 池模式
处理 100 张图片时,单线程方案太慢。工具库采用了 Worker 池 + OffscreenCanvas 模式:
// Worker 池管理器
class ImageWorkerPool {
private workers: Worker[] = [];
private queue: Array<() => void> = [];
constructor(poolSize = navigator.hardwareConcurrency || 4) {
for (let i = 0; i < poolSize; i++) {
this.workers.push(new Worker(new URL('./image-worker.ts', import.meta.url)));
}
}
async process(imageFile: File, options: ProcessOptions): Promise<Blob> {
return new Promise((resolve) => {
const run = (worker: Worker) => {
worker.postMessage({ file: imageFile, options });
worker.onmessage = ({ data }) => {
resolve(data.blob);
if (this.queue.length > 0) this.queue.shift()!();
};
};
const idle = this.workers.find(w => !w['busy']);
if (idle) { idle['busy'] = true; run(idle); }
else { this.queue.push(() => run(this.workers.find(w => !w['busy'])!)); }
});
}
}
ImageBitmap 零拷贝优化
对于不需要持久操作 ImageData 的场景,createImageBitmap() 比 drawImage + getImageData 更高效:
// ✅ 零拷贝:适合缩放、简单转换
const bitmap = await createImageBitmap(file, {
resizeWidth: 800,
resizeHeight: 600,
resizeQuality: 'high'
});
ctx.drawImage(bitmap, 0, 0);
bitmap.close(); // 务必释放
// ❌ 需要操作像素时必须用 getImageData
const imageData = ctx.getImageData(0, 0, w, h);
总结
Canvas 2D 是浏览器端图像处理的标准方案。理解 drawImage → toBlob 的编码链路、ImageData 的像素操作、以及 OffscreenCanvas 的 Worker 卸载,是构建高性能图片工具的核心能力。
本站提供浏览器本地工具,免注册即可试用 →