DOM & BOM – 用 Canvas 修图

前言

以前有写过一篇关于 canvas 处理图片的文章. 非常乱, 这篇做一个整理.

 

参考 

Stack Overflow – HTML5 Canvas Rotate Image

Stack Overflow – How to flip images horizontally with HTML5

crop image 需要的基础知识

 

Import to and Export from Canvas (导入, 导出图片)

fetch an image

const response = await fetch(`https://192.168.1.152:4201/src/images/tifa2.jpg`); // ajax an image
const blob = await response.blob(); // get blob from response 
const image = await createImageBitmap(blob); // convert blob to Image
console.log(`${image.width} x ${image.height}`); // get image info 1600 x 900 

流程 : request image > response to blob > blob to Image

import image to canvas

const canvas = document.createElement('canvas');
[canvas.width, canvas.height] = [image.width, image.height]; // set canvas dimension
const context = canvas.getContext('2d')!;
context.drawImage(image, 0, 0); // drawImage, image can be jpeg, png, webp 等等等
document.body.appendChild(canvas);

canvas 就是一个画布, 我们想把整张图画进去, 所以 canvas 的大小一定要大过图片. 这里我们 set  成一样大小就好.

context.drawImage 就是把 image 画进去 canvas. 这个 image 对象就是上面我们从 blob convert 来的, jpeg, png, webp 等都接受.

export from canvas

canvas.toBlob(blob => {
  const url = window.URL.createObjectURL(blob!);
  const anchor = document.createElement('a');
  anchor.href = url;
  anchor.download = 'tifa.jpg';
  anchor.click();
}, 'image/jpeg');

canvas.toBlob 可以把 canvas 所以内容 convert 成图片. 支持多种格式, 比如 jpeg, png, webp 等.

它还有一个参数可以设置 quality 0 到 1.

如果没有填, 那游览器会用自己的默认值. 我的经验是, 设置 1 通常会比原图还大的多, 0.95 会差不多, 但是我建议还是用游览器默认会比较好.

转换成 blob 以后, 我们就可以做任何后续的处理了, 比如 download or upload.

另外,还有一种 force open save as dialog 的下载方式,有兴趣可以参考: showSaveFilePicker

 

cross-origin 和 Security and tainted canvases

不清楚什么是跨域的,先看这篇

参考:

MDN – Allowing cross-origin use of images and canvas

Stack Overflow – Tainted canvases may not be exported

当 canvas 包含跨域图片时,它是不允许被 JS 读取的。

你尝试调用 getImageData()、toBlob()、toDataURL() 这类方法,游览器就会报错 "Failed to execute 'toBlob' on 'HTMLCanvasElement': Tainted canvases may not be exported".

跨域图片哪来的呢?

我们用 fetch (ajax) 获取图片,如果对方不允许跨域,那你压根拿不到这张图,ajax response 时就报错了。

但我们不一定要用 fetch,我们可以用 new Image()。这样至少可以拿到图 (即使是跨域并且对方没有允许)。

但是呢,这张图游览器只允许用户 "看见" (你可以用 canvas 把它画出来),但是游览器不允许 JS 代码读取这张图的信息。

所以 getImageData 会报错。

游览器的安全考量是这样的

小明访问 a.com,a.com 去 b.com 拿图片。但是 b.com 不允许跨域 (意思是,这张图不允许其它网站以 JS 的方式读取信息)

a.com 可以用 JS 把这张图展现给小明。但是 a.com 不可以用 JS 读取这张图信息做别的事。这就是游览器对跨域的保护。

总结,要 getImageData 或 toBlob 我们要确保拿到的图片是允许跨域的。

若使用 new Image 获取图片,我们需要加上 crossOrigin,并且对方服务器需要配合返回 Access-Control-Allow-Origin

const image = new Image();
image.crossOrigin = 'anonymous'; // 关键
image.addEventListener('load', () => {
  ctx.drawImage(image, 0, 0);
});
image.src = 'https://cdn-icons-png.flaticon.com/512/5360/5360938.png';

如果对方服务器无法配合,那我们就无法单靠前端去完成这件事了,我们需要靠后端去下载图片,保存在同域,才可以避开跨域的问题。

