在反应中将缩放限制限制为特定图像大小

问题描述

大家好,我们正在为公司构建应用程序。我的任务是为个人资料图片创建图像裁剪功能。我正在使用react-easy-crop库。我已经实现了几乎所有功能,唯一缺少的是放大到某个像素。我们公司不希望用户能够放大小于600 X 600像素。我的意思是,当用户放大图像大小不能低于600 x 600像素时。这是我的代码结构(请注意,这不是我为简化复杂性而减少的完整代码)

import React,{ useEffect,useRef,useState,FC,useCallback,SetStateAction } from 'react';
import Cropper from 'react-easy-crop';
import Resizer from 'react-image-file-resizer';
import { Text,Button,DragAndDrop } from '@shared/components';
import { Modal } from '@shared/components/Modal/lazy';
import { getOrientation } from 'get-orientation';
import { COLOR_NAMES } from '@src/settings/colors.constants';
import { Icon } from '@src/shared/components/icon';
import { Centered } from '@src/shared/components/layout/centered';
import { useDispatch } from 'react-redux';
import { Flex } from 'rebass';
import { Dispatch } from 'redux';
import { ZoomAndApplyContainer,CropContainer } from '@app/shared/components/crop-image/styled';
import { FileValue } from '@shared/components/upload-single-document/interfaces';
import { UploadSingleImageProps } from '@app/shared/components/upload-single-image/interfaces';
import { CoverPicEditComponent,ImageUploadContainer,PicEditComponent } from '@app/shared/components/upload-single-image/styled';
import { MODAL_NAMES,MODAL_TYPES } from '@app/shared/store/constants/modal.constants';
import { ShowModalAC,HideModalAC } from '@app/shared/store/actions/modal.actions';
import { NumberOfBytesInOneMB,TOASTER_APPEARANCES } from '@app/shared/constants';
import { SetToastsActionCreator } from '@app/shared/store/actions/toast.actions';
import { validateFileType } from '@app/utils/validations';
import { PRIMARY } from '../button/button.constants';
import { FormikFieldErrorMessage } from '../formik-field/styled';

const readFile: any = (file: any) =>
  new Promise((resolve: any) => {
    const reader: any = new FileReader();
    reader.addEventListener('load',() => resolve(reader.result),false);
    reader.readAsDataURL(file);
  });

const createImage: any = (url: any) =>
  new Promise((resolve: any,reject: any) => {
    const image: any = new Image();
    image.addEventListener('load',() => resolve(image));
    image.addEventListener('error',(error: any) => reject(error));
    image.setAttribute('crossOrigin','anonymous'); // needed to avoid cross-origin issues on CodeSandbox
    image.src = url;
  });

const getRadianAngle: any = (degreeValue: any) => (degreeValue * Math.PI) / 180;

const ORIENTATION_TO_ANGLE: any = {
  3: 180,6: 90,8: -90,};

/**
 * This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
 * @param {File} image - Image File url
 * @param {Object} pixelCrop - pixelCrop Object provided by react-easy-crop
 * @param {number} rotation - optional rotation parameter
 */
const getCroppedImg: any = async (imageSrc: any,pixelCrop: any,rotation: any = 0) => {
  const image: any = await createImage(imageSrc);
  const canvas: any = document.createElement('canvas');
  const ctx: any = canvas.getContext('2d');

  const maxSize: any = Math.max(image.width,image.height);
  const safeArea: any = 2 * ((maxSize / 2) * Math.sqrt(2));

  // set each dimensions to double largest dimension to allow for a safe area for the
  // image to rotate in without being clipped by canvas context
  canvas.width = safeArea;
  canvas.height = safeArea;

  // translate canvas context to a central location on image to allow rotating around the center.
  ctx.translate(safeArea / 2,safeArea / 2);
  ctx.rotate(getRadianAngle(rotation));
  ctx.translate(-safeArea / 2,-safeArea / 2);

  // draw rotated image and store data.
  ctx.drawImage(image,safeArea / 2 - image.width * 0.5,safeArea / 2 - image.height * 0.5);
  const data: any = ctx.getImageData(0,safeArea,safeArea);

  // set canvas width to final desired crop size - this will clear existing context
  canvas.width = pixelCrop.width;
  canvas.height = pixelCrop.height;

  // paste generated rotate image with correct offsets for x,y crop values.
  ctx.putImageData(data,Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x),Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y));

  // As Base64 string
  // return canvas.toDataURL('image/jpeg');

  // As a blob
  return new Promise((resolve: any) => {
    canvas.toBlob((file: any) => {
      resolve(file);
    },'image/jpeg');
  });
};

