业务场景: 拿到封面图的同时 将图片按照16:9或9:16的比例进行裁剪,如何拿到裁剪后的图片url
参考: demo 示例
// CropImg 组件
import React, {
useState,
useMemo,
forwardRef,
useImperativeHandle,
useRef,
useCallback,
} from "react";
import type { MutableRefObject } from "react";
import Cropper from "react-easy-crop";
import type { Area } from "react-easy-crop/types";
import { uploadFile } from "@/services/PGCService/file";
import { getCroppedImg, dataURLtoFile, ImageTypes } from "./canvasUtils";
interface CropImgProps {
imageSrc: string;
rotation?: number; // 旋转角度
aspectWidth?: number; // 宽
aspectHeight?: number; // 高 两个参数即位宽高比
onCropReady?: () => void;
onImageDrawSuccess?: () => void;
onImageDrawFail?: () => void;
onUploadSuccess?: (imgSrc: string) => void; //
onUploadFail?: () => void;
}
export type CropImgRef = {
onCrop: () => Area;
cropUploaded: () => Promise<string>;
cropPixelsRef: MutableRefObject<Area>; // 裁剪区域
getFile: () => Promise<File>;
};
const CropImg = forwardRef<CropImgRef, CropImgProps>(
(
{
imageSrc,
rotation = 0,
aspectWidth = 16,
aspectHeight = 9,
onCropReady,
onImageDrawSuccess,
onImageDrawFail,
onUploadSuccess,
onUploadFail,
},
ref,
) => {
const [crop, setCrop] = useState({ x: 0, y: 0 });
const cropPixelsRef = useRef<Area>({ width: 0, height: 0, x: 0, y: 0 });
const onCropComplete = useCallback(
(_: Area, croppedAreaPixels: Area) => {
cropPixelsRef.current = croppedAreaPixels;
onCropReady?.();
},
[onCropReady],
);
// 简易的文件类型截取,不一定是真实的图片类型
const imgType = useMemo<string>(() => {
if (!imageSrc) return "";
const [imgPath] = imageSrc.split("?");
return imgPath.substring(imgPath.lastIndexOf(".") + 1);
}, [imageSrc]);
const getFile = async (): Promise<File> => {
// console.time('a')
if (!cropPixelsRef.current.width) {
onImageDrawFail?.();
return null;
}
const croppedImage = await getCroppedImg({
imageSrc,
pixelCrop: cropPixelsRef.current,
imgType: imgType as ImageTypes,
quality: 0.8,
});
// 简单粗暴的判断当前裁剪的是否有内容
if (croppedImage.length < 50) {
onImageDrawFail?.();
return null;
}
onImageDrawSuccess?.();
// console.timeEnd('a')
return dataURLtoFile({
base64Data: croppedImage,
fileName: `${Date.now()}.${imgType}`,
type: imgType,
});
};
const cropUploaded = async (): Promise<string> => {
// console.time('b')
const file = await getFile();
if (!file) return undefined;
const formdata = new FormData();
formdata.append("file", file);
const res = await uploadFile(formdata, 1);
// console.timeEnd('b')
if (res.code === 0) {
onUploadSuccess?.(res.result?.url);
return res.result?.url;
}
onUploadFail?.();
return undefined;
};
useImperativeHandle(ref, () => ({
onCrop: () => cropPixelsRef.current,
cropPixelsRef,
cropUploaded,
getFile,
}));
return (
<div style={{ visibility: "hidden", opacity: 0, zIndex: -1 }}>
{imageSrc ? (
<Cropper
image={imageSrc}
crop={crop}
rotation={rotation}
aspect={aspectWidth / aspectHeight}
onCropChange={setCrop}
onCropComplete={onCropComplete}
/>
) : null}
</div>
);
},
);
export default React.memo(CropImg);
// canvasUtils.ts
import type { Area } from "react-easy-crop/types";
export const createImage = (url: string): Promise<HTMLImageElement> =>
new Promise((resolve, reject) => {
const image = new Image();
image.addEventListener("load", () => resolve(image));
image.addEventListener("error", (error) => reject(error));
image.setAttribute("crossOrigin", "anonymous"); // needed to avoid cross-origin issues on CodeSandbox
image.src = url;
});
// 旋转角度
export function getRadianAngle(degreeValue: number) {
return (degreeValue * Math.PI) / 180;
}
/**
* Returns the new bounding area of a rotated rectangle.
*/
interface RotateSizeParams {
width: number;
height: number;
rotation: number;
}
export function rotateSize({ width, height, rotation }: RotateSizeParams): RotateSizeParams {
const rotRad = getRadianAngle(rotation);
return {
width: Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
height: Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),
rotation,
};
}
/**
* This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
*/
/**
* This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
*/
interface CroppedImageParams {
imageSrc: string;
pixelCrop: Area;
imgType: ImageTypes;
rotation?: number;
flip?: { horizontal: boolean; vertical: boolean };
quality?: number;
}
export async function getCroppedImg({
imageSrc,
pixelCrop,
imgType,
rotation = 0,
flip = { horizontal: false, vertical: false },
quality = 1,
}: CroppedImageParams): Promise<string> {
const image = await createImage(imageSrc);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) {
return "";
}
const rotRad = getRadianAngle(rotation);
// calculate bounding box of the rotated image
const { width: bBoxWidth, height: bBoxHeight } = rotateSize({
width: image.width,
height: image.height,
rotation,
});
// set canvas size to match the bounding box
canvas.width = bBoxWidth;
canvas.height = bBoxHeight;
// translate canvas context to a central location to allow rotating and flipping around the center
ctx.translate(bBoxWidth / 2, bBoxHeight / 2);
ctx.rotate(rotRad);
ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);
ctx.translate(-image.width / 2, -image.height / 2);
// draw rotated image
ctx.drawImage(image, 0, 0);
const croppedCanvas = document.createElement("canvas");
const croppedCtx = croppedCanvas.getContext("2d") as CanvasRenderingContext2D;
if (!croppedCtx) {
return "";
}
// Set the size of the cropped canvas
croppedCanvas.width = pixelCrop.width;
croppedCanvas.height = pixelCrop.height;
// Draw the cropped image onto the new canvas
croppedCtx.drawImage(
canvas,
pixelCrop.x,
pixelCrop.y,
pixelCrop.width,
pixelCrop.height,
0,
0,
pixelCrop.width,
pixelCrop.height,
);
// As Base64 string
return croppedCanvas.toDataURL(`image/${imgType}`, quality);
// As a blob
// return new Promise((resolve, reject) => {
// croppedCanvas.toBlob((file) => {
// resolve(URL.createObjectURL(file));
// }, "image/jpeg");
// });
}
export async function getRotatedImage(imageSrc, rotation = 0) {
const image = await createImage(imageSrc);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const orientationChanged =
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) => {
canvas.toBlob((file) => {
resolve(URL.createObjectURL(file));
}, "image/png");
});
}
export enum ImageTypes {
JPG = "jpg",
PNG = "png",
JPEG = "jpeg",
}
export interface DataURLFileProps {
base64Data: string;
fileName: string;
type?: string | ImageTypes;
}
/**
* @desc app chooseImage base64 -> File
* @param {base64Data, fileName, type} DataURLFileProps
*/
export function dataURLtoFile({
base64Data,
fileName,
type = ImageTypes.JPG,
}: DataURLFileProps): File {
const arr = base64Data.split(",");
const bstr = window.atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
const file = new File([u8arr], `${fileName}.${type}`, {
type: `image/${type}`,
});
return file;
}
业务场景实现
import CropImg, { CropImgRef } from "./CropImg";
const cropRef = useRef<CropImgRef>();
const [imageSrc, setImageSrc] = useState("");
const [videoCoverState, setVideoCoverState] = useState(false)
const onVideoCoverReady = async () => {
const videoCover = form.getFieldValue("videoCover");
// 封面图无 并且正在上传中 则不替换
try {
if (!videoCover && !videoCoverState) {
setVideoCoverState(true);
const result = await cropRef.current.cropUploaded();
form.setFieldsValue({
videoCover: result,
});
}
} finally {
setVideoCoverState(false);
}
};
<CropImg
imageSrc={imageSrc}
ref={cropRef}
aspectWidth={16}
aspectHeight={9}
onCropReady={onVideoCoverReady}
/>