为什么靠后端就可以?因为跨域保护只是游览器的机制,用后端发请求本来就可以轻松下载图片做任何事情。(游览器只是不相信 JS 而已)

 

Convert Image Format

其实上一段已经讲了. 就是在 canvas.blob 的使用选类型就可以了.

canvas.toBlob(blob => {}, 'image/webp');
canvas.toBlob(blob => {}, 'image/jpeg');
canvas.toBlob(blob => {}, 'image/png');

 

drawImage 参数详解

在实现其它修图前, 我们先认识一下 canvas context 的 drawImage 功能.

它一共有 3 个重载方法, 我们直接看最复杂的就可以了. 其余 2 个底层都是调用这一个.

它虽然有 9 个参数, 但我们可以把它分成 3 个来看.

第一个是 image 就不需要介绍了.

第二个是 sx, sy, sw, sh

x y 是 coordinate, w h 是 width 和 height

试想我们有一张图. 我们要把它画进 canvas. 这时我们可以选择是否把整张图都画进去, 或者只画某一部分. (AKA Crop 截图), 比如

我只想把中间的小猫画进去, 那么我的 sx, sy, sw, sh 的参数就是上面这样

第三个参数是 dx, dy, dw, dh

和参数 2 一样, x y 是 coordinate, w h 是 width 和 height

只是这一次的对象不是 image 而是 canvas

canvas 是画布, 它不一定非得和图片一样大, 试想我们的 canvas 很大, 我想把刚才的小猫画到靠近中间区域.

外面红色框是整个 canvas 范围. 中间是被画进来的小猫.

negative width height

negative width, height 可以帮助我们设置绘画的坐标.

它类似于 CSS 的 position + translate 居中

position top: 50%, left 50% + translate(-50%-50%)

用 positive 的方式画出来的效果是这样

图不在中心点.

加上 negative 之后

图片居中了.

 

Thumbnail Image (缩率图)

原图 1600 x 900

缩率图 160 x 90

首先是 canvas dimension 要改

[canvas.width, canvas.height] = [image.width * 0.1, image.height * 0.1];

最终输出的图要求是 160 x 90, 所以 canvas 的 dimension 必须是这个.

接着是 drawImage 要改

context.drawImage(image, 0, 0, canvas.width, canvas.height);

这是第二重载方法, 参数是 dx, dy, dw, dh, 对象是 canvas.

x, y = 0, 0 表示从 canvas 左上角开始画

width, height 是 canvas width, height 也就是 160 x 90, 这表示把原图 1600 x 900 画进 160 x 90 的 canvas 里

这样就实现把图片缩小的效果.

 

Rotate Image

context.translate

除了 drawImage 时, 我们可以调整下笔的 coordinate, 还有一个方式是 translate

context.translate(50, 50);
context.drawImage(image, 0, 0, canvas.width, canvas.height);

效果

在绘画前, 通过 translate 调整下笔的 coordinate. 调整后 drawImage 的 coordinate 虽然是 0, 0 但画下去的地方已经不是 canvas 的 0, 0 了

context.rotate

context.translate(50, 50);
context.rotate(degreeToRadian(25)); // 相等于 45 * Math.PI / 180
context.drawImage(image, 0, 0, canvas.width, canvas.height);

在绘画前调整绘画的方向.

注: rotate 的参数是 radian 而不是 degree 哦, 需要自己转换一下.

效果

rotate 90°

首先是 canvas 最终的 dimension

[canvas.width, canvas.height] = [image.height * 0.25, image.width * 0.25];

* 0.25 是因为我的图片调大, 我随便做了缩略.

接着把笔头移动到右侧

context.translate(canvas.width, 0);

如果现在画, 效果是这样的.

context.drawImage(image, 0, 0, canvas.width, canvas.height);

红色是 canvas, 绿色是图片, 这明显不是我们要的效果.

再加上 rotate

context.rotate(degreeToRadian(90));

方向对了, 但是画的 width 和 height 不对.

因为方向换了, 之前是 horizontal 所以是 canvas.width, canvas.height

现在是 vertical, 所以是反过来 canvas.height, canvas.width

context.drawImage(image, 0, 0, canvas.height, canvas.width); 

