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)
}

注意:

  1. 这个插件在pc端完全适用,在ios端,因为上传的图片大于5M,会导致裁剪后的图片为全黑。
  2. 在不久前,我又使用了一次这个插件对图片进行裁剪,发现当裁剪完成会调一次默认请求,如下所示:
    在这里插入图片描述

这个请求来自于,upload组件没有设置他的beforeUpload为false所以就会走这条路。。。总之,这个插件有问题,大家看看使用方法就成


二、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])
  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值