import { removeEditorImageBackground, type EditorBackgroundRemovalInput, type EditorBackgroundRemovalResult, } from '../../services/image-editor/editorProjectClient'; export type CropExpandInsets = { left: number; top: number; right: number; bottom: number; }; export type CropExpandGeometry = { outputWidth: number; outputHeight: number; sourceX: number; sourceY: number; sourceWidth: number; sourceHeight: number; targetX: number; targetY: number; }; export type CropExpandFrame = { x: number; y: number; width: number; height: number; }; export type RasterEditResult = { imageSrc: string; objectKey?: string | null; assetObjectId?: string | null; width: number; height: number; taskId?: string | null; elapsedMs?: number; provider?: string; }; function assertCanvasSupport() { if (typeof document === 'undefined') { throw new Error('当前环境不支持图片编辑'); } } function loadImageElement(source: string) { return new Promise((resolve, reject) => { const image = new Image(); image.crossOrigin = 'anonymous'; image.onload = () => resolve(image); image.onerror = () => reject(new Error('图片读取失败')); image.src = source; }); } function createRasterCanvas(width: number, height: number) { assertCanvasSupport(); const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const context = canvas.getContext('2d', { willReadFrequently: true }); if (!context) { throw new Error('当前浏览器不支持图片编辑'); } return { canvas, context }; } function resolveImageSize(image: HTMLImageElement) { return { width: Math.max(1, image.naturalWidth || image.width || 1), height: Math.max(1, image.naturalHeight || image.height || 1), }; } export function parseCropExpandInsetValue(value: string) { const trimmedValue = value.trim(); if (!trimmedValue) { return 0; } const parsedValue = Number.parseInt(trimmedValue, 10); if (!Number.isFinite(parsedValue)) { throw new Error('裁扩数值必须是整数'); } return parsedValue; } export function buildCropExpandInsetsFromFrame({ layer, frame, }: { layer: { x: number; y: number; width: number; height: number; originalWidth: number; originalHeight: number; }; frame: CropExpandFrame; }): CropExpandInsets { const scaleX = layer.width > 0 ? Math.max(1, layer.originalWidth) / layer.width : 1; const scaleY = layer.height > 0 ? Math.max(1, layer.originalHeight) / layer.height : 1; const sourceRight = layer.x + layer.width; const sourceBottom = layer.y + layer.height; const frameRight = frame.x + frame.width; const frameBottom = frame.y + frame.height; return { left: Math.round((layer.x - frame.x) * scaleX), top: Math.round((layer.y - frame.y) * scaleY), right: Math.round((frameRight - sourceRight) * scaleX), bottom: Math.round((frameBottom - sourceBottom) * scaleY), }; } export function buildCropExpandGeometry({ width, height, insets, }: { width: number; height: number; insets: CropExpandInsets; }): CropExpandGeometry { const sourceWidth = Math.max(1, Math.round(width || 1)); const sourceHeight = Math.max(1, Math.round(height || 1)); const cropLeft = Math.max(0, -insets.left); const cropTop = Math.max(0, -insets.top); const cropRight = Math.max(0, -insets.right); const cropBottom = Math.max(0, -insets.bottom); const targetX = Math.max(0, insets.left); const targetY = Math.max(0, insets.top); const drawableWidth = sourceWidth - cropLeft - cropRight; const drawableHeight = sourceHeight - cropTop - cropBottom; if (drawableWidth <= 0 || drawableHeight <= 0) { throw new Error('裁剪范围不能小于 1 像素'); } return { outputWidth: targetX + drawableWidth + Math.max(0, insets.right), outputHeight: targetY + drawableHeight + Math.max(0, insets.bottom), sourceX: cropLeft, sourceY: cropTop, sourceWidth: drawableWidth, sourceHeight: drawableHeight, targetX, targetY, }; } export function buildCropExpandLayerPatch({ layerX, layerY, insets, outputWidth, outputHeight, }: { layerX: number; layerY: number; insets: CropExpandInsets; outputWidth: number; outputHeight: number; }) { return { x: layerX - insets.left, y: layerY - insets.top, width: outputWidth, height: outputHeight, originalWidth: outputWidth, originalHeight: outputHeight, }; } export async function renderCropExpandImage({ source, insets, }: { source: string; insets: CropExpandInsets; }): Promise { const image = await loadImageElement(source); const imageSize = resolveImageSize(image); const geometry = buildCropExpandGeometry({ width: imageSize.width, height: imageSize.height, insets, }); const { canvas, context } = createRasterCanvas( geometry.outputWidth, geometry.outputHeight, ); // 中文注释:画布默认透明,扩大出来的新区域不填色,直接保留 alpha=0。 context.clearRect(0, 0, geometry.outputWidth, geometry.outputHeight); context.drawImage( image, geometry.sourceX, geometry.sourceY, geometry.sourceWidth, geometry.sourceHeight, geometry.targetX, geometry.targetY, geometry.sourceWidth, geometry.sourceHeight, ); return { imageSrc: canvas.toDataURL('image/png'), width: geometry.outputWidth, height: geometry.outputHeight, }; } export async function removeImageBackground( input: string | EditorBackgroundRemovalInput, ): Promise { return removeEditorImageBackground( typeof input === 'string' ? { sourceImageSrc: input } : input, ); }