效果

rotate 180°

[canvas.width, canvas.height] = [image.width * 0.2, image.height * 0.2];
context.translate(canvas.width, canvas.height); // 移动到右下角
context.rotate(degreeToRadian(180));
context.drawImage(image, 0, 0, canvas.width, canvas.height);

和上面原理是一样的.

rotate -90° or 270°

[canvas.width, canvas.height] = [image.height * 0.2, image.width * 0.2];
context.translate(0, canvas.height); // 移动到右下角
context.rotate(degreeToRadian(-90));
context.drawImage(image, 0, 0, canvas.height, canvas.width);

效果

 

Flip Image

CSS 实现 flip 效果是透过 scaleX, scaleY

canvas 也是透过 scale

flip horizontal

[canvas.width, canvas.height] = [image.width * 0.2, image.height * 0.2];
context.scale(-1, 1);
context.drawImage(image, 0, 0, canvas.width, canvas.height);

效果

红框是 canvas, 绿框是图. 看绿色箭头, horizontal 的方向改变了. 它现在是往右边绘画.

flip 和 rotate 类似, 必须搭配 translate 才能正确画出结果.

先把 translate 调到右上角

[canvas.width, canvas.height] = [image.width * 0.2, image.height * 0.2];
context.translate(canvas.width, 0);
context.scale(-1, 1);
context.drawImage(image, 0, 0, canvas.width, canvas.height);

效果

 

flip vertical

和 horizontal 同一个原理

[canvas.width, canvas.height] = [image.width * 0.2, image.height * 0.2];
context.translate(canvas.width, canvas.height);
context.scale(-1, -1);
context.drawImage(image, 0, 0, canvas.width, canvas.height);

效果

把下笔点移动到右下角, 然后往左上角作画. 效果就出来了.

 

Water Mark

效果

水印的原理就是 draw 2 张图. 第二张会覆盖第一张.

如果想要透明, 就在下笔前设置

context.globalAlpha = 0.4;

这样就可以了.

 

Save and Restore 配置

canvas 就是一个画布, 它是一个操作, 一个操作, 叠加画上去的.

下笔前, 我们可以为每一次的操作做配置. 比如 

translate (下笔的位置)

rotate, sclae (绘画的方向)

globalAlpha (透明度)

等等...

绘画完成后, 通常都需要一个 reset 配置. 毕竟下一次操作未必和上一次同样配置.

canvas 默认是没有 reset 的. 但我们可以通过 save 和 restore 做到这一点.

[canvas.width, canvas.height] = [image.width * 0.4, image.height * 0.4];
context.save();
context.globalAlpha = 0.5;
context.restore();
context.drawImage(image, 0, 0, canvas.width, canvas.height);

最终没有透明, 因为在绘画前它就被 restore 了.

注: save and resotre 指的是配置, 而不是绘画的成果哦, 它不是拿来做 undo 了.

 

Background Color

canvas to jpeg by default background color 是 黑色

to png background color 是 transparent

set background color 的方法是

const ctx = canvas.getContext('2d')!;
ctx.fillStyle = '#f00'; // 写 'red', hsl(0deg 100% 50%), rgb(255 0 0) 也可以,估计 CSS 语法都支持
ctx.fillRect(0, 0, canvas.width, canvas.height);

想清除的话是这样

ctx.clearRect(0, 0, canvas.width, canvas.height);

 

冷知识 – 当你 resize canvas,draw 就不见了

参考: Stack Overflow – HTML canvas - drawing disappear on resizing

画一个红色的正方形

const canvas = document.querySelector<HTMLCanvasElement>('.img')!;
const ctx = canvas.getContext('2d')!;
canvas.width = 100;
canvas.height = 100;
ctx.fillStyle = '#f00';
ctx.fillRect(0, 0, canvas.width, canvas.height);

效果

加上 resize

ctx.fillRect(0, 0, canvas.width, canvas.height);
canvas.width = 100;

注意,哪怕我只是 set 回原本的值 100 也不行哦。

效果

红色正方形完全消失了

 

posted @ 2023-04-09 12:45  兴杰  阅读(57)  评论(0编辑  收藏  举报