const getRotatedImage: any = async (imageSrc: any,rotation: number = 0) => {
  const image: any = await createImage(imageSrc);
  const canvas: any = document.createElement('canvas');
  const ctx: any = canvas.getContext('2d');

  const orientationChanged: boolean = rotation === 90 || rotation === -90 || rotation === 270 || rotation === -270;

  if (orientationChanged) {
    canvas.width = image.height;
    canvas.height = image.width;
  } else {
    canvas.width = image.width;
    canvas.height = image.height;
  }

  ctx.translate(canvas.width / 2,canvas.height / 2);
  ctx.rotate((rotation * Math.PI) / 180);
  ctx.drawImage(image,-image.width / 2,-image.height / 2);

  return new Promise((resolve: any) => {
    canvas.toBlob((file: any) => {
      resolve(URL.createObjectURL(file));
    },'image/jpeg');
  });
};

export const UploadSingleImage: FC<UploadSingleImageProps> = ({
  setFieldValue,setFieldTouched,name,extensionName,width = '600',height = '600',errorMessage,isDisabled,fileId,extension,title,validationRules,isCoverPhoto,customContainerCss,onChange,editIconName = 'edit',onUploaded,}: UploadSingleImageProps) => {
  const [value,setValue]: [FileValue,React.Dispatch<SetStateAction<FileValue>>] = useState(null);
  const [imgSrc,setImgSrc]: any = useState(null);
  const [maxZoom,setMaxZoom]: any = useState(1);
  const [rotation,setRotation]: any = useState(0);
  const [crop,setCrop]: any = useState({ x: 0,y: 0 });
  const [imageSendingFail,setImageSendingFail]: [boolean,React.Dispatch<SetStateAction<boolean>>] = useState(true);
  const [zoom,setZoom]: any = useState(1);
  const [croppedAreaPixels,setCroppedAreaPixels]: any = useState(null);
  const showCroppedImage: any = useCallback(async () => {
    try {
      const cropedImage: any = await getCroppedImg(imgSrc,croppedAreaPixels,rotation);
      Resizer.imageFileResizer(
        cropedImage,600,'JPEG',100,(file: any) => {
          onChange(file,setValue);
          dispatch(HideModalAC(MODAL_NAMES.IMAGE_CROP_MODAL));
          setImgSrc(null);
        },'blob'
      );
    } catch (e) {
      console.error(e);
    }
  },[imgSrc,rotation]);
  const imageInput: React.MutableRefObject<HTMLInputElement> = useRef();
  const dispatch: Dispatch = useDispatch();
  const toast: any = useCallback((toasts: any) => dispatch(SetToastsActionCreator(toasts)),[dispatch]);
  const onCropComplete: any = useCallback((croppedArea: any,croppedAreaPixel: any) => {
    setCroppedAreaPixels(croppedAreaPixel);
  },[]);
  const handleFileDrop: any = (e: any) => {
    const files: any = e.dataTransfer.files;
    if (files && files.length === 1) {
      validateImage(files[0]);
    }
  };

  const onFileChange: any = async (e: any) => {
    if (e.target.files && e.target.files.length === 1) {
      const file: any = e.target.files[0];
      validateImage(file);
    }
  };

  const onClick: any = (e: any) => {
    setZoom(1);
    e.target.value = '';
  };


  const validateImage: (file: File) => void = async (file: any) => {
    setImageSendingFail(false);
    // const imageDataUrl: any = await readFile(file);
    // setImgSrc(imageDataUrl);
    if (setFieldTouched) {
      setFieldTouched(name);
    }
    if (validateFileType(toast,validationRules?.fileTypes,file)) {
      let imageDataUrl: any = await readFile(file);
      const img: any = createImage(imageDataUrl);
      if (!validationRules || validateImg(toast,img,file)) {
        const orientation: any = await getOrientation(file);
        const rotationPortion: any = ORIENTATION_TO_ANGLE[orientation];
        if (rotation) {
          imageDataUrl = await getRotatedImage(imageDataUrl,rotationPortion);
        }
        setImgSrc(imageDataUrl);
        dispatch(ShowModalAC(MODAL_NAMES.IMAGE_CROP_MODAL));
      } else {
        imageInput.current.value = '';
        setImageSendingFail(true);
      }
    }
  };

  useEffect(() => {
    if (fileId && extension) {
      setValue({ fileId,extension });
    }
  },[fileId,extension]);

  useEffect(() => {
    if (setFieldValue) {
      setFieldValue(name,value?.fileId);
      setFieldValue(extensionName,value?.extension);
    }
    if (onUploaded && value?.fileId) {
      onUploaded(name,value);
    }
  },[value?.fileId,value?.extension]);

  return (
    <Flex justifyContent={'center'} alignItems={'center'} flexDirection={'column'} css={{ height: '100%' }} name={name}>
      {imgSrc && (
        <Modal bodyCss={{ padding: 0 }} bodyHeight='90%' width={'70%'} height={'85%'} borderRadius={'4px'} modalId={MODAL_NAMES.IMAGE_CROP_MODAL} headingText={'Resmi Düzenle'} type={MODAL_TYPES.NORMAL}>
          <CropContainer>
            <div style={{ width: '80%',height: '70%',position: 'relative' }}>
              <Cropper
                image={imgSrc}
                crop={crop}
                zoom={zoom}
                rotation={rotation}
                aspect={1 / 1}
                onCropChange={setCrop}
                onCropComplete={onCropComplete}
                onZoomChange={setZoom}
                restrictPosition={false}
                onRotationChange={setRotation}
                minZoom={0.5}
                maxZoom={maxZoom}
                onMediaLoaded={(imageSize: any) => {
                  if (imageSize.naturalWidth > 600) {
                    setMaxZoom(600 / Math.max(imageSize.height,imageSize.width));
                  } else {
                    setMaxZoom(1);
                  }
                  console.log(imageSize);
                }}
              />
            </div>
            <ZoomAndApplyContainer>
              <input type='range' value={zoom} min={0.5} max={maxZoom} step={0.05} onChange={(e: any) => setZoom(e.target.value)} />
              <input type='range' value={rotation} min={0} max={360} step={10} onChange={(e: any) => setRotation(e.target.value)} />
              <Button button={PRIMARY} onClick={showCroppedImage}>
                Upload
              </Button>
            </ZoomAndApplyContainer>
          </CropContainer>
        </Modal>
      )}

      <DragAndDrop handleDrop={handleFileDrop}>
        <ImageUploadContainer customContainerCss={customContainerCss} url={fileId && extension && `${fileId}${extension}`} width={width} height={height} errorMessage={errorMessage}>
          {value?.fileId && value?.extension && isCoverPhoto && !isDisabled && (
            <CoverPicEditComponent>
              <label htmlFor={name}>
                <Icon margin={'auto'} name={'upload-white'} />
                <Text color={COLOR_NAMES.REMAX_WHITE} customWeight={1}>
                  Yeni kapak fotoğrafı yükle
                </Text>
              </label>
            </CoverPicEditComponent>
          )}
          {value?.fileId && value?.extension && !isCoverPhoto && !isDisabled && (
            <PicEditComponent className='edit-icon-container' htmlFor={name}>
              <Icon name={editIconName} />
            </PicEditComponent>
          )}
          {!(value?.fileId && value?.extension) && (
            <Centered css={{ flexDirection: 'column' }}>
              <Text customSize={[2,3,3]} lineHeight={1} customWeight={1} color={COLOR_NAMES.REMAX_TEXT_GREY_7f7f7f} css={{ textAlign: 'center',width: '145px',paddingBottom: '12px' }}>
                {title}
              </Text>
              <label htmlFor={name}>
                <Text customSize={[2,3]} customWeight={1} color={COLOR_NAMES.REMAX_BLUE_SELECTED_1451EF} textDecoration={'underline'}>
                  Dosya Seç
                </Text>
              </label>
            </Centered>
          )}
          <input id={name} ref={imageInput} name={name} type='file' onChange={onFileChange} onClick={onClick} />
        </ImageUploadContainer>
      </DragAndDrop>
      {/* Eğer resim yok ve upload işlemi fail olduysa hata mesajı basılsın */}
      {!value?.fileId && imageSendingFail && errorMessage && <FormikFieldErrorMessage elipsis={true}>{errorMessage}</FormikFieldErrorMessage>}
    </Flex>
  );
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

这是我针对该问题的解决方案:裁剪机内部的回调函数

onMediaLoaded={(imageSize: any) => {
                  if (imageSize.naturalWidth > 600) {
                    setMaxZoom(600 / Math.max(imageSize.height,imageSize.width));
                  } else {
                    setMaxZoom(1);
                  }
                }}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

,但是此代码不能确保我的最大缩放限制最终会产生600 x 600。 (它适用于某些图片,但不适用于某些图片。我非常感谢您提出的任何建议。

解决方法

const onCropComplete: any = useCallback((croppedArea: any,croppedAreaPixel: any) => {
    if(croppedAreaPixel.width < requiredWidth || croppedAreaPixel.height< requiredHeight){
      /** do something here to disallow  zoom 
        * this is a workaround
       */
    } 
    setCroppedAreaPixels(croppedAreaPixel);
     
  },[]);

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...