refactor(image-editor): 提取图片尺寸计算逻辑到共享模块

将 drawnix.tsx 和 popup-toolbar.tsx 中重复的图片尺寸计算代码提取到 image.ts:
- getImageNaturalSize: 加载图片获取自然尺寸,支持超时和 fallback
- calculateEditedImagePoints: 计算编辑后图片的新尺寸和位置

新增特性:
- 5 秒超时机制,防止加载阻塞
- 除零保护,确保尺寸始终有效
This commit is contained in:
liquid committed 2026-01-23 22:56:36 +08:00
1 parent 81a59bacca
commit 596faab2ee
3 files changed
+111 -66

No files matched your search

@@ -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<void>((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,
+11 -33
View File
@@ -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<DrawnixContentProps> = ({
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<void>((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,
+89
View File
@@ -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<void>((resolve) => {
img.onload = () => resolve();
img.onerror = () => resolve(); // 失败时使用 fallback
img.src = url;
}),
new Promise<void>((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<ScaledImageResult> {
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, {