diff --git a/packages/drawnix/src/components/toolbar/popup-toolbar/popup-toolbar.tsx b/packages/drawnix/src/components/toolbar/popup-toolbar/popup-toolbar.tsx index 8fe8328a..7663c812 100644 --- a/packages/drawnix/src/components/toolbar/popup-toolbar/popup-toolbar.tsx +++ b/packages/drawnix/src/components/toolbar/popup-toolbar/popup-toolbar.tsx @@ -77,6 +77,7 @@ import { MessagePlugin } from 'tdesign-react'; import { mergeVideos } from '../../../services/video-merge-webcodecs'; import { ImageEditor } from '../../image-editor'; import { insertImageFromUrl } from '../../../data/image'; +import { calculateEditedImagePoints } from '../../../utils/image'; export const PopupToolbar = () => { const board = useBoard(); @@ -1171,40 +1172,17 @@ export const PopupToolbar = () => { // 使用 Transforms.setNode 更新画布中的图片元素 const elementIndex = board.children.findIndex(child => child.id === editingImageElement.id); if (elementIndex >= 0) { - // 获取原元素的位置和尺寸 const element = board.children[elementIndex] as any; - const [start, end] = element.points || [[0, 0], [0, 0]]; - const originalDisplayWidth = end[0] - start[0]; - const originalDisplayHeight = end[1] - start[1]; - - // 获取原图的实际尺寸(如果元素没有存储,则从原图 URL 加载) - let originalNaturalWidth = element.width; - let originalNaturalHeight = element.height; - - if (!originalNaturalWidth || !originalNaturalHeight) { - // 从原图 URL 加载获取实际尺寸 - const originalImg = new Image(); - await new Promise((resolve) => { - originalImg.onload = () => resolve(); - originalImg.onerror = () => resolve(); // 失败时使用显示尺寸 - originalImg.src = element.url; - }); - originalNaturalWidth = originalImg.naturalWidth || originalDisplayWidth; - originalNaturalHeight = originalImg.naturalHeight || originalDisplayHeight; - } - - // 计算原图的缩放比例 - const scaleX = originalDisplayWidth / originalNaturalWidth; - const scaleY = originalDisplayHeight / originalNaturalHeight; - // 使用较小的缩放比例保持宽高比一致 - const scale = Math.min(scaleX, scaleY); - - // 计算新的显示尺寸,保持原图的缩放比例 - const newDisplayWidth = img.naturalWidth * scale; - const newDisplayHeight = img.naturalHeight * scale; - - // 计算新的 points,保持左上角位置不变 - const newPoints = [start, [start[0] + newDisplayWidth, start[1] + newDisplayHeight]]; + const { newPoints } = await calculateEditedImagePoints( + { + url: element.url, + width: element.width, + height: element.height, + points: element.points || [[0, 0], [0, 0]], + }, + img.naturalWidth, + img.naturalHeight + ); Transforms.setNode(board, { url: stableUrl, diff --git a/packages/drawnix/src/drawnix.tsx b/packages/drawnix/src/drawnix.tsx index d6f10bcc..de28251d 100644 --- a/packages/drawnix/src/drawnix.tsx +++ b/packages/drawnix/src/drawnix.tsx @@ -84,6 +84,7 @@ import { withDefaultFill } from './plugins/with-default-fill'; import { withGradientFill } from './plugins/with-gradient-fill'; import { API_AUTH_ERROR_EVENT, ApiAuthErrorDetail } from './utils/api-auth-error-event'; import { MessagePlugin } from 'tdesign-react'; +import { calculateEditedImagePoints } from './utils/image'; const TTDDialog = lazy(() => import('./components/ttd-dialog/ttd-dialog').then(module => ({ default: module.TTDDialog }))); const SettingsDialog = lazy(() => import('./components/settings-dialog/settings-dialog').then(module => ({ default: module.SettingsDialog }))); @@ -871,39 +872,16 @@ const DrawnixContent: React.FC = ({ const elementIndex = board.children.findIndex(child => child.id === elementId); if (elementIndex >= 0) { const element = board.children[elementIndex] as any; - // 获取原元素的位置和尺寸 - const [start, end] = element.points || [[0, 0], [0, 0]]; - const originalDisplayWidth = end[0] - start[0]; - const originalDisplayHeight = end[1] - start[1]; - - // 获取原图的实际尺寸(如果元素没有存储,则从原图 URL 加载) - let originalNaturalWidth = element.width; - let originalNaturalHeight = element.height; - - if (!originalNaturalWidth || !originalNaturalHeight) { - // 从原图 URL 加载获取实际尺寸 - const originalImg = new Image(); - await new Promise((resolve) => { - originalImg.onload = () => resolve(); - originalImg.onerror = () => resolve(); // 失败时使用显示尺寸 - originalImg.src = element.url; - }); - originalNaturalWidth = originalImg.naturalWidth || originalDisplayWidth; - originalNaturalHeight = originalImg.naturalHeight || originalDisplayHeight; - } - - // 计算原图的缩放比例 - const scaleX = originalDisplayWidth / originalNaturalWidth; - const scaleY = originalDisplayHeight / originalNaturalHeight; - // 使用较小的缩放比例保持宽高比一致 - const scale = Math.min(scaleX, scaleY); - - // 计算新的显示尺寸,保持原图的缩放比例 - const newDisplayWidth = img.naturalWidth * scale; - const newDisplayHeight = img.naturalHeight * scale; - - // 计算新的 points,保持左上角位置不变 - const newPoints = [start, [start[0] + newDisplayWidth, start[1] + newDisplayHeight]]; + const { newPoints } = await calculateEditedImagePoints( + { + url: element.url, + width: element.width, + height: element.height, + points: element.points || [[0, 0], [0, 0]], + }, + img.naturalWidth, + img.naturalHeight + ); Transforms.setNode(board, { url: stableUrl, diff --git a/packages/drawnix/src/utils/image.ts b/packages/drawnix/src/utils/image.ts index c68be4d6..fc82a020 100644 --- a/packages/drawnix/src/utils/image.ts +++ b/packages/drawnix/src/utils/image.ts @@ -5,6 +5,95 @@ import { fileOpen } from '../data/filesystem'; import { IMAGE_MIME_TYPES } from '../constants'; import { insertImage } from '../data/image'; +/** + * 加载图片获取其自然尺寸(带超时机制) + * @param url 图片 URL + * @param fallbackWidth 加载失败时的回退宽度 + * @param fallbackHeight 加载失败时的回退高度 + * @param timeout 超时时间(毫秒),默认 5000ms + */ +export async function getImageNaturalSize( + url: string, + fallbackWidth: number, + fallbackHeight: number, + timeout = 5000 +): Promise<{ width: number; height: number }> { + const img = new Image(); + + await Promise.race([ + new Promise((resolve) => { + img.onload = () => resolve(); + img.onerror = () => resolve(); // 失败时使用 fallback + img.src = url; + }), + new Promise((resolve) => setTimeout(resolve, timeout)) + ]); + + const width = img.naturalWidth || fallbackWidth; + const height = img.naturalHeight || fallbackHeight; + + // 防止除零错误 + return { + width: width > 0 ? width : fallbackWidth, + height: height > 0 ? height : fallbackHeight, + }; +} + +/** + * 计算图片编辑后的新尺寸和位置 + * 保持原图的缩放比例,左上角位置不变 + */ +export interface ImageElementInfo { + url: string; + width?: number; + height?: number; + points: [[number, number], [number, number]]; +} + +export interface ScaledImageResult { + newPoints: [[number, number], [number, number]]; + scale: number; +} + +export async function calculateEditedImagePoints( + element: ImageElementInfo, + newNaturalWidth: number, + newNaturalHeight: number +): Promise { + const [start, end] = element.points; + const originalDisplayWidth = end[0] - start[0]; + const originalDisplayHeight = end[1] - start[1]; + + // 获取原图的实际尺寸 + let originalNaturalWidth = element.width; + let originalNaturalHeight = element.height; + + if (!originalNaturalWidth || !originalNaturalHeight) { + const size = await getImageNaturalSize( + element.url, + originalDisplayWidth, + originalDisplayHeight + ); + originalNaturalWidth = size.width; + originalNaturalHeight = size.height; + } + + // 计算原图的缩放比例 + const scaleX = originalDisplayWidth / originalNaturalWidth; + const scaleY = originalDisplayHeight / originalNaturalHeight; + // 使用较小的缩放比例保持宽高比一致 + const scale = Math.min(scaleX, scaleY); + + // 计算新的显示尺寸 + const newDisplayWidth = newNaturalWidth * scale; + const newDisplayHeight = newNaturalHeight * scale; + + return { + newPoints: [start, [start[0] + newDisplayWidth, start[1] + newDisplayHeight]], + scale, + }; +} + export const saveAsImage = (board: PlaitBoard, isTransparent: boolean) => { const selectedElements = getSelectedElements(board); boardToImage(board, {