552 lines
16 KiB
TypeScript
552 lines
16 KiB
TypeScript
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<HTMLElement> {
|
|
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<any>;
|
|
/* 切换编辑模式 */
|
|
setShowCrop: React.Dispatch<React.SetStateAction<boolean>>;
|
|
}
|
|
|
|
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<ImgViewRef, ImgViewProps>((props, ref) => {
|
|
const {
|
|
width,
|
|
height,
|
|
showScore = false,
|
|
data,
|
|
showOpt = false,
|
|
showAttachImgLabel = true,
|
|
screenshotButtonAlign = defaultAlignOption,
|
|
screenshotButtonRender = () => <div style={{ color: '#fff', width: '80px', top: 0, fontSize: 12 }}>回调DOM</div>,
|
|
hideLeftTopBtn = true,
|
|
onDraw,
|
|
viewOption = {},
|
|
type,
|
|
hideTypeBtns,
|
|
customEmpty,
|
|
} = props;
|
|
const {
|
|
imageKey,
|
|
attachImg,
|
|
odRect,
|
|
score,
|
|
objects = [],
|
|
} = data
|
|
const imgContainerRef = React.useRef(null);
|
|
|
|
// ============================= viewer =========================
|
|
const imgInsRef = useRef<any>(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<any>([]);
|
|
const [extendOdList, setExtendOdList] = useState([]);
|
|
const [selectRectId, setSelectRectId] = useState(null);
|
|
|
|
// 定位按钮相关参数
|
|
const alginContainerRef: any = useRef(null);
|
|
const alignRef: any = useRef(null);
|
|
const [cropRect, setCropRect] = useState<Rect | null>(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 (
|
|
<div className={classNames(`${componentName}`)} style={{ height, width }}>
|
|
{/*场景图大图 */}
|
|
{imageKey ?
|
|
<>
|
|
<div
|
|
className={classNames(
|
|
`${componentName}-main`,
|
|
cropType === CROP_TYPE['AUTO'] && `${componentName}-main--cursor`
|
|
)}
|
|
ref={imgContainerRef}
|
|
// style={{ width: width, height: height }}
|
|
/>
|
|
{/* 图片操作 */}
|
|
{!hideLeftTopBtn && (
|
|
<BtnGroup
|
|
className={classNames(`${componentName}-opt`)}
|
|
dataSource={operateBtnDataSource}
|
|
onClick={handleOptClick}
|
|
placement="left"
|
|
/>
|
|
)}
|
|
{!hideTypeBtns && showCrop && (
|
|
<BtnGroup
|
|
circle
|
|
className={classNames(`${componentName}-crop-opt`)}
|
|
dataSource={cropBtnDataSource}
|
|
onClick={handleCropBtnClick}
|
|
selectKey={cropType === CROP_TYPE['AUTO'] ? 'autoCrop' : 'customCrop'}
|
|
/>
|
|
)}
|
|
{showCrop && cropRect && screenshotButtonRender && (
|
|
<>
|
|
<div
|
|
ref={alginContainerRef}
|
|
className={classNames(`${componentName}-align`)}
|
|
style={Object.assign(
|
|
{
|
|
width: cropRect.w,
|
|
height: cropRect.h,
|
|
},
|
|
getTransforms({
|
|
translateX: cropRect.x,
|
|
translateY: cropRect.y,
|
|
})
|
|
)}
|
|
></div>
|
|
<Align
|
|
ref={alignRef}
|
|
monitorWindowResize
|
|
align={screenshotButtonAlign}
|
|
target={function () {
|
|
return alginContainerRef.current;
|
|
}}
|
|
>
|
|
{screenshotButtonRender({
|
|
model: 'IMAGE',
|
|
// @ts-ignore
|
|
getCropInfo,
|
|
setShowCrop,
|
|
cropType,
|
|
selectAlgorithmVersion,
|
|
})}
|
|
</Align>
|
|
</>
|
|
)}
|
|
{/* 场景图小图 */}
|
|
{attachImg?.length && !showCrop && (
|
|
<AttachImage
|
|
showAttachImgLabel={showAttachImgLabel}
|
|
data={attachImg}
|
|
/>
|
|
)}
|
|
{(showScore || score) && <div
|
|
style={{ bottom: 20 }}
|
|
className={classNames(`${componentName}__face-score`)}
|
|
>
|
|
{`人脸质量分:${(Number(score) as number).toFixed(2)}`}
|
|
</div>}
|
|
</>
|
|
:
|
|
<div style={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
|
{customEmpty || <Empty style={{ margin: 0 }} image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无数据" />}
|
|
</div>
|
|
}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
BigImagePreview.displayName = 'BigImagePreview';
|
|
|
|
export default BigImagePreview;
|