前端图片处理技术:Canvas API 详解

工具相关 ·

前言

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 不同图片源的加载方式

图片源加载方式适用场景
URLimg.src = 'url'网络图片
Data URLimg.src = 'data:image/...'Base64 编码图片
Blob URLimg.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);

参数详解:

参数类型必填说明
callbackFunction是回调函数,接收 Blob 参数
typeString否MIME 类型,默认 'image/png'
qualityNumber否质量 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 对比

特性toBlobtoDataURL
返回类型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 兼容性

APIChromeFirefoxSafariEdgeIE
getContext('2d')4+3.6+3.1+12+9+
drawImage4+3.6+3.1+12+9+
toBlob50+18+11+79+10+*
toDataURL4+3.6+3.1+12+9+
getImageData4+3.6+3.1+12+9+
imageSmoothingQuality46+未支持未支持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 进行图片处理的关键:

核心要点:

  1. drawImage() 将图片绘制到 Canvas
  2. toBlob() 以异步方式导出为各种格式(推荐)
  3. toDataURL() 以同步方式导出为 Base64 字符串
  4. 质量参数仅对 JPG 和 WebP 有效
  5. 处理透明背景时需要先填充背景色
  6. 注意大图片的内存管理
  7. 使用 polyfill 处理兼容性问题

性能建议:

  • 大图片优先使用 toBlob() 而非 toDataURL()
  • 使用 OffscreenCanvas 和 Web Worker 避免阻塞主线程
  • 分步缩放比一步大幅缩放质量更好
  • 及时释放 Object URL 和 Blob 引用
阅读 15