refactor(image-editor): 提取图片尺寸计算逻辑到共享模块
将 drawnix.tsx 和 popup-toolbar.tsx 中重复的图片尺寸计算代码提取到 image.ts: - getImageNaturalSize: 加载图片获取自然尺寸,支持超时和 fallback - calculateEditedImagePoints: 计算编辑后图片的新尺寸和位置 新增特性: - 5 秒超时机制,防止加载阻塞 - 除零保护,确保尺寸始终有效
This commit is contained in:
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Reference in new issue
Block a user