Canvas转图片下载功能实现

1,955 阅读4分钟

前言

工作中遇到了需要将统计模块的图表下载的需求,查阅资料后发现直接把 canvas 转为图片即可实现,但是下载的图片除了图表中的线条外背景颜色是透明的,经过后续的改进能自定义下载图片的背景颜色以及背景图片,并扩展了多个 canvas 合并下载功能。本文为记录此需求的实现方式。

前置知识点

toDataURL()

HTMLCanvasElement.toDataURL() 方法用于将canvas中的图像内容转换为 base64 格式图片,默认为 PNG 格式。

可用于将图片转成 base64 格式

let canvas = document.getElementsByTagName('canvas')[0];

/**
 * @param {String} type:图片类型
 * @param {Number} encoderOptions:图片质量 0-1,默认值:0.92
 * @return {String} base64格式图片数据
 */
canvas.toDataURL(type, encoderOptions);

drawImage()

CanvasRenderingContext2D.drawImage() 用于在 canvas 上绘制图像。

三种调用语法:

  • 绘制原图:context.drawImage(image, x, y)
  • 绘制指定大小的原图:context.drawImage(img, dx, dy, dWidth, dHeight)
  • 绘制裁剪后的图片:context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
let canvas = document.getElementsByTagName('canvas')[0];
let context = canvas.getContext('2d');

/**
 * @param {ImageSource} image:图像源
 * 例:CSSImageValue || HTMLImageElement || SVGImageElement || HTMLVideoElement || HTMLCanvasElement || ImageBitmap  || offscreenCanvas
 * @param {Number} sx:开始裁剪图像的 x 坐标位置
 * @param {Number} sy:开始裁剪图像的 y 坐标位置
 * @param {Number} sWidth:裁剪出的图像宽度
 * @param {Number} sHeight:裁剪出的图像高度
 * @param {Number} dx:在画布上放置图像的 x 坐标位置
 * @param {Number} dy:在画布上放置图像的 y 坐标位置
 * @param {Number} dWidth:在画布上绘制图像宽度
 * @param {Number} dHeight:在画布上绘制图像高度
 */
context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

getImageData()

CanvasRenderingContext2D.getImageData() 返回 ImageData 对象,包含 canvas 区域类的像素数据。

let canvas = document.getElementsByTagName('canvas')[0];
let context = canvas.getContext('2d');

/**
 * @param {Number} sx:将要提取图像数据矩形区域左上角x坐标
 * @param {Number} sy:将要提取图像数据矩形区域左上角y坐标
 * @param {Number} sw:矩形区域宽度
 * @param {Number} sh:矩形区域高度
 * @return {ImageData} canvas像素数据
 */
context.getImageData(sx, sy, sw, sh);

ImageData.data:一维数组,包含RGBA顺序的数据,0~255

ImageData.height:使用像素描述 ImageData 的实际高度

ImageData.width:使用像素描述 ImageData 的实际宽度

putImageData()

CanvasRenderingContext2D.putImageData() 将 ImageData 对象绘制到 cavas 中,如果提供一个绘制过的 canvas,则只绘制该 canvas 的像素

let canvas = document.getElementsByTagName('canvas')[0];
let context = canvas.getContext('2d');

/**
 * @param {iamgeData} iamgeData:包含像素值的数组对象
 * @param {Number} dx:x轴方向上的偏移量
 * @param {Number} dy:y轴方向上的偏移量
 */
context.putImageData(iamgeData, dx, dy);

还有一些可选参数,详情 CanvasRenderingContext2D.putImageData() - Web API 接口参考 | MDN (mozilla.org)

globalCompositeOperation

CanvasRenderingContext2D.globalCompositeOperation 绘制新形状时合成操作的类型,默认值为 'source-over'

具体如何执行合成操作,可查看 CanvasRenderingContext2D.globalCompositeOperation - Web API 接口参考 | MDN (mozilla.org)

Canvas转图片并添加背景

添加背景颜色

/**
 * canvas 增加背景颜色并生成图片
 * @param {element} cav:canvas元素
 * @param {String} backgroundColor:背景颜色
 * @return {String} imageData:base64格式图片数据
 */
