欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

react之上传图片并裁剪(Img-Crop&&react-cropper)

程序员文章站 2022-07-04 19:31:49
文章目录一、图片裁剪插件介绍ImgCrop1. 使用ImgCrop2. 上传函数3. 获取图片base64注意:二、react-cropperreact-cropper使用1. 组件内使用2. 裁剪框对图片进行裁剪(1) 组件中设置上传裁剪框(2) 设置state变量(3) 图片上传时更改state3. 设置裁剪框CutModal(1)裁剪框接收的props(2)裁剪框组件(3)裁剪框用到的state变量(4)原图展示函数(5)图片裁剪函数4. 父组件获取裁剪后的照片(1) 父组件调用函数(2)裁剪展示函数...

一、图片裁剪插件介绍

在这里我介绍两种裁剪插件

  • 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

相关标签: react js