import React, { useEffect, ReactElement, useState, useRef, useImperativeHandle } from 'react'; import classNames from 'classnames'; import { get, pick, isNull, generateImg, dataURLToBlob, getTransforms, addEventListenerWrapper, getFileByRect } from '@zhst/func'; import Align from 'rc-align'; import { Empty, AttachImage } from '..'; import { type Rect, type IScreenshotButtonProp, type AlignType } from '@zhst/types' import { Cropper, Viewer, EVENT_VIEWER_TRANSFORM_CHANGE, EVENT_VIEWER_READY, EVENT_CROP_START, EVENT_CROP_END, } from '../ImageEditor'; import BtnGroup from './components/BtnGroup'; import './index.less' import { defaultAlignOption, CROP_TYPE } from '../utils/constants' import { getOdRect, IOdObject, getExtendRect, getTransformRect, getRotateImg, } from './bigImagePreviewHelper' const componentName = `zhst-image__img-view`; export interface ViewOption { /* 图片url */ image?: string | HTMLImageElement; /* 缩放灵敏度(0,1],default: 0.1 */ wheelZoomRatio?: number; /* * 是否允许缩放 * @default: true */ scaleAble?: boolean; /* * 是否允许拖拽 * @default: true */ dragAble?: boolean; /* * fit scale 作为 最小缩放 * @default: false */ fitScaleAsMinScale?: boolean; } export interface ImgViewProps extends React.HTMLAttributes { data: { url?: string imageKey: string attachImg?: Array<{ label: string; url: string }>; // 缩略图列表 odRect?: Rect score?: number objects?: IOdObject } showAttachImgLabel?: boolean; // 是否显示缩略图 showOpt?: boolean; // 是否显示操作面板 width?: string | number; // 画布宽度 height?: string | number; // 画布高度 /* 截图渲染 */ screenshotButtonAlign?: AlignType; screenshotButtonRender?: (screenshotButtonProp: IScreenshotButtonProp) => ReactElement; hideLeftTopBtn?: boolean; onDraw?: (obj: { rectList: any; extendRectList: { x: number; y: number; w: number; h: number; }[]; selectIndex: number; imgKey: string; }) => void showScore?: boolean // 是否显示相似度 viewOption?: ViewOption; onRectSelect?: (data: { rectList: any[]; extendRectList: any[]; selectIndex: number; imgKey: string; }) => void type?: 'CUSTOM' | 'AUTO' hideTypeBtns?: boolean customEmpty?: any } export interface ImgViewRef { /* 图片实例 */ imgInsRef: React.MutableRefObject; /* 切换编辑模式 */ setShowCrop: React.Dispatch>; } const cropBtnDataSource = [ { key: 'close', icon: 'icon-danchuangguanbi', title: '退出', }, { key: 'autoCrop', icon: 'icon-zidong', title: '智能框选', }, { key: 'customCrop', icon: 'icon-shoudong', title: '手动框选', }, ]; const operateBtnDataSource = [ { key: 'zoomOut', icon: 'icon-fangda', title: '放大', }, { key: 'zoomIn', icon: 'icon-suoxiao', title: '缩小', }, { key: 'reset', icon: 'icon-zhongzhi3', title: '重置图片', }, ]; export const BigImagePreview = React.forwardRef((props, ref) => { const { width, height, showScore = false, data, showOpt = false, showAttachImgLabel = true, screenshotButtonAlign = defaultAlignOption, screenshotButtonRender = () =>
回调DOM
, hideLeftTopBtn = true, onDraw, viewOption = {}, type, hideTypeBtns, customEmpty, } = props; const { imageKey, attachImg, odRect, score, objects = [], } = data const imgContainerRef = React.useRef(null); // ============================= viewer ========================= const imgInsRef = useRef(null); const [isImgReady, setIsImgReady] = useState(false); useEffect(() => { if (!imgContainerRef?.current) return; const handleReady = addEventListenerWrapper(imgContainerRef.current, EVENT_VIEWER_READY, () => { setIsImgReady(true); }); const handleTransformChange = addEventListenerWrapper( imgContainerRef.current, EVENT_VIEWER_TRANSFORM_CHANGE, () => { } ); imgInsRef.current = new Viewer(imgContainerRef.current, { ...viewOption, fitScaleAsMinScale: true, image: generateImg(imageKey), }); return () => { handleReady?.remove(); handleTransformChange?.remove(); imgInsRef?.current?.destroy?.(); imgInsRef.current = null; }; }, [imageKey]); // ============================= viewer操作按钮 ========================= const handleOptClick = (v: string) => { switch (v) { case 'zoomOut': imgInsRef?.current?.scaleTo?.(0.1); break; case 'zoomIn': imgInsRef?.current?.scaleTo?.(-0.1); break; case 'reset': imgInsRef?.current?.reset?.(-0.1); break; } }; // ============================= cropper ========================= // 手动截图相关参数 const cropInsRef: any = useRef(null); const [showCrop, setShowCrop] = useState(showOpt); const [cropType, setCropType] = useState(type || CROP_TYPE['AUTO']); // 自动截图相关参数 const [odList, setOdList] = useState([]); const [extendOdList, setExtendOdList] = useState([]); const [selectRectId, setSelectRectId] = useState(null); // 定位按钮相关参数 const alginContainerRef: any = useRef(null); const alignRef: any = useRef(null); const [cropRect, setCropRect] = useState(null); // 选中的版本号 const [selectAlgorithmVersion, setSelectAlgorithmVersion] = useState(null); const handlerCropStartRef: any = useRef(null); const handlerCropEndRef: any = useRef(null); const handleShapeSelectRef: any = useRef(null); useEffect(() => { initData(objects) return () => { imgInsRef.current?.clearShape?.(); handlerCropStartRef.current?.remove(); handlerCropEndRef.current?.remove(); handleShapeSelectRef.current?.remove(); cropInsRef?.current?.destroy?.(); cropInsRef.current = null; }; }, [isImgReady, showCrop, cropType, imageKey]); // 初始化页面的绘制矩形 const initData = (_objects: IOdObject | never[]) => { const imgIns = imgInsRef.current; const _odRect = odRect //清理crop setCropRect(null); if (!isImgReady) return; if (!showCrop) { imgIns?.addShape?.({ x: get(_odRect, 'x', 0), y: get(_odRect, 'y', 0), w: get(_odRect, 'w', 0), h: get(_odRect, 'h', 0), selectAble: false, }); return; } // 自动模式 if (cropType === CROP_TYPE['AUTO']) { const handleGetOD = (odList: any) => { const imgSize = imgIns.getImgSize(); const shapeList = odList.map((rect: { [x: string]: any; algorithmVersion: any; }) => ({ ...rect, selectAble: true, id: ['id'], algorithmVersion: rect.algorithmVersion, })); imgIns.replaceShape(shapeList); //顺便吧扩展框拿到 const extendRect = shapeList.map((rect: { algorithmVersion: string; }) => { // @ts-ignore const _extendRect = getExtendRect(rect, imgSize.w, imgSize.h, rect.algorithmVersion); return { ...rect, ..._extendRect }; }); setExtendOdList(extendRect); imgIns.replaceShape(shapeList); // 框选监听事件 handleShapeSelectRef.current = addEventListenerWrapper(imgContainerRef.current, 'shape-select', async (e: { detail: any; }) => { const id = e.detail; setSelectRectId(id); const selectShape = shapeList.find((v: { [x: string]: any; }) => v['id'] === id); console.log('selectShape', selectShape,) if (selectShape) { setSelectAlgorithmVersion(selectShape['algorithmVersion']); //换算成屏幕坐标 const axisRect = imgIns.imgRectAxisToCanvasAxisRect(selectShape); const rect = { x: axisRect.x2 > axisRect.x ? axisRect.x : axisRect.x2, y: axisRect.y2 > axisRect.y ? axisRect.y : axisRect.y2, w: Math.abs(axisRect.x2 - axisRect.x), h: Math.abs(axisRect.y2 - axisRect.y), }; setCropRect(rect); onDraw?.({ rectList: [rect], extendRectList: [rect], imgKey: imageKey, selectIndex: id }) } else { // @ts-ignore setCropRect(null); } }); }; // @ts-ignore const rect = getOdRect({ objects: _objects }) setOdList(rect); handleGetOD(rect); } // 手动模式 if (cropType === CROP_TYPE['CUSTOM']) { // 手动框选状态预先清除imgIns imgIns?.clearShape?.(); handlerCropStartRef.current = addEventListenerWrapper(imgContainerRef.current, EVENT_CROP_START, () => { setSelectAlgorithmVersion(null); setCropRect(null); }); handlerCropEndRef.current = addEventListenerWrapper(imgContainerRef.current, EVENT_CROP_END, async (event: { detail: any; }) => { const data = event.detail; setSelectAlgorithmVersion(null); const _rect = { x: data.left, y: data.top, w: data.width, h: data.height, } setCropRect(_rect); const _cropData = await getCropInfo({ type: cropType, rect: _rect }) onDraw?.(_cropData) alignRef?.current?.forceAlign?.(); }); cropInsRef.current = new Cropper(imgContainerRef.current, { showMask: true, type: 'arrow', viewer: imgIns, }); } } // 获取框选的截图框信息 const getCropInfo = async (opt: { type: string; rect: Rect }) => { const { type, rect } = opt const cropType = type; const cropRect = rect; const imgIns = imgInsRef.current; const transform = imgIns.targetTransform; let newImgKey = imageKey; let rectList: any = []; let extendRectList = []; let selectIndex = 0; switch (cropType) { case CROP_TYPE['AUTO']: const shapes = imgIns.getSelectShape(); const shapeIds = shapes.map((v: { [x: string]: any; }) => v['id']); rectList = odList .filter((v: { [x: string]: any; }) => shapeIds.includes(v['id'])) .map((item: any) => { if ( item.algorithmVersion === 'OBJECT_TYPE_FACE' || item.objectType === 'OBJECT_TYPE_FACE' ) { if (!isNull(item.extendBox)) { return { ...item, w: get(item, 'extendBox.w'), h: get(item, 'extendBox.h'), x: get(item, 'extendBox.x'), y: get(item, 'extendBox.y'), }; } } else { return item; } }); extendRectList = extendOdList .filter((v) => shapeIds.includes(v['id'])) .map((v) => pick(v, ['x', 'y', 'w', 'h', 'algorithmVersion', 'id'])); selectIndex = rectList.findIndex((v: { [x: string]: null; }) => v['id'] === selectRectId); break; default: //获取旋转过的坐标 // @ts-ignore const newRect = getTransformRect(imgIns.image, transform, cropRect); //判断是不是旋转过 if (get(transform, 'rotate', 0) % 360 != 0) { const data = getRotateImg(imgIns.image, get(transform, 'rotate', 0)) as any; //在画布上画旋转后的图片 newImgKey = data as any; } rectList.push(newRect); extendRectList.push(newRect); break; } //扩展框获取imgkey await Promise.all( extendRectList.map(async (rect, index) => { const file = await getFileByRect(imgIns.image, rect); const imgKey = file; extendRectList[index] = { ...rect, imgKey }; }) ); //人脸图获取矫正图 await Promise.all( rectList.map(async (rect: { [x: string]: any; }, index: string | number) => { const faceCorrectImage = rect['faceCorrectImage']; let faceCorrectImageKey; if (faceCorrectImage) { const base64 = `data:image/jpg;base64,${faceCorrectImage}`; const blobData = dataURLToBlob(base64); const file = new window.File([blobData], `${new Date().getTime()}`); faceCorrectImageKey = file } const newRect: any = { ...rect, ...(faceCorrectImageKey ? { faceCorrectImageKey } : {}), }; delete newRect['faceCorrectImage']; rectList[index] = newRect; }) ); let data = { rectList, extendRectList, selectIndex, imgKey: newImgKey } return data; }; // 操作界面判断 const handleCropBtnClick = (v: string) => { switch (v) { case 'close': setShowCrop(false); break; case 'autoCrop': setCropType(CROP_TYPE['AUTO']); break; case 'customCrop': setCropType(CROP_TYPE['CUSTOM']); break; } }; // ============================== Ref =============================== useImperativeHandle(ref, () => ({ imgInsRef, setShowCrop, initData, getCropInfo, })); return (
{/*场景图大图 */} {imageKey ? <>
{/* 图片操作 */} {!hideLeftTopBtn && ( )} {!hideTypeBtns && showCrop && ( )} {showCrop && cropRect && screenshotButtonRender && ( <>
{screenshotButtonRender({ model: 'IMAGE', // @ts-ignore getCropInfo, setShowCrop, cropType, selectAlgorithmVersion, })} )} {/* 场景图小图 */} {attachImg?.length && !showCrop && ( )} {(showScore || score) &&
{`人脸质量分:${(Number(score) as number).toFixed(2)}`}
} :
{customEmpty || }
}
); }); BigImagePreview.displayName = 'BigImagePreview'; export default BigImagePreview;