function canvasToImageByBackgroundColor(cav, backgroundColor) {
	// 缓存 canvas 宽高
    const height = cav.height;
    const width = cav.width;
    
    const context = cav.getContext('2d');
    
    // 获取 canvas 上的像素信息
    const oldImage = context.getImageData(0, 0, height, width);
    // 保存 canvas 绘制新形状时合成操作的类型
    const compositeOperation = context.globalCompositeOperation;
    
    if (backgroundColor) {
        // destination-over:在原有图形后面绘制新的图形
        context.globalCompositeOperation = 'destination-over';
        
        // 绘制背景颜色
        context.fillStyle = backgroundColor;
        context.fillRect(0, 0, width, height);
    }
    
    // 获取 canvas 中图像内容,格式为 base64
    let imageData = cav.toDataUrl('image/png');
    
    if (backgroundColor) {
        // 擦除 canvas 中的内容
        context.clearRect(0, 0, width, height);
        // 还原增加背景色之前的 canvas
        context.putImageData(oldImage, 0, 0);
        context.globalCompositeOperation = compositeOperation;
    }
    
    return imageData;
}

添加背景图片

/**
 * canvas 增加背景图片并生成图片
 * @param {element} cav:canvas元素image
 * @param {String} image:背景图片
 * @return {String} imageData:base64格式图片数据
 */
function canvasToImageByBackgroundImage(cav, image) {
    const height = cav.height;
    const width = cav.width;
    
    let imageEle = document.createElement('image');
    imageEle.src = image;
    
    let newCanvas = document.createElement('canvas');
    let newContext = newCanvas.getContext('2d');
    newCanvas.height = height;
    newCanvas.width = width;
    newContext.drawImage(imageEle, 0, 0, width, height);
    newContext.drawImage(cav, 0, 0);
 
    return newCanvas.toDataURL('image/png');
}

多个Canvas合并

/**
 * 多个 canvas 合并为一个
 * @param {HTMLCollection} canvasList:canvas元素列表
 * @param {String} orientation:'row' || 'columns' 排序方式
 * @return {HTMLElement} 合并后的 canvas
 */
function mergeCanvasList(canvasList, orientation = 'row') {
  
  if(!canvasList || !canvasList.length) {
    return null;
  }
  
  // 创建合并后的canvas
  let totalCanvas = document.createElement('canvas');
  let totalContext = totalCancas.getContext('2D');
  // 初始化canvas宽高
  totalCanvas.width = canvasList[0].width;
  totalCanvas.height = canvasList[0].height;
  
  for (let index = 0; index < canvasList.length; index++) {
    const element = canvasList[index];
    let elementWidth = element.clientWidth;
    let elementHeight = element.clientHeight;
    
    if (index === 0) {
      totalContext.drawImage(element, 0, 0, elementWidth, elementHeight);
    } else {
      let width =  totalCanvas.width;
      let height = totalCanvas.height;
      
      // 保存已绘制的图像,防止改变canvas宽高后canvas重新绘制导致丢失之前绘制内容
      let canvasData = totalContext.getImageData(0, 0, width, height);
      
      if (orientation === 'row') {
        // 加宽 canvas
        totalCanvas.width = width + elementWidth;
        // 判断绘制的图像高度是否大于canvas高度,是则加高
        if (elementHeight > height) {
          totalCanvas.height = elementHeight;
        }
        // 将之前绘制的图像重新绘制到canvas
        context.putImageData(canvasData, 0, 0);
        context.drawIamge(element, width, 0, elementWidth, elementHeight)
      } else {
        // 加高 canvas
        totalCanvas.height = height + elementHeight;
        // 判断绘制的图像宽度是否大于canvas宽度,是则加宽
        if (elementWidth > width) {
          totalCanvas.width = elementWidth;
        }
        // 将之前绘制的图像重新绘制到canvas
        context.putImageData(canvasData, 0, 0);
        context.drawIamge(element, 0, height, elementWidth, elementHeight)
      }
    }
  }
  return totalCanvas
}

图片下载

function tagAToDownload({url, title = '', target = '_blank'} = {}) {
  let tagA = document.createElement('a');
  tagA.setAttribute('href', url);
  tagA.setAttribute('download', title);
  tagA.setAttribute('target', target);
  document.body.appendChild(tagA);
  tagA.click();
  document.body.removeChild(tagA);
}

// tagAToDownload({url: canvasToImageByBackgroundColor(CanvasList, '#fff'), title: '下载文件名称'});
// tagAToDownload({url: canvasToImageByBackgroundImage(Canvas, imageUrl), title: '下载文件名称'});
// tagAToDownload({url: mergeCanvasList([CanvasList], 'row'), title: '下载文件名称'});

结语

感谢各位能看到这里!如果有所收获就点个赞再走吧,如果有更简单的实现方式或是其他的需求欢迎在评论区交流。