裁剪工具
如何做出一个完整的图片裁剪组件? 背景 一个图片裁剪组件的应用场景其实比较多,相应的第三方插件也不少,但有时候会需要一些特定的功能(比如想有个特定样式的裁剪框,想批量裁剪,甚至想直接裁出需求的最优尺寸等等),这时就只能手写一个裁剪组件了。
大致流程 300
关于为什么会需要一个传入图片链接的入口,是因为会有一些编辑上传信息的场景,就比如需要修改已经发布过的商品信息时,图片是已经上传过的,只有一个图片链接,所以这时就需要根据这个图片链接来裁剪图片了。 由于本组件是基于react开发的,所以下文的写法都会是react的方式。并且因为篇幅有限,涉及的内容比较多,只介绍了大致思路和实现,若有错误还请见谅。。 一、读取图片 本地图片上传 本地上传直接使用 input type="flie" 即可,multiple用于控制是否批量上传,accept用于控制上传文件的类型,设置为image/*表示接收所有image后缀的文件,onChange用于监听上传事件,在这里可以做一些上传时的规则校验(比如图片数量,图片大小限制等等)
<input
type="file"
onChange={this.handleChange}
multiple="true"
accept="image/*"
ref={e => {
this.imageUpload = e;
}}
/>
读取文件信息 首先,html5中的 input 上传的是一个file对象,里面记录了文件的name,size,type,和修改时间等(只读),可以用onChange事件来通过e.target.files来获取(注意:ie10以下不兼容target),在预览图片之前可以通过这些信息来限制一下上传图片的size、type等。
handleChange = (e) => {
const files = Array.from(e.target.files);
if (!files.length) {
// 释放系统存储当前值,避免相同文件不触发onchange事件
this.imageUpload.value = null;
return;
}
// 上传规则校验
....
}
注意点: 在 input 使用onChange上传文件时,如果连续两次选择相同的文件,第二次会因为value还是同一个值导致onChange不会触发,所以需要在第一次上传完之后,将input的value置为空。
二、Canvas绘制图片 解析图片信息 利用我们刚刚获取的file文件对象,来解析出一些图片的关键信息,比如图片的宽、高以及最重要的base64。主要是通过FileReader.readAsDataURL来实现,并实例出一个canvas需要的图片Image对象。
// 读取图片原始信息方法
filesInfo: function(file) {
return new Promise((res, rej) => {
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function() {
let image = new Image(); // 实例一个Image对象
let _this = this;
image.onload = function() {
res({
link: _this.result, // base64
width: image.width, // 宽
height: image.height, // 高
...其他图片信息
});
};
image.src = _this.result; // base64
image.crossOrigin = 'Anonymous'; //解决跨域问题
};
});
},
当 FileReader 的实例通过 readAsDataURL 方 法把图片file转换为 base64 格式之后,会触发 onload 方法,返回一个基于 base64 编码的 data-uri 对象,此方法内的 event.target.result 就是所需要的 base64 格式。
除了上述的其实还有第二种方式, window.URL.createObjectURL,功能都差不多,本文就不做赘述了。
预览图片 理论上我们拿到刚刚获取的Image对象之后,可以直接添加到dom中去就能实现图片预览,但我们核心是裁剪(硬气),所以就要放入canvas画布中去,主要通过canvas的drawImage来绘制图片。
而为什么刚刚我们还需要获取图片的宽、高呢?是因为裁剪框通常是一个固定宽高的画布,但图片的大小往往五花八门,就需要自适应这些图片。
首先canvas的宽高分为2种,第一种canvas style样式中的宽高,是整个canvas的宽高,决定了整个元素的大小;而第二种写在canvas标签属性内的宽高,表示canvas内部的画布大小。
因此我们的自适应图片居中策略:
给canvas元素外设置一个外壳div,并设置默认宽高,用来固定整个canvas的变化范围,让其始终保持居中。 根据我们刚刚获取的图片信息,计算出宽高比例proportion。 再结合我们外壳div的默认宽高,计算出scale,最终动态来得出这个canvas的宽高。
// 绘制图片方法
drawImage = (image) => {
// 获取canvas的上下文
this.showImg = this.canvasRef.getContext('2d');
// 清除画布
this.showImg.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
// 设置默认canvas元素大小
const canvasDefaultSize = 300;
// 初始化canvas画布大小, 获取等比例缩放后的canvas宽高尺寸
let proportion = image.width / image.height,
scale = proportion > 1 ? canvasDefaultSize / image.width : canvasDefaultSize / image.height,
canvasWidth = image.width * scale * this.ratio,
canvasHeight = image.height * scale * this.ratio;
this.canvasRef.width = canvasWidth;
this.canvasRef.height = canvasHeight;
this.canvasRef.style.width = canvasWidth / this.ratio + 'px';
this.canvasRef.style.height = canvasHeight / this.ratio + 'px';
// ...
// 绘制图片,这个image就是我们刚刚获取的Image对象
this.image = image; // 保存这个Image对象
this.showImg.drawImage(image, 0, 0, this.canvasRef.width, this.canvasRef.height);
};
render() {
const canvasDefaultSize = 300; // 设置默认canvas元素大小
return (
<div
className="modal-trim"
// 固定整个canvas的变化范围
style={{ width: `${canvasDefaultSize}px`, height: `${canvasDefaultSize}px` }}
>
<canvas
ref={e => {this.canvasRef = e}}
// 给予一个默认初始宽高
width={canvasDefaultSize}
height={canvasDefaultSize}
// ...
></canvas>
</div>
)
}
/* 部分css */
.modal-trim {
overflow: hidden;
position: relative;
/* 马赛克背景图 */
background-image: url(https://s10.mogucdn.com/mlcdn/c45406/190723_3afckd96l9h4fh6lcb56117cld176_503x503.jpg);
background-size: cover;
/* 使canvas始终居中 */
canvas {
cursor: default;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
}
}
为什么canvas绘制的图好模糊呢? 这就涉及到像素比devicePixelRatio的问题了,一般在绘制之前都会提前计算好这个值,具体可以戳:https://www.jianshu.com/p/4c4312dc7fc5