nicecode-v2/packages/meta/src/BigImagePreview/BigImagePreview.tsx

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;