react之上传图片并裁剪(Img-Crop&&react-cropper)
一、图片裁剪插件介绍
在这里我介绍两种裁剪插件
- ImgCrop
- react-cropper
ImgCrop
是阿里基于Upload组件开发的一个插件,详情可以搜一下api,比较适合于pc端项目
1. 使用ImgCrop
在上传图片的那个位置直接包裹Upload组件
功能具有
- 旋转
- 压缩
- 放大,缩小
<div className={styles['img-upload__div']}>
<ImgCrop
modalTitle={'图片裁剪'} //裁剪框的title
rotate={false} //图片是否旋转
quality={0.4} //自带了图片压缩,质量
zoom={true} //是否放大
minZoom={1} //最小缩放比
maxZoom={3} //最大缩放比
//beforeCrop={() => false}
modalCancel={'取消'} //取消按钮 接受字符串
modalOk={'确定'} //确定按钮
>
<Upload
className={className}
showUploadList={false}
listType="picture"
//beforeUpload={() => false}
onChange={handleOnChange}
accept={'image/*'}
>
{imgUrl ? (
<img src={imgUrl} alt="avatar" style={{ width: '100%' }} />
) : (
UploadButton
)}
</Upload>
</ImgCrop>
</div>
因为他是基于Upload组件开发的图片裁剪器,所以最终图片的处理还是在Upload组件上,此时需要对Upload组件的beforeUpload函数不能禁止,这样就会触发裁剪框的处理
2. 上传函数
上传函数处理(最终还是Upload组件中进行处理)
与之前不同的是现在需要通过fileList来获得图片
//图片上传函数
const handleOnChange = useCallback(({ fileList: newFileList }) => {
if (newFileList[0].originFileObj) {
//将图片转化为base64
getBase64(newFileList[0].originFileObj, async (imgUrl: any) => {
setLoading(true)
// const cesibase64 = await compress(imgUrl, 5)
// setImgUrl(cesibase64)
setImgUrl(imgUrl)
setClicked(false)
})
}
}, [])
3. 获取图片base64
function getBase64(img: any, callback: any) {
const reader = new FileReader()
reader.addEventListener('load', () => callback(reader.result))
reader.readAsDataURL(img)
}
注意:
这个插件在pc端完全适用,在ios端,因为上传的图片大于5M,会导致裁剪后的图片为全黑。
二、react-cropper
react-cropper是基于cropper这个js库封装的一个react库
github:https://github.com/DominicTobias/react-image-crop
react-cropper使用
1. 组件内使用
这个我也是和antd的Upload组件一起使用,但是在裁剪那部分自己封装了一个组件
<Upload
className={className}
showUploadList={false}
beforeUpload={() => false}
onChange={handleOnChange}
accept={'image/*'}
>
{imgUrl ? (
<img src={imgUrl} alt="avatar" style={{ width: '100%' }} />
) : (
UploadButton
)}
</Upload>
2. 裁剪框对图片进行裁剪
下面这个CutModal,是新建的一个组件,将在第三步介绍,
(1) 组件中设置上传裁剪框
在render函数中设置裁剪框
{visible && (
<CutModal
uploadedImageFile={modalFile}
onClose={() => {
return setVisible(false)
}}
onSubmit={handleCutImage}
/>
)}
(2) 设置state变量
在这里我用到两个变量,如下:
const [modalFile, setModalFile] = useState('') //存上传的图片数据
const [visible, setVisible] = useState(false)//设置裁剪框是否可见
(3) 图片上传时更改state
//图片上传函数
const handleOnChange = useCallback(
async (info) => {
//可以加重复上传判断
// if (imgUrl) {
// message.error('您上传照片,请不要重复上传')
// return
// }
if (!info.file) {
console.error('图片选取不能为空!')
}
//获取到图片文件
setLoading(true)
setModalFile(info.file) //存入modal中
setVisible(true) //设置裁剪框为true
},
[imgUrl]
)
3. 设置裁剪框CutModal
从上面的第一步可以看出,我们向裁剪框传了三个参数,分别是:
- uploadedImageFile
- onClose
- onSubmit
(1)裁剪框接收的props
interface CutProps {
uploadedImageFile: any
onClose: () => void
onSubmit: (values: string) => void
}
(2)裁剪框组件
这个组件的api巨多,巨麻烦,详情看他的 GitHub,但我这边的需求是要求,图片可以拖动,可以方法缩小,而裁剪框是固定值。
import React, { FC, useCallback, useState, useRef, useEffect } from 'react'
import { Button, Result } from 'antd'
import Cropper from 'react-cropper' // 引入Cropper
import 'cropperjs/dist/cropper.css' // 引入Cropper对应的css
import styles from './cut.module.scss'
interface CutProps {
uploadedImageFile: any
onClose: () => void
onSubmit: (values: string) => void
}
const CutModal: FC<CutProps> = ({ uploadedImageFile, onClose, onSubmit }) => {
return (
<div className={styles['modal']}>
<div className={styles['modal__cropper-container']}>
<p>图片裁剪</p>
<hr />
<Cropper
src={src}
className={styles['modal__cropper']}
initialAspectRatio={1} //定义裁剪框的初始宽高比
viewMode={1} //裁剪框不能超过画布大小
guides={true} //网格线
minCropBoxHeight={10} //最小高度
minCropBoxWidth={10}
background={false}
autoCropArea={1} //定义自动裁剪的大小比
cropBoxMovable={false} //裁剪框是否移动
cropBoxResizable={false} //裁剪框大小是否变化
scalable={true} //是否可以放大
rotatable={false} //是否可以旋转
dragMode={'move'} //单击设置为移动图片
//这个比较重要,单击时,设置裁剪框不可变化
toggleDragModeOnDblclick={false}
//这个也比较重要,对于有方向值的图片是否根据方向值旋转
checkOrientation={false}
//这个将裁剪好的图片存入state变量
onInitialized={(instance) => {
setCropper(instance)
}}
/>
<Button type="primary" onClick={getCropData}>
确定
</Button>
</div>
</div>
)
}
export default CutModal
(3)裁剪框用到的state变量
const [src, setSrc] = useState('') //存原始图片
const [cropData, setCropData] = useState('') //存裁剪后的图片
const [cropper, setCropper] = useState<any>() //获取裁剪后的图片
(4)原图展示函数
使用钩子函数将Upload上传的图片进行展示在裁剪框中,所以用到了useEffect
useEffect(() => {
const fileReader = new FileReader()
fileReader.onload = (e) => {
//拿到传过来的照片
if (e.target) {
const dataURL = e.target.result as string
setSrc(dataURL)
}
}
fileReader.readAsDataURL(uploadedImageFile)
}, [uploadedImageFile])
(5)图片裁剪函数
const getCropData = useCallback(() => {
if (typeof cropper !== 'undefined') {
//存入裁剪图片,其实下面这一步可以不用存
setCropData(cropper.getCroppedCanvas().toDataURL())
//传递给父组件
onSubmit(cropper.getCroppedCanvas().toDataURL())
}
}, [setCropper, cropper])
4. 父组件获取裁剪后的照片
(1)父组件调用函数
父组件在调用裁剪框组件时,传递了一个函数,onSubmit={handleCutImage}
{visible && (
<CutModal
uploadedImageFile={modalFile}
onClose={() => {
return setVisible(false)
}}
onSubmit={handleCutImage}
/>
)}
(2)裁剪展示函数
只需要将传过来的values存入state变量即可
const handleCutImage = useCallback((values) => {
setLoading(false)
setVisible(false)
setImgUrl(values)
}, [])
注意:
这个插件也存在一些问题,对于较大的图片裁剪会出错,所以我决定先对图片进行压缩后再进行裁剪
后续会更新
三、react-cropper压缩后再裁剪
图片上传时,对于不同的手机端,还是会出现图片旋转的问题,所以需要首先判别图片的方向
1. 判断浏览器是否支持回正
const testAutoOrientationImageURL =
'data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAA' +
'AAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA' +
'QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE' +
'BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAEAAgMBEQACEQEDEQH/x' +
'ABKAAEAAAAAAAAAAAAAAAAAAAALEAEAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAA' +
'AAAAAEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8H//2Q==';
let isImageAutomaticRotation;
export function detectImageAutomaticRotation() {
return new Promise((resolve) => {
if (isImageAutomaticRotation === undefined) {
const img = new Image();
img.onload = () => {
// 如果图片变成 1x2,说明浏览器对图片进行了回正
isImageAutomaticRotation = img.width === 1 && img.height === 2;
resolve(isImageAutomaticRotation);
};
img.src = testAutoOrientationImageURL;
} else {
resolve(isImageAutomaticRotation);
}
});
}
2. 获取图片方向
import EXIF from 'exif-js'
export const getOrientation = (file: any): Promise<number> => {
return new Promise((resolve, reject) => {
EXIF.getData(file, function () {
try {
EXIF.getAllTags(file)
const orientation = EXIF.getTag(file, 'Orientation')
resolve(orientation)
} catch (e) {
reject(e)
}
})
})
}
3. 对图片进行旋转
/* eslint-disable @typescript-eslint/no-explicit-any */
import EXIF from 'exif-js'
export const setImgVertical = (
imgSrc: string,
orientation: number
): Promise<string> => {
return new Promise((resolve, reject) => {
const image = new Image()
if (!imgSrc) return
const type = imgSrc.split(';')[0].split(':')[0]
const encoderOptions = 1
image.src = imgSrc
image.onload = function (): void {
const imgWidth = (this as any).width
const imgHeight = (this as any).height
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) return
canvas.width = imgWidth
canvas.height = imgHeight
if (orientation && orientation !== 1) {
switch (orientation) {
case 6:
canvas.width = imgHeight
canvas.height = imgWidth
ctx.rotate(Math.PI / 2)
ctx.drawImage(this as any, 0, -imgHeight, imgWidth, imgHeight)
break
case 3:
ctx.rotate(Math.PI)
ctx.drawImage(
this as any,
-imgWidth,
-imgHeight,
imgWidth,
imgHeight
)
break
case 8:
canvas.width = imgHeight
canvas.height = imgWidth
ctx.rotate((3 * Math.PI) / 2)
ctx.drawImage(this as any, -imgWidth, 0, imgWidth, imgHeight)
break
}
} else {
ctx.drawImage(this as any, 0, 0, imgWidth, imgHeight)
}
resolve(canvas.toDataURL(type, 0.92))
}
image.onerror = (e): void => reject(e)
})
}
export const getBase64 = (img: any): Promise<string | ArrayBuffer | null> => {
return new Promise((resolve, reject) => {
getOrientation(img).then((orientation) => {
const reader = new FileReader()
reader.addEventListener(
'load',
async (): Promise<void> => {
try {
const base64 = await setImgVertical(
reader.result as string,
orientation
)
resolve(base64)
} catch (e) {
reject(e)
}
}
)
reader.addEventListener('error', () =>
reject(new Error('获取图片Base64失败'))
)
reader.readAsDataURL(img)
})
})
}
4. 在Cut组件中对图片进行处理
在获取图片方向后,进行旋转,我发现img.onload
执行速度特变慢,是因为某些手机拍出来的图片质量太大了,所以我在旋转之前做了一次压缩
压缩代码如下:
export function compress(
base64, // 源图片
rate, // 缩放比例
) {
return new Promise((resolve) => {
//处理缩放,转格式
var _img = new Image();
_img.src = base64;
_img.onload = function () {
var _canvas = document.createElement("canvas");
var w = this.width / rate;
var h = this.height / rate;
_canvas.setAttribute("width", w);
_canvas.setAttribute("height", h);
_canvas.getContext("2d").drawImage(this, 0, 0, w, h);
var base64 = _canvas.toDataURL("image/jpeg");
_canvas.toBlob(function (blob) {
if (blob.size > 750 * 1334) { //如果还大,继续压缩
compress(base64, rate);
} else {
resolve(base64);
}
}, "image/jpeg");
}
})
}
最终处理函数:
useEffect(() => {
setLoading(true)
const fileReader = new FileReader()
fileReader.onload = async (e) => {
//拿到传过来的照片
if (e.target) {
try {
const iosSystem = await detectImageAutomaticRotation()
if (iosSystem) {
//做了回正,直接压缩
const dataURL = e.target.result as string
const cesibase64 = await compress(dataURL, 5)
setLoading(false)
setSrc(cesibase64)
} else {
//浏览器不自带回正,需要旋转根据旋转方向进行旋转
const dataURL = e.target.result as string
getOrientation(uploadedImageFile).then(async (orientation) => {
const cesibase64 = await compress(dataURL, 6)
const base64 = await setImgVertical(
cesibase64 as string,
orientation
)
setLoading(false)
setSrc(base64)
})
}
} catch (e) {
message.error(e)
}
}
}
fileReader.readAsDataURL(uploadedImageFile)
}, [uploadedImageFile])
四、代码调整
因为之前是将cropper设置为一个state变量,通过onInitialized去做的处理,现在将其优化:(可以忽略)
1. 使用ref设置cropper
const cropperRef = useRef<HTMLImageElement>(null)
2. cropper组件中去掉onInitialized
<Cropper
src={src}
className={styles['modal__cropper']}
initialAspectRatio={1} //定义裁剪框的初始宽高比
viewMode={1} //裁剪框不能超过画布大小
guides={true} //网格线
minCropBoxHeight={12} //最小高度
minCropBoxWidth={12}
background={false}
responsive={true}
autoCropArea={1} //定义自动裁剪的大小比
cropBoxMovable={false} //裁剪框是否移动
cropBoxResizable={false} //裁剪框大小是否变化
scalable={true}
rotatable={false}
dragMode={'move'} //单击设置为移动图片
toggleDragModeOnDblclick={false}
checkOrientation={false}
ref={cropperRef}
/>
3.修改裁剪函数
//确定裁剪
const getCropData = useCallback(() => {
const imageElement: any = cropperRef?.current
const cropper: any = imageElement?.cropper
if (cropper) {
onSubmit(cropper.getCroppedCanvas().toDataURL())
} else {
message.error('裁剪失败!')
}
}, [onSubmit])
4. 对父组件传递过来的img对象进行修改
可以看到之前的代码中,我直接设置的是一个null,空对象,为了设置代码的严谨,需要确定对象类型
(1)声明类型
import { UploadChangeParam } from 'antd/lib/upload'
interface CutProps {
uploadedImageFile: UploadChangeParam['file'] | null
onClose: () => void
onSubmit: (values: string) => void
}
(2)修改图片函数
//获取图片
useEffect(() => {
setLoading(true)
const fileReader = new FileReader()
fileReader.onload = async (e) => {
//拿到传过来的照片
if (e.target) {
try {
const iosSystem = await detectImageAutomaticRotation()
if (iosSystem) {
//做了回正,直接压缩
const dataURL = e.target.result as string
const cesibase64 = await compress(dataURL, 5)
setLoading(false)
setSrc(cesibase64)
} else {
//浏览器不自带回正,需要旋转根据旋转方向进行旋转
const dataURL = e.target.result as string
const orientation = await getOrientation(uploadedImageFile)
const cesibase64 = await compress(dataURL, 6)
const base64 = await setImgVertical(
cesibase64 as string,
orientation
)
setLoading(false)
setSrc(base64)
}
} catch (e) {
message.error(e)
setLoading(false)
}
}
}
//指定对象类型
fileReader.readAsDataURL((uploadedImageFile as unknown) as Blob)
}, [uploadedImageFile])
本文地址:https://blog.csdn.net/Welkin_qing/article/details/109614327