import { useCallback } from 'react' import { v4 as uuidv4 } from 'uuid' import { toast } from 'sonner' import { imageMatting, imageOCR, recognizeImage, updateImageOCRText, segmentLayer, getSegmentLayerResult, } from '../service/api' import type { ImageOCRResponse, ImageOCRTextUpdateItem, RecognizedImageElement, } from '../service/type' import { type ImageElement, type PlaceholderElement, type SceneElement, type ArtboardElement, useDominoStoreInstance, } from '../components/canvas' import { MAX_IMAGE_SIZE_BYTES, MATTING_MIN_DIM, MATTING_MAX_DIM, NEED_COMPRESS_MAX_SIZE, NEED_COMPRESS_MIN_SIZE, } from '../consts' import { getImageFileSize, getImageDimension, getImageUrl, isValidImageFormat, isValidImageSize, manualPersistence, } from '../utils/helper' export function useImageEditActions( taskId: string, addElementToFlow: (elementTemplate: SceneElement) => SceneElement, onPartialRedraw?: ( element: ImageElement, recognizedElements: readonly RecognizedImageElement[], ) => void, ) { const store = useDominoStoreInstance() const handleMatting = useCallback( async (element: ImageElement) => { const { originalWidth: w, originalHeight: h } = element // 1. Dimension check if (w < MATTING_MIN_DIM || h < MATTING_MIN_DIM) { toast.error('图片尺寸不能小于 32x32px') return } if (w > MATTING_MAX_DIM || h > MATTING_MAX_DIM) { toast.error('图片尺寸不能超过 4000x4000px') return } // 2. Size check const { updateElement } = store.getState() const fileSize = await getImageFileSize(element) if (!element.data?.size && fileSize > 0) { updateElement(element.id, { data: { ...element.data, size: fileSize } }) } if (fileSize > MAX_IMAGE_SIZE_BYTES) { toast.error('图片大小不能超过 20MB') return } const placeholderId = uuidv4() const { removePlaceholder, updateElementUIState, addElement } = store.getState() // Immediately insert placeholder element for instant feedback const placeholder: PlaceholderElement = { id: placeholderId, type: 'placeholder', x: 0, y: 0, width: element.width, height: element.height, rotation: element.rotation || 0, originalWidth: element.width, originalHeight: element.height, label: '生成中', } const addedPlaceholder = addElementToFlow( placeholder, ) as PlaceholderElement updateElementUIState(placeholderId, { status: 'pending' }) try { const res = await imageMatting({ image_url: element.data?.path || element.id, task_id: taskId, compress: fileSize > NEED_COMPRESS_MIN_SIZE, }) try { // Preload image const url = await getImageUrl(taskId, res.path) const { width, height } = await getImageDimension(url) // Replace placeholder with matting result removePlaceholder(placeholderId) const newImage: ImageElement = { ...addedPlaceholder, id: res.path, type: 'image', src: url, width, height, fileName: res.path.split('/').pop() || 'image.jpg', originalWidth: width, originalHeight: height, data: { path: res.path, }, } addElement(newImage) updateElementUIState(res.path, { status: 'idle' }) // Support background task persistence manualPersistence(taskId, store) } catch (e) { console.warn('Preload failed', e) } } catch (error) { console.error(error) removePlaceholder(placeholderId) } }, [taskId, addElementToFlow, store], ) const handlePartialRedraw = useCallback( async (element: ImageElement) => { if (!isValidImageFormat(element.fileName)) { toast.error('不合法的图片格式') return } if (!isValidImageSize(element.originalWidth, element.originalHeight)) { toast.error('图片尺寸不能超过 2160x3840px 或 3840x2160px') return } const { updateElement, updateElementUIState } = store.getState() const fileSize = await getImageFileSize(element) if (!element.data?.size && fileSize > 0) { updateElement(element.id, { data: { ...element.data, size: fileSize } }) } if (fileSize >= MAX_IMAGE_SIZE_BYTES) { toast.error('图片大小不能超过 20MB') return } updateElementUIState(element.id, { status: 'pending', statusText: '识图中...', }) try { const res = await recognizeImage({ image_url: element.data?.path || element.id, task_id: taskId, width: element.originalWidth, height: element.originalHeight, }) onPartialRedraw?.(element, res || []) } catch (err) { console.error('Failed to recognize image:', err) } finally { updateElementUIState(element.id, { status: 'idle', }) } }, [taskId, onPartialRedraw, store], ) const handleEditText = useCallback( async ( element: ImageElement, onOCRSuccess: (data: ImageOCRResponse) => void, ) => { if (!isValidImageFormat(element.fileName)) { toast.error('不合法的图片格式') return } if (!isValidImageSize(element.originalWidth, element.originalHeight)) { toast.error('图片尺寸不能超过 2160x3840px 或 3840x2160px') return } const { updateElement, updateElementUIState } = store.getState() const fileSize = await getImageFileSize(element) if (!element.data?.size && fileSize > 0) { updateElement(element.id, { data: { ...element.data, size: fileSize } }) } if (fileSize >= MAX_IMAGE_SIZE_BYTES) { toast.error('图片大小不能超过 20MB') return } updateElementUIState(element.id, { status: 'pending', statusText: '正在提取文字', }) const errorText = '没有提取到文字' try { const data = await imageOCR({ image_url: element.data?.path || element.id, task_id: taskId, }) if (data.length > 0) { onOCRSuccess(data) } else { toast.error(errorText) } } catch (error) { console.error(error) toast.error(errorText) } finally { updateElementUIState(element.id, { status: 'idle' }) } }, [taskId, store], ) const handleConfirmEditText = useCallback( async (element: ImageElement, updatedList: ImageOCRTextUpdateItem[]) => { const placeholderId = uuidv4() const { removePlaceholder, updateElementUIState, addElement } = store.getState() // Insert new placeholder element into the flow const placeholder: PlaceholderElement = { id: placeholderId, type: 'placeholder', x: 0, y: 0, width: element.width, height: element.height, rotation: element.rotation || 0, originalWidth: element.width, originalHeight: element.height, label: '处理中...', } const addedPlaceholder = addElementToFlow( placeholder, ) as PlaceholderElement updateElementUIState(placeholderId, { status: 'pending' }) try { const res = await updateImageOCRText({ image_url: element.data?.path || element.id, texts: updatedList, task_id: taskId, compress: (element.data?.size ?? 0) > NEED_COMPRESS_MAX_SIZE, }) // Preload next image try { const url = await getImageUrl(taskId, res.path) const { width, height } = await getImageDimension(url) // Replace placeholder with result removePlaceholder(placeholderId) const newImage: ImageElement = { ...addedPlaceholder, id: res.path, type: 'image', src: url, width, height, fileName: res.path.split('/').pop() || 'image.jpg', originalWidth: width, originalHeight: height, data: { path: res.path, }, } addElement(newImage) updateElementUIState(res.path, { status: 'idle' }) // Support background task persistence manualPersistence(taskId, store) } catch (e) { console.warn('Preload failed', e) } } catch (error) { console.error(error) removePlaceholder(placeholderId) toast.error('修改失败') } }, [taskId, addElementToFlow, store], ) const handleEditElements = useCallback( async (element: ImageElement) => { const { updateElementUIState, addElement, setSelectedIds } = store.getState() updateElementUIState(element.id, { status: 'pending', statusText: '正在分层...', }) try { const taskRecordId = await segmentLayer({ image_url: element.data?.path || element.id, task_id: taskId, width: element.originalWidth, height: element.originalHeight, }) // Polling loop let task = await getSegmentLayerResult(taskRecordId) while (task.status === 'PROCESSING') { await new Promise(resolve => setTimeout(resolve, 2000)) task = await getSegmentLayerResult(taskRecordId) } if ( task.status === 'TIMEOUT' || task.status === 'FAILED' || !task.layers ) { throw new Error('分层任务执行失败') } const layersData = task.layers // 1. Prepare all layers (Sorted by layer_order from server) const allLayersData = layersData.map(layer => ({ image: layer.image, line_rect: { x: layer.x, y: layer.y, width: layer.width, height: layer.height, }, line_text: layer.desc, hideMetadata: layer.desc === 'background', })) // 2. Preload all layers and prepare IDs const layers = await Promise.all( allLayersData.map(async item => { const itemUrl = await getImageUrl(taskId, item.image.path) const childId = uuidv4() return { item, itemUrl, childId } }), ) // 3. Create Artboard Template const artboardId = uuidv4() const artboardTemplate: ArtboardElement = { id: artboardId, type: 'artboard', name: `画板 - ${element.fileName}`, x: 0, y: 0, width: element.width, height: element.height, rotation: element.rotation || 0, originalWidth: element.originalWidth, originalHeight: element.originalHeight, background: '#FFFFFF', childrenIds: layers.map(l => l.childId), } // Add artboard to flow const addedArtboard = addElementToFlow( artboardTemplate, ) as ArtboardElement // 4. Add all child elements to store for (const layer of layers) { const { item, itemUrl, childId } = layer const childImage: ImageElement = { id: childId, type: 'image', x: item.line_rect.x, y: item.line_rect.y, width: item.line_rect.width, height: item.line_rect.height, rotation: 0, originalWidth: item.line_rect.width, originalHeight: item.line_rect.height, src: itemUrl, fileName: item.image.path.split('/').pop() || 'layer.png', parentId: artboardId, selectable: true, hideMetadata: item.hideMetadata, data: { path: item.image.path, text: item.line_text, }, } addElement(childImage) } setSelectedIds([addedArtboard.id]) // Support background task persistence manualPersistence(taskId, store) } catch (err) { console.error('Failed to segment text layer:', err) toast.error('修改失败') } finally { updateElementUIState(element.id, { status: 'idle', statusText: undefined, }) } }, [taskId, store, addElementToFlow], ) return { handleMatting, handlePartialRedraw, handleEditText, handleConfirmEditText, handleEditElements, } }