初始化模版工程
This commit is contained in:
436
components/image-editor/hooks/use-image-edit-actions.ts
Normal file
436
components/image-editor/hooks/use-image-edit-actions.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user