怎么用 canvas 绘制页面中 img 标签(缩放后)的图片内容?

注意图片是使用 object-fit: cover 这种方式缩放过的
我试过下面的常规方法,但是得到的图片不太一样,因为浏览器缩放过的原因:

export const ImageToCanvas = (image: HTMLImageElement) => {
  const canvas = document.createElement('canvas');

  canvas.width = image.width;
  canvas.height = image.height;
  canvas.getContext('2d')?.drawImage(image, 0, 0);

  return canvas;
};

export const canvasToImage = (canvas: HTMLCanvasElement) => {
  const image = new Image();
  image.src = canvas.toDataURL('image/png');

  return image;
};

export const downloadImage = (image: HTMLImageElement, name: string) => {
  const link = document.createElement('a');

  link.download = name;
  link.href = image.src;
  link.click();
};

image.png

阅读 3.1k
3 个回答
export const ImageToCanvas = (image: HTMLImageElement) => {
  const canvas = document.createElement('canvas');
  const context = canvas.getContext('2d');

  canvas.width = image.width;
  canvas.height = image.height;

  // 计算缩放比例和剪裁位置
  const imgRatio = image.naturalWidth / image.naturalHeight;
  const canvasRatio = canvas.width / canvas.height;
  let sx, sy, sw, sh;
  if (imgRatio > canvasRatio) {
    sw = image.naturalHeight * canvasRatio;
    sh = image.naturalHeight;
    sx = (image.naturalWidth - sw) / 2;
    sy = 0;
  } else {
    sw = image.naturalWidth;
    sh = image.naturalWidth / canvasRatio;
    sx = 0;
    sy = (image.naturalHeight - sh) / 2;
  }

  context?.drawImage(image, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height);

  return canvas;
};

找了一个库,可以试试

https://www.npmjs.com/package/canvas-object-fit

import Canvas from 'canvas';
import {drawImage} from 'canvas-object-fit';
 
const [width, height] = [200, 200];
const canvas = new Canvas(width, height);
const context = canvas.getContext('2d');
const image = new Canvas.Image();
image.src = await fs.readFileAsync(`${fixturesPath}/image.jpg`);
drawImage(context, image, 0, 0, canvas.width, canvas.height, {objectFit: 'cover'});
const buffer = canvas.toBuffer('png');
/**
 * @param {{width: number, height: number}} containerSize 图片容器的宽高,单位px
 * @param {{width: number, height: number}} imageSize 图片宽高,单位px
 * @param {'fill'|'cover'|'contain'|'none'} [objectFit]
 * @return {[sx: number, sy: number, sWidth: number, sHeight: number, dx: number, dy: number, dWidth: number, dHeight: number]}
 */
export function getDrawImagePosition(containerSize, imageSize, objectFit = 'fill') {
  // 图片宽高比
  const imageRatio = imageSize.width / imageSize.height

  // 图片按容器宽高比缩放至占满整个容器
  if (objectFit === 'fill') {
    return [0, 0, imageSize.width, imageSize.height, 0, 0, containerSize.width, containerSize.height]
  }

  // 保持宽高比缩放,图片最长边占满容器,不管图片是否足以被容器包含
  if (objectFit === 'contain') {
    // 确定图片最长边
    const longSide = imageRatio >= 1 ? 'width' : 'height'
    const scaleRatio = containerSize[longSide] / imageSize[longSide]
    // 最终绘制的图片尺寸
    const scaleWidth = imageSize.width * scaleRatio
    const scaleHeight = imageSize.height * scaleRatio
    return [
      0,
      0,
      imageSize.width,
      imageSize.height,
      longSide === 'width' ? 0 : (containerSize.width - scaleWidth) / 2,
      longSide === 'height' ? 0 : (containerSize.height - scaleHeight) / 2,
      scaleWidth,
      scaleHeight
    ]
  }

  // 保持宽高比缩放,直到恰好占满整个容器,居中显示,有可能发生裁剪,不管图片是否足以被容器包含
  if (objectFit === 'cover') {
    const longSide = imageRatio >= 1 ? 'width' : 'height'
    const shortSide = imageRatio >= 1 ? 'height' : 'width'
    let scaleRatio = containerSize[shortSide] / imageSize[shortSide]
    // 最终绘制的图片尺寸
    const scaleSize = {
      width: imageSize.width * scaleRatio,
      height: imageSize.height * scaleRatio
    }
    // 缩放后长边可能未占满容器,需要再次缩放
    if (scaleSize[longSide] < containerSize[longSide]) {
      const ratio = containerSize[longSide] / scaleSize[longSide]
      scaleSize.width *= ratio
      scaleSize.height *= ratio
      scaleRatio = scaleSize.width / imageSize.width
    }

    // 原图映射到画布上的左上角相对于原图尺寸裁剪的距离
    const clipWidth = (scaleSize.width - containerSize.width) / 2 / scaleRatio
    const clipHeight = (scaleSize.height - containerSize.height) / 2 / scaleRatio

    return [
      clipWidth,
      clipHeight,
      imageSize.width - clipWidth,
      imageSize.height - clipHeight,
      0,
      0,
      containerSize.width,
      containerSize.height
    ]
  }

  // 保持原尺寸,居中显示,有可能发生裁剪
  if (objectFit === 'none') {
    // 确定是容器还是图片更大
    const horizontalDiff = (containerSize.width - imageSize.width) / 2
    const verticalDiff = (containerSize.height - imageSize.height) / 2

    return [
      horizontalDiff >= 0 ? 0 : -horizontalDiff,
      verticalDiff >= 0 ? 0 : -verticalDiff,
      horizontalDiff >= 0 ? imageSize.width : imageSize.width + 2 * horizontalDiff,
      verticalDiff >= 0 ? imageSize.height : imageSize.height + 2 * verticalDiff,
      horizontalDiff >= 0 ? horizontalDiff : 0,
      verticalDiff >= 0 ? verticalDiff : 0,
      horizontalDiff >= 0 ? imageSize.width : containerSize.width,
      verticalDiff >= 0 ? imageSize.height : containerSize.height
    ]
  }

  throw new Error('unknown objectFit:' + objectFit)
}

返回的参数就是drawImage的后8个参数

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题