2218 lines
62 KiB
JavaScript
2218 lines
62 KiB
JavaScript
/* eslint-disable no-async-promise-executor */
|
||
import { fabric } from "fabric-with-all";
|
||
import { LayerType, OperationType, createBitmapLayer } from "./layerHelper";
|
||
// 导入新的复合命令
|
||
import { CreateImageLayerCommand } from "../commands/LayerCommands";
|
||
// 导入新的命令
|
||
import { ChangeFixedImageCommand, AddImageToLayerCommand } from "../commands/LayerCommands";
|
||
import { generateId } from "./helper";
|
||
import { isBoolean } from "lodash-es";
|
||
|
||
/**
|
||
* 加载并处理图片
|
||
* @param {string} imageSource - 图片URL或Base64字符串
|
||
* @param {Object} options - 配置选项
|
||
* @param {number} options.maxWidth - 最大宽度
|
||
* @param {number} options.maxHeight - 最大高度
|
||
* @param {boolean} options.centerOnCanvas - 是否居中图片
|
||
* @param {function} options.onLoad - 加载完成回调
|
||
* @returns {Promise<Object>} - 返回图片对象的Promise
|
||
*/
|
||
export function loadImage(imageSource, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
fabric.Image.fromURL(
|
||
imageSource,
|
||
(fabricImage) => {
|
||
if (!fabricImage) {
|
||
reject(new Error("加载图片失败"));
|
||
return;
|
||
}
|
||
|
||
// 计算缩放比例
|
||
const imgWidth = fabricImage.width;
|
||
const imgHeight = fabricImage.height;
|
||
|
||
// 应用缩放
|
||
if (options.maxWidth && options.maxHeight) {
|
||
const scaleX = options.maxWidth / imgWidth;
|
||
const scaleY = options.maxHeight / imgHeight;
|
||
const scale = Math.min(scaleX, scaleY, 1); // 不超过原始大小
|
||
|
||
fabricImage.scale(scale);
|
||
}
|
||
|
||
// 设置图片位置 - 默认居中
|
||
if (options.centerOnCanvas !== false) {
|
||
fabricImage.set({
|
||
id: generateId("fabricImage"),
|
||
left: (options.canvasWidth || 800) / 2,
|
||
top: (options.canvasHeight || 600) / 2,
|
||
originX: "center",
|
||
originY: "center",
|
||
selectable: true,
|
||
hasControls: true,
|
||
hasBorders: true,
|
||
});
|
||
}
|
||
|
||
// 执行加载完成回调
|
||
if (typeof options.onLoad === "function") {
|
||
options.onLoad(fabricImage);
|
||
}
|
||
|
||
resolve(fabricImage);
|
||
},
|
||
{ crossOrigin: "anonymous" }
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 创建图片图层
|
||
* @param {Object} layerManager - 图层管理器
|
||
* @param {Object} fabricImage - fabric图片对象
|
||
* @param {Object} toolManager - 工具管理器
|
||
* @param {string} layerName - 图层名称 (可选)
|
||
* @returns {Promise<string>} 新图层ID
|
||
*/
|
||
export async function createImageLayer({
|
||
layerManager,
|
||
fabricImage,
|
||
toolManager,
|
||
layerName = null,
|
||
undoable,
|
||
} = {}) {
|
||
if (!layerManager || !fabricImage) {
|
||
console.error("图层管理器或图片对象无效");
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
// 使用新的复合命令
|
||
const createImageLayerCmd = new CreateImageLayerCommand({
|
||
layerManager,
|
||
fabricImage,
|
||
toolManager,
|
||
layerName,
|
||
});
|
||
|
||
// 设置命令的撤销状态
|
||
if (isBoolean(undoable)) createImageLayerCmd.undoable = undoable; // 是否撤销
|
||
|
||
// 执行复合命令
|
||
const newLayerId = await layerManager.commandManager.execute(createImageLayerCmd);
|
||
|
||
return newLayerId;
|
||
} catch (error) {
|
||
console.error("创建图片图层失败:", error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更改固定图层的图像
|
||
* @param {Object} options - 配置选项
|
||
* @param {Object} options.layerManager - 图层管理器
|
||
* @param {string} options.fixedLayerId - 固定图层ID
|
||
* @param {Object} options.fabricImage - 新的图像对象
|
||
* @returns {Promise<boolean>} 是否成功更改
|
||
*/
|
||
export async function changeFixedImage({ layerManager, fixedLayerId, fabricImage } = {}) {
|
||
if (!layerManager || !fixedLayerId || !fabricImage) {
|
||
console.error("更改固定图层图像:参数无效");
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
// 创建更改固定图层图像命令
|
||
const changeFixedImageCmd = new ChangeFixedImageCommand({
|
||
canvas: layerManager.canvas,
|
||
layers: layerManager.layers,
|
||
fixedLayerId,
|
||
newImage: fabricImage,
|
||
layerManager,
|
||
});
|
||
|
||
// 通过命令管理器执行
|
||
const result = await layerManager.commandManager.execute(changeFixedImageCmd);
|
||
|
||
if (result) {
|
||
console.log(`✅ 成功更改固定图层 "${fixedLayerId}" 的图像`);
|
||
}
|
||
|
||
return result;
|
||
} catch (error) {
|
||
console.error("更改固定图层图像失败:", error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 添加图片到指定图层或创建新图层
|
||
* @param {Object} options - 配置选项
|
||
* @param {Object} options.layerManager - 图层管理器
|
||
* @param {Object} options.toolManager - 工具管理器
|
||
* @param {Object} options.fabricImage - 图像对象
|
||
* @param {string} options.targetLayerId - 目标图层ID(可选,未指定则创建新图层)
|
||
* @param {string} options.layerName - 图层名称(用于新建图层)
|
||
* @returns {Promise<string>} 图层ID
|
||
*/
|
||
export async function addImageToLayer({
|
||
layerManager,
|
||
toolManager,
|
||
fabricImage,
|
||
targetLayerId = null,
|
||
layerName = null,
|
||
} = {}) {
|
||
if (!layerManager || !fabricImage) {
|
||
console.error("添加图片到图层:参数无效");
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
// 创建添加图片到图层命令
|
||
const addImageToLayerCmd = new AddImageToLayerCommand({
|
||
canvas: layerManager.canvas,
|
||
layers: layerManager.layers,
|
||
layerManager,
|
||
toolManager,
|
||
fabricImage,
|
||
targetLayerId,
|
||
layerName,
|
||
activeLayerId: layerManager.activeLayerId,
|
||
});
|
||
|
||
// 通过命令管理器执行
|
||
const resultLayerId = await layerManager.commandManager.execute(addImageToLayerCmd);
|
||
|
||
if (resultLayerId) {
|
||
if (targetLayerId) {
|
||
console.log(`✅ 成功添加图片到现有图层 "${targetLayerId}"`);
|
||
} else {
|
||
console.log(`✅ 成功创建新图层 "${resultLayerId}" 并添加图片`);
|
||
}
|
||
}
|
||
|
||
return resultLayerId;
|
||
} catch (error) {
|
||
console.error("添加图片到图层失败:", error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从base64或者url加载图片并创建图层
|
||
* @param {str} imageUrl - base64字符串或图片URL
|
||
* @param {Object} layerManager - 图层管理器
|
||
* @param {Object} canvas - fabric.js画布实例
|
||
* @param {Object} options - 配置选项
|
||
* @returns {Promise<string>} 新图层ID的Promise
|
||
*/
|
||
export function loadImageUrlToLayer({ imageUrl, layerManager, canvas, toolManager }, options = {}) {
|
||
return new Promise(async (resolve, reject) => {
|
||
if (!imageUrl || !layerManager || !canvas) {
|
||
reject(new Error("参数无效"));
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 查找背景图层以获取尺寸
|
||
const bgLayer = layerManager.layers.value.find((layer) => layer.isBackground);
|
||
|
||
// 设置最大宽高为背景图层的尺寸
|
||
const maxWidth = bgLayer?.canvasWidth || canvas.width;
|
||
const maxHeight = bgLayer?.canvasHeight || canvas.height;
|
||
|
||
// 加载并处理图片
|
||
const fabricImage = await loadImage(imageUrl, {
|
||
maxWidth: maxWidth * 0.8, // 默认图片最大宽度为背景宽度的80%
|
||
maxHeight: maxHeight * 0.8, // 默认图片最大高度为背景高度的80%
|
||
canvasWidth: canvas.width,
|
||
canvasHeight: canvas.height,
|
||
...options,
|
||
});
|
||
|
||
if (options.imageMode) {
|
||
imageModeHandler({
|
||
imageMode: options.imageMode,
|
||
newImage: fabricImage,
|
||
canvasWidth: maxWidth,
|
||
canvasHeight: maxHeight,
|
||
});
|
||
|
||
// 默认居中
|
||
fabricImage.set({
|
||
originX: "center",
|
||
originY: "center",
|
||
left: canvas.width / 2,
|
||
top: canvas.height / 2,
|
||
});
|
||
}
|
||
|
||
// 创建图片图层
|
||
const layerId = await createImageLayer({
|
||
layerManager,
|
||
fabricImage,
|
||
toolManager,
|
||
...options,
|
||
});
|
||
|
||
resolve(layerId);
|
||
} catch (error) {
|
||
console.error("处理图片失败:", error);
|
||
reject(error);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 从File对象加载图片并创建图层
|
||
* @param {File} file - 文件对象
|
||
* @param {Object} layerManager - 图层管理器
|
||
* @param {Object} canvas - fabric.js画布实例
|
||
* @param {Object} options - 配置选项
|
||
* @returns {Promise<string>} 新图层ID的Promise
|
||
*/
|
||
export function uploadImageAndCreateLayer(
|
||
{ file, layerManager, canvas, toolManager },
|
||
options = {}
|
||
) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!file || !layerManager || !canvas) {
|
||
reject(new Error("参数无效"));
|
||
return;
|
||
}
|
||
|
||
const reader = new FileReader();
|
||
|
||
reader.onload = async (e) => {
|
||
try {
|
||
// 查找背景图层以获取尺寸
|
||
const bgLayer = layerManager.layers.value.find((layer) => layer.isBackground);
|
||
|
||
// 设置最大宽高为背景图层的尺寸
|
||
const maxWidth = bgLayer?.canvasWidth || canvas.width;
|
||
const maxHeight = bgLayer?.canvasHeight || canvas.height;
|
||
|
||
// 加载并处理图片
|
||
const fabricImage = await loadImage(e.target.result, {
|
||
maxWidth: maxWidth * 0.8, // 默认图片最大宽度为背景宽度的80%
|
||
maxHeight: maxHeight * 0.8, // 默认图片最大高度为背景高度的80%
|
||
canvasWidth: canvas.width,
|
||
canvasHeight: canvas.height,
|
||
...options,
|
||
});
|
||
|
||
// 创建图片图层
|
||
const layerId = await createImageLayer({
|
||
layerManager,
|
||
fabricImage,
|
||
toolManager,
|
||
layerName: file.name,
|
||
});
|
||
|
||
resolve(layerId);
|
||
} catch (error) {
|
||
console.error("处理图片失败:", error);
|
||
reject(error);
|
||
}
|
||
};
|
||
|
||
reader.onerror = (error) => {
|
||
console.error("读取文件失败:", error);
|
||
reject(error);
|
||
};
|
||
|
||
reader.readAsDataURL(file);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 安全加载图片
|
||
* 添加错误处理和重试机制
|
||
* @param {string} imageSource - 图片URL或Base64字符串
|
||
* @param {Object} options - 配置选项
|
||
* @returns {Promise<Object>} - 返回图片对象的Promise
|
||
*/
|
||
export function safeLoadImage(imageSource, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
let retries = options.retries || 1;
|
||
|
||
const attemptLoad = (attempt = 0) => {
|
||
loadImage(imageSource, options)
|
||
.then(resolve)
|
||
.catch((error) => {
|
||
if (attempt < retries) {
|
||
console.warn(`图片加载失败,正在重试 (${attempt + 1}/${retries})...`);
|
||
setTimeout(() => attemptLoad(attempt + 1), 500);
|
||
} else {
|
||
reject(error);
|
||
}
|
||
});
|
||
};
|
||
|
||
attemptLoad();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 从URL加载图片并更改固定图层
|
||
* @param {Object} options - 配置选项
|
||
* @param {string} options.imageUrl - 图片URL
|
||
* @param {Object} options.layerManager - 图层管理器
|
||
* @param {string} options.fixedLayerId - 固定图层ID
|
||
* @param {Object} options.imageOptions - 图片加载选项
|
||
* @returns {Promise<boolean>} 是否成功
|
||
*/
|
||
export function loadImageAndChangeFixedLayer({
|
||
imageUrl,
|
||
layerManager,
|
||
fixedLayerId,
|
||
imageOptions = {},
|
||
}) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!imageUrl || !layerManager || !fixedLayerId) {
|
||
reject(new Error("参数无效"));
|
||
return;
|
||
}
|
||
|
||
loadImage(imageUrl, imageOptions)
|
||
.then(async (fabricImage) => {
|
||
try {
|
||
const result = await changeFixedImage({
|
||
layerManager,
|
||
fixedLayerId,
|
||
fabricImage,
|
||
});
|
||
resolve(result);
|
||
} catch (error) {
|
||
console.error("更改固定图层失败:", error);
|
||
reject(error);
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
console.error("加载图片失败:", error);
|
||
reject(error);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 从File对象更改固定图层图像
|
||
* @param {Object} options - 配置选项
|
||
* @param {File} options.file - 图像文件对象
|
||
* @param {Object} options.layerManager - 图层管理器
|
||
* @param {string} options.layerId - 固定图层ID
|
||
* @param {Object} options.imageOptions - 图片加载选项
|
||
* @returns {Promise<string>} 新图像对象ID的Promise
|
||
*/
|
||
export function uploadImageAndChangeFixedLayer({ file, layerManager, layerId, imageOptions = {} }) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!file || !layerManager || !layerId) {
|
||
reject(new Error("参数无效:需要文件、图层管理器和图层ID"));
|
||
return;
|
||
}
|
||
|
||
// 验证文件类型
|
||
if (!file.type.startsWith("image/")) {
|
||
reject(new Error("无效的文件类型:必须是图像文件"));
|
||
return;
|
||
}
|
||
|
||
const reader = new FileReader();
|
||
|
||
reader.onload = async (e) => {
|
||
try {
|
||
// 查找目标固定图层以获取尺寸信息
|
||
const targetLayer = layerManager.layers.value.find((layer) => layer.id === layerId);
|
||
|
||
if (!targetLayer) {
|
||
throw new Error(`找不到图层 ID: ${layerId}`);
|
||
}
|
||
|
||
// 验证是否为固定图层
|
||
if (!targetLayer.isFixed && !targetLayer.isBackground) {
|
||
throw new Error("只能更改固定图层或背景图层的图像");
|
||
}
|
||
|
||
// 查找背景图层以获取画布尺寸
|
||
const bgLayer = layerManager.layers.value.find((layer) => layer.isBackground);
|
||
|
||
const maxWidth = bgLayer?.canvasWidth || layerManager.canvas.width;
|
||
const maxHeight = bgLayer?.canvasHeight || layerManager.canvas.height;
|
||
|
||
// 加载并处理图片
|
||
const fabricImage = await loadImage(e.target.result, {
|
||
maxWidth: maxWidth,
|
||
maxHeight: maxHeight,
|
||
canvasWidth: layerManager.canvas.width,
|
||
canvasHeight: layerManager.canvas.height,
|
||
centerOnCanvas: true,
|
||
...imageOptions,
|
||
});
|
||
|
||
// 创建更改固定图层图像命令
|
||
const changeFixedImageCmd = new ChangeFixedImageCommand({
|
||
canvas: layerManager.canvas,
|
||
layers: layerManager.layers,
|
||
layerId: layerId,
|
||
newImageFile: file,
|
||
layerManager: layerManager,
|
||
});
|
||
|
||
// 通过命令管理器执行
|
||
const newImageId = await layerManager.commandManager.execute(changeFixedImageCmd);
|
||
|
||
if (newImageId) {
|
||
console.log(`✅ 成功更改固定图层 "${targetLayer.name}" 的图像,新图像ID: ${newImageId}`);
|
||
resolve(newImageId);
|
||
} else {
|
||
throw new Error("更改固定图层图像失败");
|
||
}
|
||
} catch (error) {
|
||
console.error("处理图片失败:", error);
|
||
reject(error);
|
||
}
|
||
};
|
||
|
||
reader.onerror = (error) => {
|
||
console.error("读取文件失败:", error);
|
||
reject(new Error("文件读取失败"));
|
||
};
|
||
|
||
reader.readAsDataURL(file);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 从File对象加载图片并添加到指定图层 (简化版)
|
||
* @param {Object} options - 配置选项
|
||
* @param {File} options.file - 文件对象
|
||
* @param {Object} options.layerManager - 图层管理器
|
||
* @param {Object} options.toolManager - 工具管理器
|
||
* @param {string} options.targetLayerId - 目标图层ID(可选)
|
||
* @param {Object} options.imageOptions - 图片加载选项
|
||
* @returns {Promise<Object>} 返回 { layerId, imageId, wasLayerCreated } 的Promise
|
||
*/
|
||
export function uploadImageAndAddToLayer({
|
||
file,
|
||
layerManager,
|
||
toolManager,
|
||
targetLayerId = null,
|
||
imageOptions = {},
|
||
}) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!file || !layerManager) {
|
||
reject(new Error("参数无效:需要文件和图层管理器"));
|
||
return;
|
||
}
|
||
|
||
// 验证文件类型
|
||
if (!file.type.startsWith("image/")) {
|
||
reject(new Error("无效的文件类型:必须是图像文件"));
|
||
return;
|
||
}
|
||
|
||
// 创建添加图像到图层命令
|
||
const addImageToLayerCmd = new AddImageToLayerCommand({
|
||
canvas: layerManager.canvas,
|
||
layers: layerManager.layers,
|
||
activeLayerId: layerManager.activeLayerId,
|
||
imageFile: file,
|
||
targetLayerId: targetLayerId,
|
||
layerManager: layerManager,
|
||
toolManager: toolManager,
|
||
});
|
||
|
||
// 通过命令管理器执行
|
||
layerManager.commandManager
|
||
.execute(addImageToLayerCmd)
|
||
.then((result) => {
|
||
if (result) {
|
||
console.log(`✅ 成功添加图像到图层,结果:`, result);
|
||
resolve(result);
|
||
} else {
|
||
throw new Error("添加图像到图层失败");
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
console.error("添加图像到图层失败:", error);
|
||
reject(error);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 从File对象加载图片并添加到指定图层 (简化版)
|
||
* @param {Object} options - 配置选项
|
||
* @param {File} options.file - 文件对象
|
||
* @param {Object} options.layerManager - 图层管理器
|
||
* @param {Object} options.toolManager - 工具管理器
|
||
* @param {string} options.targetLayerId - 目标图层ID(可选)
|
||
* @param {Object} options.imageOptions - 图片加载选项
|
||
* @returns {Promise<Object>} 返回 { layerId, imageId, wasLayerCreated } 的Promise
|
||
*/
|
||
export function uploadImageAndAddToLayerSimple({
|
||
file,
|
||
layerManager,
|
||
toolManager,
|
||
targetLayerId = null,
|
||
imageOptions = {},
|
||
}) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!file || !layerManager) {
|
||
reject(new Error("参数无效:需要文件和图层管理器"));
|
||
return;
|
||
}
|
||
|
||
// 验证文件类型
|
||
if (!file.type.startsWith("image/")) {
|
||
reject(new Error("无效的文件类型:必须是图像文件"));
|
||
return;
|
||
}
|
||
|
||
// 创建添加图像到图层命令
|
||
const addImageToLayerCmd = new AddImageToLayerCommand({
|
||
canvas: layerManager.canvas,
|
||
layers: layerManager.layers,
|
||
activeLayerId: layerManager.activeLayerId,
|
||
imageFile: file,
|
||
targetLayerId: targetLayerId,
|
||
layerManager: layerManager,
|
||
toolManager: toolManager,
|
||
});
|
||
|
||
// 通过命令管理器执行
|
||
layerManager.commandManager
|
||
.execute(addImageToLayerCmd)
|
||
.then((result) => {
|
||
if (result) {
|
||
console.log(`✅ 成功添加图像到图层,结果:`, result);
|
||
resolve(result);
|
||
} else {
|
||
throw new Error("添加图像到图层失败");
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
console.error("添加图像到图层失败:", error);
|
||
reject(error);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 批量上传图片并创建图层
|
||
* @param {Object} options - 配置选项
|
||
* @param {FileList|Array<File>} options.files - 文件列表
|
||
* @param {Object} options.layerManager - 图层管理器
|
||
* @param {Object} options.canvas - fabric.js画布实例
|
||
* @param {Object} options.toolManager - 工具管理器
|
||
* @param {Object} options.imageOptions - 图片加载选项
|
||
* @param {function} options.onProgress - 进度回调函数
|
||
* @returns {Promise<Array<string>>} 新图层ID数组的Promise
|
||
*/
|
||
export async function batchUploadImagesAndCreateLayers({
|
||
files,
|
||
layerManager,
|
||
canvas,
|
||
toolManager,
|
||
imageOptions = {},
|
||
onProgress = null,
|
||
}) {
|
||
if (!files || files.length === 0) {
|
||
throw new Error("没有提供文件");
|
||
}
|
||
|
||
if (!layerManager || !canvas) {
|
||
throw new Error("缺少必要的参数:图层管理器或画布");
|
||
}
|
||
|
||
const results = [];
|
||
const errors = [];
|
||
|
||
for (let i = 0; i < files.length; i++) {
|
||
const file = files[i];
|
||
|
||
try {
|
||
// 调用进度回调
|
||
if (typeof onProgress === "function") {
|
||
onProgress({
|
||
current: i + 1,
|
||
total: files.length,
|
||
fileName: file.name,
|
||
status: "processing",
|
||
});
|
||
}
|
||
|
||
// 验证文件类型
|
||
if (!file.type.startsWith("image/")) {
|
||
console.warn(`跳过非图像文件: ${file.name}`);
|
||
continue;
|
||
}
|
||
|
||
// 上传图片并创建图层
|
||
const layerId = await uploadImageAndCreateLayer(
|
||
{ file, layerManager, canvas, toolManager },
|
||
imageOptions
|
||
);
|
||
|
||
results.push({
|
||
fileName: file.name,
|
||
layerId: layerId,
|
||
success: true,
|
||
});
|
||
|
||
// 调用进度回调
|
||
if (typeof onProgress === "function") {
|
||
onProgress({
|
||
current: i + 1,
|
||
total: files.length,
|
||
fileName: file.name,
|
||
status: "success",
|
||
layerId: layerId,
|
||
});
|
||
}
|
||
|
||
console.log(`✅ 成功处理文件: ${file.name}, 图层ID: ${layerId}`);
|
||
} catch (error) {
|
||
console.error(`❌ 处理文件失败: ${file.name}`, error);
|
||
|
||
errors.push({
|
||
fileName: file.name,
|
||
error: error.message,
|
||
success: false,
|
||
});
|
||
|
||
// 调用进度回调
|
||
if (typeof onProgress === "function") {
|
||
onProgress({
|
||
current: i + 1,
|
||
total: files.length,
|
||
fileName: file.name,
|
||
status: "error",
|
||
error: error.message,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// 输出批量处理结果
|
||
console.log(`📊 批量处理完成:`);
|
||
console.log(` ✅ 成功: ${results.length} 个文件`);
|
||
console.log(` ❌ 失败: ${errors.length} 个文件`);
|
||
|
||
if (errors.length > 0) {
|
||
console.warn("失败的文件:", errors);
|
||
}
|
||
|
||
return {
|
||
results: results,
|
||
errors: errors,
|
||
successCount: results.length,
|
||
errorCount: errors.length,
|
||
total: files.length,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 高级图像管理工具
|
||
* 提供批量图像处理、缓存、预加载等高级功能
|
||
*/
|
||
export class AdvancedImageManager {
|
||
constructor(canvasManager) {
|
||
this.canvasManager = canvasManager;
|
||
this.canvas = canvasManager.canvas;
|
||
this.layerManager = canvasManager.layerManager;
|
||
|
||
// 图像缓存
|
||
this.imageCache = new Map();
|
||
this.preloadQueue = [];
|
||
this.maxCacheSize = 50; // 最大缓存数量
|
||
|
||
// 批量操作状态
|
||
this.batchOperations = [];
|
||
this.isBatchMode = false;
|
||
|
||
// 性能监控
|
||
this.performanceMetrics = {
|
||
imageLoads: 0,
|
||
cacheHits: 0,
|
||
totalLoadTime: 0,
|
||
averageLoadTime: 0,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 预加载图像列表
|
||
* @param {Array} imageUrls 要预加载的图像URL数组
|
||
* @param {Object} options 选项
|
||
*/
|
||
async preloadImages(imageUrls, options = {}) {
|
||
const {
|
||
concurrency = 3, // 并发数量
|
||
timeout = 10000,
|
||
onProgress = null,
|
||
onError = null,
|
||
} = options;
|
||
|
||
const loadPromises = [];
|
||
const results = [];
|
||
let completed = 0;
|
||
|
||
// 分批并发加载
|
||
for (let i = 0; i < imageUrls.length; i += concurrency) {
|
||
const batch = imageUrls.slice(i, i + concurrency);
|
||
|
||
const batchPromises = batch.map(async (url, index) => {
|
||
try {
|
||
const startTime = performance.now();
|
||
const image = await this.loadAndCacheImage(url, { timeout });
|
||
const loadTime = performance.now() - startTime;
|
||
|
||
// 更新性能指标
|
||
this.updatePerformanceMetrics(loadTime);
|
||
|
||
completed++;
|
||
onProgress?.({ completed, total: imageUrls.length, url });
|
||
|
||
return { success: true, url, image, loadTime };
|
||
} catch (error) {
|
||
completed++;
|
||
onError?.({ url, error, completed, total: imageUrls.length });
|
||
return { success: false, url, error: error.message };
|
||
}
|
||
});
|
||
|
||
const batchResults = await Promise.all(batchPromises);
|
||
results.push(...batchResults);
|
||
}
|
||
|
||
return {
|
||
results,
|
||
summary: {
|
||
total: imageUrls.length,
|
||
successful: results.filter((r) => r.success).length,
|
||
failed: results.filter((r) => !r.success).length,
|
||
cacheHitRate: this.performanceMetrics.cacheHits / this.performanceMetrics.imageLoads,
|
||
averageLoadTime: this.performanceMetrics.averageLoadTime,
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 加载并缓存图像
|
||
* @param {String} url 图像URL
|
||
* @param {Object} options 选项
|
||
*/
|
||
async loadAndCacheImage(url, options = {}) {
|
||
// 检查缓存
|
||
if (this.imageCache.has(url)) {
|
||
this.performanceMetrics.cacheHits++;
|
||
return this.imageCache.get(url);
|
||
}
|
||
|
||
// 加载新图像
|
||
const image = await this.loadImage(url, options);
|
||
|
||
// 添加到缓存
|
||
this.addToCache(url, image);
|
||
|
||
return image;
|
||
}
|
||
|
||
/**
|
||
* 开始批量操作模式
|
||
*/
|
||
startBatch() {
|
||
this.isBatchMode = true;
|
||
this.batchOperations = [];
|
||
}
|
||
|
||
/**
|
||
* 批量更换多个固定图层的图像
|
||
* @param {Array} operations 操作数组 [{layerType, imageUrl, options}, ...]
|
||
*/
|
||
async batchChangeFixedImages(operations) {
|
||
const operationResults = [];
|
||
|
||
if (this.isBatchMode) {
|
||
// 如果在批量模式下,只收集操作
|
||
this.batchOperations.push(
|
||
...operations.map((op) => ({
|
||
type: "changeFixed",
|
||
...op,
|
||
}))
|
||
);
|
||
return { queued: operations.length };
|
||
}
|
||
|
||
// 立即执行模式
|
||
for (const operation of operations) {
|
||
try {
|
||
const result = await this.canvasManager.changeFixedImage(operation.imageUrl, {
|
||
targetLayerType: operation.layerType,
|
||
...operation.options,
|
||
});
|
||
operationResults.push({ success: true, ...result, operation });
|
||
} catch (error) {
|
||
operationResults.push({
|
||
success: false,
|
||
error: error.message,
|
||
operation,
|
||
});
|
||
}
|
||
}
|
||
|
||
return {
|
||
results: operationResults,
|
||
summary: this.getSummary(operationResults),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 批量向多个图层添加图像
|
||
* @param {Array} operations 操作数组 [{layerId, imageUrl, position, options}, ...]
|
||
*/
|
||
async batchAddImagesToLayers(operations) {
|
||
const operationResults = [];
|
||
|
||
if (this.isBatchMode) {
|
||
this.batchOperations.push(
|
||
...operations.map((op) => ({
|
||
type: "addToLayer",
|
||
...op,
|
||
}))
|
||
);
|
||
return { queued: operations.length };
|
||
}
|
||
|
||
// 并发执行以提高性能
|
||
const concurrentOperations = operations.map(async (operation) => {
|
||
try {
|
||
const result = await this.canvasManager.addImageToLayer(
|
||
operation.imageUrl,
|
||
operation.layerId,
|
||
{
|
||
position: operation.position,
|
||
...operation.options,
|
||
}
|
||
);
|
||
return { success: true, ...result, operation };
|
||
} catch (error) {
|
||
return {
|
||
success: false,
|
||
error: error.message,
|
||
operation,
|
||
};
|
||
}
|
||
});
|
||
|
||
const concurrentResults = await Promise.all(concurrentOperations);
|
||
return {
|
||
results: concurrentResults,
|
||
summary: this.getSummary(concurrentResults),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 执行批量操作
|
||
*/
|
||
async executeBatch() {
|
||
if (!this.isBatchMode || this.batchOperations.length === 0) {
|
||
return { message: "No batch operations to execute" };
|
||
}
|
||
|
||
const results = [];
|
||
const startTime = performance.now();
|
||
|
||
// 按类型分组操作以优化执行
|
||
const groupedOps = this.groupOperationsByType(this.batchOperations);
|
||
|
||
// 执行分组操作
|
||
for (const [type, ops] of Object.entries(groupedOps)) {
|
||
try {
|
||
let typeResults;
|
||
|
||
switch (type) {
|
||
case "changeFixed":
|
||
typeResults = await this.batchChangeFixedImages(ops);
|
||
break;
|
||
case "addToLayer":
|
||
typeResults = await this.batchAddImagesToLayers(ops);
|
||
break;
|
||
default:
|
||
console.warn(`Unknown operation type: ${type}`);
|
||
continue;
|
||
}
|
||
|
||
if (typeResults.results) {
|
||
results.push(...typeResults.results);
|
||
}
|
||
} catch (error) {
|
||
console.error(`Batch execution failed for type ${type}:`, error);
|
||
// 继续执行其他类型的操作
|
||
}
|
||
}
|
||
|
||
const executionTime = performance.now() - startTime;
|
||
|
||
// 清理批量状态
|
||
this.isBatchMode = false;
|
||
this.batchOperations = [];
|
||
|
||
return {
|
||
results,
|
||
summary: {
|
||
...this.getSummary(results),
|
||
executionTime,
|
||
operationsPerSecond: results.length / (executionTime / 1000),
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 创建图像替换模板
|
||
* @param {String} templateName 模板名称
|
||
* @param {Array} operations 操作定义
|
||
*/
|
||
createTemplate(templateName, operations) {
|
||
if (!this.templates) {
|
||
this.templates = new Map();
|
||
}
|
||
|
||
this.templates.set(templateName, {
|
||
name: templateName,
|
||
operations,
|
||
createdAt: new Date(),
|
||
usageCount: 0,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 应用模板
|
||
* @param {String} templateName 模板名称
|
||
* @param {Object} variables 变量替换映射
|
||
*/
|
||
async applyTemplate(templateName, variables = {}) {
|
||
const template = this.templates?.get(templateName);
|
||
if (!template) {
|
||
throw new Error(`Template "${templateName}" not found`);
|
||
}
|
||
|
||
// 替换模板中的变量
|
||
const operations = this.replaceTemplateVariables(template.operations, variables);
|
||
|
||
// 执行操作
|
||
const result = await this.batchAddImagesToLayers(operations);
|
||
|
||
// 更新使用计数
|
||
template.usageCount++;
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 智能图像优化
|
||
* @param {String} imageUrl 图像URL
|
||
* @param {Object} targetSpecs 目标规格 {width, height, quality}
|
||
*/
|
||
async optimizeImage(imageUrl, targetSpecs) {
|
||
const image = await this.loadAndCacheImage(imageUrl);
|
||
|
||
// 检查是否需要优化
|
||
const currentSpecs = {
|
||
width: image.width,
|
||
height: image.height,
|
||
};
|
||
|
||
if (this.shouldOptimize(currentSpecs, targetSpecs)) {
|
||
return this.performImageOptimization(image, targetSpecs);
|
||
}
|
||
|
||
return image;
|
||
}
|
||
|
||
/**
|
||
* 清理缓存
|
||
* @param {String} strategy 清理策略 'lru', 'size', 'all'
|
||
*/
|
||
clearCache(strategy = "lru") {
|
||
switch (strategy) {
|
||
case "all":
|
||
this.imageCache.clear();
|
||
break;
|
||
case "size":
|
||
if (this.imageCache.size > this.maxCacheSize) {
|
||
const excess = this.imageCache.size - this.maxCacheSize;
|
||
const keys = Array.from(this.imageCache.keys());
|
||
for (let i = 0; i < excess; i++) {
|
||
this.imageCache.delete(keys[i]);
|
||
}
|
||
}
|
||
break;
|
||
case "lru":
|
||
// 实现 LRU 清理逻辑
|
||
this.implementLRUCleanup();
|
||
break;
|
||
}
|
||
}
|
||
|
||
// === 私有方法 ===
|
||
|
||
loadImage(url, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
const timeout = setTimeout(() => {
|
||
reject(new Error(`Image load timeout: ${url}`));
|
||
}, options.timeout || 10000);
|
||
|
||
fabric.Image.fromURL(
|
||
url,
|
||
(img) => {
|
||
clearTimeout(timeout);
|
||
if (!img || !img.getElement()) {
|
||
reject(new Error("Invalid image"));
|
||
return;
|
||
}
|
||
resolve(img);
|
||
},
|
||
{ crossOrigin: "anonymous" }
|
||
);
|
||
});
|
||
}
|
||
|
||
addToCache(url, image) {
|
||
// 检查缓存大小限制
|
||
if (this.imageCache.size >= this.maxCacheSize) {
|
||
this.clearCache("size");
|
||
}
|
||
|
||
// 添加时间戳用于 LRU
|
||
const cacheEntry = {
|
||
image,
|
||
lastUsed: Date.now(),
|
||
usageCount: 1,
|
||
};
|
||
|
||
this.imageCache.set(url, cacheEntry);
|
||
}
|
||
|
||
updatePerformanceMetrics(loadTime) {
|
||
this.performanceMetrics.imageLoads++;
|
||
this.performanceMetrics.totalLoadTime += loadTime;
|
||
this.performanceMetrics.averageLoadTime =
|
||
this.performanceMetrics.totalLoadTime / this.performanceMetrics.imageLoads;
|
||
}
|
||
|
||
getSummary(results) {
|
||
return {
|
||
total: results.length,
|
||
successful: results.filter((r) => r.success).length,
|
||
failed: results.filter((r) => !r.success).length,
|
||
successRate: results.filter((r) => r.success).length / results.length,
|
||
};
|
||
}
|
||
|
||
groupOperationsByType(operations) {
|
||
return operations.reduce((groups, op) => {
|
||
const type = op.type;
|
||
if (!groups[type]) groups[type] = [];
|
||
groups[type].push(op);
|
||
return groups;
|
||
}, {});
|
||
}
|
||
|
||
replaceTemplateVariables(operations, variables) {
|
||
return operations.map((op) => {
|
||
const newOp = { ...op };
|
||
|
||
// 替换字符串中的变量 {{variable}}
|
||
Object.keys(newOp).forEach((key) => {
|
||
if (typeof newOp[key] === "string") {
|
||
newOp[key] = newOp[key].replace(/\{\{(\w+)\}\}/g, (match, varName) => {
|
||
return variables[varName] || match;
|
||
});
|
||
}
|
||
});
|
||
|
||
return newOp;
|
||
});
|
||
}
|
||
|
||
shouldOptimize(current, target) {
|
||
const sizeThreshold = 0.8; // 80% 的阈值
|
||
return (
|
||
current.width > target.width * (1 / sizeThreshold) ||
|
||
current.height > target.height * (1 / sizeThreshold)
|
||
);
|
||
}
|
||
|
||
performImageOptimization(image, targetSpecs) {
|
||
// 实现图像优化逻辑
|
||
// 这里可以集成图像压缩、尺寸调整等功能
|
||
return image; // 简化实现
|
||
}
|
||
|
||
implementLRUCleanup() {
|
||
if (this.imageCache.size <= this.maxCacheSize) return;
|
||
|
||
// 按最后使用时间排序,移除最久未使用的
|
||
const entries = Array.from(this.imageCache.entries()).sort(
|
||
(a, b) => a[1].lastUsed - b[1].lastUsed
|
||
);
|
||
|
||
const toRemove = entries.slice(0, this.imageCache.size - this.maxCacheSize);
|
||
toRemove.forEach(([key]) => this.imageCache.delete(key));
|
||
}
|
||
|
||
// 获取性能报告
|
||
getPerformanceReport() {
|
||
return {
|
||
...this.performanceMetrics,
|
||
cacheSize: this.imageCache.size,
|
||
maxCacheSize: this.maxCacheSize,
|
||
cacheUtilization: this.imageCache.size / this.maxCacheSize,
|
||
recommendations: this.generatePerformanceRecommendations(),
|
||
};
|
||
}
|
||
|
||
generatePerformanceRecommendations() {
|
||
const recommendations = [];
|
||
|
||
if (this.performanceMetrics.cacheHits / this.performanceMetrics.imageLoads < 0.3) {
|
||
recommendations.push("考虑增加缓存大小以提高缓存命中率");
|
||
}
|
||
|
||
if (this.performanceMetrics.averageLoadTime > 2000) {
|
||
recommendations.push("图像加载时间较长,考虑图像优化或CDN");
|
||
}
|
||
|
||
return recommendations;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 图像工具集
|
||
* 提供常用的图像处理和图层操作功能
|
||
*/
|
||
export const ImageUtils = {
|
||
// 基础图像加载
|
||
loadImage,
|
||
safeLoadImage,
|
||
|
||
// 图层操作
|
||
createImageLayer,
|
||
changeFixedImage,
|
||
addImageToLayer,
|
||
|
||
// 文件上传处理
|
||
uploadImageAndCreateLayer,
|
||
uploadImageAndChangeFixedLayer,
|
||
uploadImageAndAddToLayer,
|
||
uploadImageAndAddToLayerSimple,
|
||
batchUploadImagesAndCreateLayers,
|
||
|
||
// URL图像处理
|
||
loadImageAndChangeFixedLayer,
|
||
|
||
/**
|
||
* 快速创建图像图层 (别名)
|
||
* @param {File} file - 图像文件
|
||
* @param {Object} layerManager - 图层管理器
|
||
* @param {Object} canvas - 画布实例
|
||
* @param {Object} toolManager - 工具管理器
|
||
* @returns {Promise<string>} 图层ID
|
||
*/
|
||
quickCreateImageLayer: (file, layerManager, canvas, toolManager) => {
|
||
return uploadImageAndCreateLayer({
|
||
file,
|
||
layerManager,
|
||
canvas,
|
||
toolManager,
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 快速更改固定图层图像 (别名)
|
||
* @param {File} file - 图像文件
|
||
* @param {string} layerId - 图层ID
|
||
* @param {Object} layerManager - 图层管理器
|
||
* @returns {Promise<string>} 新图像ID
|
||
*/
|
||
quickChangeFixedImage: (file, layerId, layerManager) => {
|
||
return uploadImageAndChangeFixedLayer({ file, layerId, layerManager });
|
||
},
|
||
|
||
/**
|
||
* 快速添加图像到图层 (别名)
|
||
* @param {File} file - 图像文件
|
||
* @param {Object} layerManager - 图层管理器
|
||
* @param {Object} toolManager - 工具管理器
|
||
* @param {string} targetLayerId - 目标图层ID (可选)
|
||
* @returns {Promise<Object>} 执行结果
|
||
*/
|
||
quickAddImageToLayer: (file, layerManager, toolManager, targetLayerId = null) => {
|
||
return uploadImageAndAddToLayerSimple({
|
||
file,
|
||
layerManager,
|
||
toolManager,
|
||
targetLayerId,
|
||
});
|
||
},
|
||
};
|
||
|
||
/**
|
||
* 栅格化画布对象为图像
|
||
* 参考fabric.brushes.js中的convertToImg方法,考虑画布变换参数
|
||
* @param {Object} options - 配置选项
|
||
* @param {fabric.Canvas} options.canvas - fabric画布实例
|
||
* @param {Array} options.objects - 要栅格化的对象数组
|
||
* @param {Object} options.bounds - 边界框 {left, top, width, height} (可选)
|
||
* @param {boolean} options.trimWhitespace - 是否裁剪空白区域,默认true
|
||
* @param {number} options.trimPadding - 裁剪时保留的空白边距,默认10像素
|
||
* @param {number} options.quality - 图像质量 0-1,默认1
|
||
* @param {string} options.format - 图像格式 'png'|'jpeg',默认'png'
|
||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||
*/
|
||
export function rasterizeCanvasObjects(options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
const {
|
||
canvas,
|
||
objects = [],
|
||
bounds = null,
|
||
trimWhitespace = true,
|
||
trimPadding = 10,
|
||
quality = 1,
|
||
format = "png",
|
||
} = options;
|
||
|
||
if (!canvas || !Array.isArray(objects)) {
|
||
reject(new Error("无效的参数:需要画布实例和对象数组"));
|
||
return;
|
||
}
|
||
|
||
if (objects.length === 0) {
|
||
reject(new Error("没有对象可栅格化"));
|
||
return;
|
||
}
|
||
|
||
// 使用改进的栅格化方法
|
||
_rasterizeUsingCanvasCopy(canvas, objects, {
|
||
trimWhitespace,
|
||
trimPadding,
|
||
quality,
|
||
format,
|
||
})
|
||
.then(resolve)
|
||
.catch(reject);
|
||
} catch (error) {
|
||
console.error("栅格化对象失败:", error);
|
||
reject(error);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 使用画布复制方式进行栅格化(参考convertToImg实现)
|
||
* @param {fabric.Canvas} canvas - fabric画布实例
|
||
* @param {Array} objects - 要栅格化的对象数组
|
||
* @param {Object} options - 配置选项
|
||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||
* @private
|
||
*/
|
||
function _rasterizeUsingCanvasCopy(canvas, objects, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
const { trimWhitespace = true, trimPadding = 10, quality = 1, format = "png" } = options;
|
||
|
||
// 保存原始状态
|
||
const originalObjects = canvas.getObjects();
|
||
const originalViewportTransform = [...canvas.viewportTransform];
|
||
const originalZoom = canvas.getZoom();
|
||
|
||
// 临时隐藏其他对象,只显示要栅格化的对象
|
||
const objectsToHide = originalObjects.filter((obj) => !objects.includes(obj));
|
||
|
||
// 隐藏不需要的对象
|
||
objectsToHide.forEach((obj) => {
|
||
obj._originalVisible = obj.visible;
|
||
obj.set("visible", false);
|
||
});
|
||
|
||
// 确保要栅格化的对象可见
|
||
objects.forEach((obj) => {
|
||
obj._originalVisible = obj.visible;
|
||
obj.set("visible", true);
|
||
});
|
||
|
||
// 重新渲染画布以应用可见性变化
|
||
canvas.renderAll();
|
||
|
||
// 等待一帧确保渲染完成
|
||
requestAnimationFrame(() => {
|
||
try {
|
||
// 获取画布的像素比例
|
||
const pixelRatio = canvas.getRetinaScaling();
|
||
|
||
// 复制画布元素(这会保持所有变换状态)
|
||
const copiedCanvas = fabric.util.copyCanvasElement(canvas.lowerCanvasEl);
|
||
|
||
let finalCanvas = copiedCanvas;
|
||
let trimOffset = { x: 0, y: 0 };
|
||
|
||
// 裁剪空白区域(如果需要,支持padding)
|
||
if (trimWhitespace) {
|
||
const trimResult = _trimCanvas(copiedCanvas, trimPadding);
|
||
if (trimResult) {
|
||
finalCanvas = trimResult.canvas;
|
||
trimOffset = { x: trimResult.offset.x, y: trimResult.offset.y };
|
||
}
|
||
}
|
||
|
||
// 创建fabric图像对象
|
||
const fabricImage = new fabric.Image(finalCanvas);
|
||
|
||
if (!fabricImage) {
|
||
throw new Error("创建fabric图像失败");
|
||
}
|
||
|
||
// 获取画布变换参数
|
||
const pointerX = canvas.viewportTransform[4];
|
||
const pointerY = canvas.viewportTransform[5];
|
||
const zoom = canvas.getZoom();
|
||
|
||
// 计算最终位置(参考convertToImg的实现)
|
||
const finalLeft = (trimOffset.x / pixelRatio - pointerX) / zoom;
|
||
const finalTop = (trimOffset.y / pixelRatio - pointerY) / zoom;
|
||
const finalScaleX = 1 / pixelRatio / zoom;
|
||
const finalScaleY = 1 / pixelRatio / zoom;
|
||
|
||
// 设置图像属性
|
||
fabricImage.set({
|
||
id: generateId("rasterized_image_"),
|
||
left: finalLeft,
|
||
top: finalTop,
|
||
scaleX: finalScaleX,
|
||
scaleY: finalScaleY,
|
||
selectable: true,
|
||
hasControls: true,
|
||
hasBorders: true,
|
||
custom: {
|
||
type: "rasterized",
|
||
originalObjects: objects.map((obj) => obj.id).filter(Boolean),
|
||
rasterizedAt: new Date().toISOString(),
|
||
trimPadding: trimPadding,
|
||
},
|
||
});
|
||
|
||
fabricImage.setCoords();
|
||
|
||
// 恢复对象的原始可见性
|
||
_restoreObjectVisibility(originalObjects);
|
||
|
||
// 重新渲染画布
|
||
canvas.renderAll();
|
||
|
||
resolve(fabricImage);
|
||
} catch (error) {
|
||
// 确保恢复对象状态
|
||
_restoreObjectVisibility(originalObjects);
|
||
canvas.renderAll();
|
||
reject(error);
|
||
}
|
||
});
|
||
} catch (error) {
|
||
reject(error);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 恢复对象的原始可见性状态
|
||
* @param {Array} objects - 对象数组
|
||
* @private
|
||
*/
|
||
function _restoreObjectVisibility(objects) {
|
||
objects.forEach((obj) => {
|
||
if (Object.prototype.hasOwnProperty.call(obj, "_originalVisible")) {
|
||
obj.set("visible", obj._originalVisible);
|
||
delete obj._originalVisible;
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 备用栅格化方法:使用toDataURL方式
|
||
* 当画布复制方法不可用时的备选方案
|
||
* @param {fabric.Canvas} canvas - fabric画布实例
|
||
* @param {Array} objects - 要栅格化的对象数组
|
||
* @param {Object} options - 配置选项
|
||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||
* @private
|
||
*/
|
||
function _rasterizeUsingDataURL(canvas, objects, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
const { quality = 1, format = "png" } = options;
|
||
|
||
// 保存原始状态
|
||
const originalObjects = canvas.getObjects();
|
||
|
||
// 临时移除其他对象
|
||
const objectsToRemove = originalObjects.filter((obj) => !objects.includes(obj));
|
||
objectsToRemove.forEach((obj) => {
|
||
canvas.remove(obj);
|
||
});
|
||
|
||
// 重新渲染画布
|
||
canvas.renderAll();
|
||
|
||
// 获取画布数据URL
|
||
const dataUrl = canvas.toDataURL({
|
||
format: format,
|
||
quality: quality,
|
||
multiplier: canvas.getRetinaScaling(),
|
||
});
|
||
|
||
// 恢复原始对象
|
||
objectsToRemove.forEach((obj) => {
|
||
canvas.add(obj);
|
||
});
|
||
|
||
// 恢复原始渲染顺序
|
||
canvas._objects = [...originalObjects];
|
||
canvas.renderAll();
|
||
|
||
// 创建fabric图像
|
||
fabric.Image.fromURL(
|
||
dataUrl,
|
||
(fabricImage) => {
|
||
if (!fabricImage) {
|
||
reject(new Error("创建fabric图像失败"));
|
||
return;
|
||
}
|
||
|
||
fabricImage.set({
|
||
id: generateId("rasterized_image_"),
|
||
left: 0,
|
||
top: 0,
|
||
selectable: true,
|
||
hasControls: true,
|
||
hasBorders: true,
|
||
custom: {
|
||
type: "rasterized",
|
||
originalObjects: objects.map((obj) => obj.id).filter(Boolean),
|
||
rasterizedAt: new Date().toISOString(),
|
||
},
|
||
});
|
||
|
||
fabricImage.setCoords();
|
||
resolve(fabricImage);
|
||
},
|
||
{ crossOrigin: "anonymous" }
|
||
);
|
||
} catch (error) {
|
||
reject(error);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 栅格化画布对象为图像(兼容版本)
|
||
* 自动选择最适合的栅格化方法
|
||
* @param {Object} options - 配置选项
|
||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||
*/
|
||
export function rasterizeCanvasObjectsCompat(options = {}) {
|
||
const { canvas, objects = [] } = options;
|
||
|
||
// 检测是否支持copyCanvasElement
|
||
if (fabric.util.copyCanvasElement && canvas.lowerCanvasEl) {
|
||
// 使用画布复制方法(推荐)
|
||
return rasterizeCanvasObjects(options);
|
||
} else {
|
||
// 使用备用方法
|
||
console.warn("使用备用栅格化方法:toDataURL");
|
||
return _rasterizeUsingDataURL(canvas, objects, options);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 高级栅格化方法:支持更多选项和优化
|
||
* @param {Object} options - 配置选项
|
||
* @param {fabric.Canvas} options.canvas - fabric画布实例
|
||
* @param {Array} options.objects - 要栅格化的对象数组
|
||
* @param {Object} options.bounds - 边界框 {left, top, width, height} (可选)
|
||
* @param {boolean} options.trimWhitespace - 是否裁剪空白区域,默认true
|
||
* @param {number} options.quality - 图像质量 0-1,默认1
|
||
* @param {string} options.format - 图像格式 'png'|'jpeg',默认'png'
|
||
* @param {boolean} options.preserveObjectState - 是否保持对象状态,默认true
|
||
* @param {number} options.multiplier - 输出倍数,默认使用画布的retina缩放
|
||
* @param {boolean} options.useBackgroundColor - 是否使用画布背景色,默认false
|
||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||
*/
|
||
export function rasterizeCanvasObjectsAdvanced(options = {}) {
|
||
return new Promise(async (resolve, reject) => {
|
||
try {
|
||
const {
|
||
canvas,
|
||
objects = [],
|
||
bounds = null,
|
||
trimWhitespace = true,
|
||
quality = 1,
|
||
format = "png",
|
||
preserveObjectState = true,
|
||
multiplier = null,
|
||
useBackgroundColor = false,
|
||
} = options;
|
||
|
||
if (!canvas || !Array.isArray(objects)) {
|
||
reject(new Error("无效的参数:需要画布实例和对象数组"));
|
||
return;
|
||
}
|
||
|
||
if (objects.length === 0) {
|
||
reject(new Error("没有对象可栅格化"));
|
||
return;
|
||
}
|
||
|
||
// 检测画布状态
|
||
const hasTransform =
|
||
canvas.getZoom() !== 1 ||
|
||
canvas.viewportTransform[4] !== 0 ||
|
||
canvas.viewportTransform[5] !== 0;
|
||
|
||
let result;
|
||
|
||
if (hasTransform && fabric.util.copyCanvasElement) {
|
||
// 有变换时使用画布复制方法
|
||
console.log("🎯 检测到画布变换,使用画布复制方法");
|
||
result = await _rasterizeUsingCanvasCopy(canvas, objects, {
|
||
trimWhitespace,
|
||
quality,
|
||
format,
|
||
});
|
||
} else {
|
||
// 无变换时可以使用更灵活的方法
|
||
console.log("📐 画布无变换,使用标准栅格化方法");
|
||
result = await _rasterizeUsingDataURL(canvas, objects, {
|
||
quality,
|
||
format,
|
||
});
|
||
}
|
||
|
||
resolve(result);
|
||
} catch (error) {
|
||
console.error("高级栅格化失败:", error);
|
||
reject(error);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 计算多个对象的边界框
|
||
* @param {Array} objects - 对象数组
|
||
* @returns {Object} 边界框 {left, top, width, height}
|
||
* @private
|
||
*/
|
||
function _calculateObjectsBounds(objects) {
|
||
if (!objects || objects.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
let minX = Infinity;
|
||
let minY = Infinity;
|
||
let maxX = -Infinity;
|
||
let maxY = -Infinity;
|
||
|
||
objects.forEach((obj) => {
|
||
if (!obj || typeof obj.getBoundingRect !== "function") {
|
||
return;
|
||
}
|
||
|
||
const bounds = obj.getBoundingRect();
|
||
minX = Math.min(minX, bounds.left);
|
||
minY = Math.min(minY, bounds.top);
|
||
maxX = Math.max(maxX, bounds.left + bounds.width);
|
||
maxY = Math.max(maxY, bounds.top + bounds.height);
|
||
});
|
||
|
||
if (minX === Infinity || minY === Infinity) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
left: minX,
|
||
top: minY,
|
||
width: maxX - minX,
|
||
height: maxY - minY,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 裁剪画布空白区域(支持保留边距)
|
||
* 参考fabric.util.trimCanvas方法,添加padding支持
|
||
* @param {HTMLCanvasElement} canvas - 要裁剪的画布
|
||
* @param {number} padding - 保留的边距像素,默认0
|
||
* @returns {Object|null} 裁剪结果 {canvas: 新画布, offset: {x, y}}
|
||
* @private
|
||
*/
|
||
function _trimCanvas(canvas, padding = 0) {
|
||
try {
|
||
const ctx = canvas.getContext("2d");
|
||
const w = canvas.width;
|
||
const h = canvas.height;
|
||
const imageData = ctx.getImageData(0, 0, w, h);
|
||
const pixels = imageData.data;
|
||
|
||
let minX = w;
|
||
let minY = h;
|
||
let maxX = 0;
|
||
let maxY = 0;
|
||
let hasContent = false;
|
||
|
||
// 扫描像素找到有内容的区域
|
||
for (let y = 0; y < h; y++) {
|
||
for (let x = 0; x < w; x++) {
|
||
const alpha = pixels[(y * w + x) * 4 + 3];
|
||
if (alpha > 0) {
|
||
hasContent = true;
|
||
minX = Math.min(minX, x);
|
||
minY = Math.min(minY, y);
|
||
maxX = Math.max(maxX, x);
|
||
maxY = Math.max(maxY, y);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!hasContent) {
|
||
return null;
|
||
}
|
||
|
||
// 应用padding,确保不超出原始画布边界
|
||
const paddedMinX = Math.max(0, minX - padding);
|
||
const paddedMinY = Math.max(0, minY - padding);
|
||
const paddedMaxX = Math.min(w - 1, maxX + padding);
|
||
const paddedMaxY = Math.min(h - 1, maxY + padding);
|
||
|
||
const trimWidth = paddedMaxX - paddedMinX + 1;
|
||
const trimHeight = paddedMaxY - paddedMinY + 1;
|
||
|
||
// 创建裁剪后的画布
|
||
const trimmedCanvas = document.createElement("canvas");
|
||
const trimmedCtx = trimmedCanvas.getContext("2d");
|
||
|
||
trimmedCanvas.width = trimWidth;
|
||
trimmedCanvas.height = trimHeight;
|
||
|
||
// 复制裁剪区域(包含padding)
|
||
const trimmedImageData = ctx.getImageData(paddedMinX, paddedMinY, trimWidth, trimHeight);
|
||
trimmedCtx.putImageData(trimmedImageData, 0, 0);
|
||
|
||
return {
|
||
canvas: trimmedCanvas,
|
||
offset: { x: paddedMinX, y: paddedMinY },
|
||
};
|
||
} catch (error) {
|
||
console.error("裁剪画布失败:", error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 栅格化图层对象(简化版接口)
|
||
* @param {Object} options - 配置选项
|
||
* @param {fabric.Canvas} options.canvas - fabric画布实例
|
||
* @param {Object} options.layer - 图层对象
|
||
* @param {boolean} options.includeChildren - 是否包含子图层,默认true
|
||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||
*/
|
||
export function rasterizeLayer(options = {}) {
|
||
const { canvas, layer, includeChildren = true } = options;
|
||
|
||
if (!canvas || !layer) {
|
||
return Promise.reject(new Error("缺少必要参数:画布或图层"));
|
||
}
|
||
|
||
// 收集图层的所有对象
|
||
const objects = [];
|
||
|
||
if (layer.fabricObjects && Array.isArray(layer.fabricObjects)) {
|
||
objects.push(...layer.fabricObjects.filter(Boolean));
|
||
}
|
||
|
||
// 如果包含子图层
|
||
if (includeChildren && layer.children && Array.isArray(layer.children)) {
|
||
const collectChildObjects = (childLayer) => {
|
||
if (childLayer.fabricObjects && Array.isArray(childLayer.fabricObjects)) {
|
||
objects.push(...childLayer.fabricObjects.filter(Boolean));
|
||
}
|
||
if (childLayer.children && Array.isArray(childLayer.children)) {
|
||
childLayer.children.forEach(collectChildObjects);
|
||
}
|
||
};
|
||
|
||
layer.children.forEach(collectChildObjects);
|
||
}
|
||
|
||
if (objects.length === 0) {
|
||
return Promise.reject(new Error("图层没有可栅格化的对象"));
|
||
}
|
||
|
||
// 调用通用栅格化方法
|
||
return rasterizeCanvasObjects({
|
||
canvas,
|
||
objects,
|
||
trimWhitespace: true,
|
||
quality: 1,
|
||
format: "png",
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 批量栅格化多个图层
|
||
* @param {Object} options - 配置选项
|
||
* @param {fabric.Canvas} options.canvas - fabric画布实例
|
||
* @param {Array} options.layers - 图层数组
|
||
* @param {function} options.onProgress - 进度回调
|
||
* @returns {Promise<Array>} 栅格化结果数组
|
||
*/
|
||
export async function batchRasterizeLayers(options = {}) {
|
||
const { canvas, layers = [], onProgress = null } = options;
|
||
|
||
if (!canvas || !Array.isArray(layers)) {
|
||
throw new Error("缺少必要参数:画布或图层数组");
|
||
}
|
||
|
||
const results = [];
|
||
const total = layers.length;
|
||
|
||
for (let i = 0; i < layers.length; i++) {
|
||
const layer = layers[i];
|
||
|
||
try {
|
||
onProgress?.({ current: i + 1, total, layer, status: "processing" });
|
||
|
||
const rasterizedImage = await rasterizeLayer({
|
||
canvas,
|
||
layer,
|
||
includeChildren: true,
|
||
});
|
||
|
||
results.push({
|
||
success: true,
|
||
layer,
|
||
image: rasterizedImage,
|
||
layerId: layer.id,
|
||
});
|
||
|
||
onProgress?.({ current: i + 1, total, layer, status: "success" });
|
||
} catch (error) {
|
||
console.error(`栅格化图层失败: ${layer.name || layer.id}`, error);
|
||
|
||
results.push({
|
||
success: false,
|
||
layer,
|
||
error: error.message,
|
||
layerId: layer.id,
|
||
});
|
||
|
||
onProgress?.({
|
||
current: i + 1,
|
||
total,
|
||
layer,
|
||
status: "error",
|
||
error: error.message,
|
||
});
|
||
}
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* 智能栅格化:根据对象类型和画布状态自动选择最佳方法
|
||
* @param {Object} options - 配置选项
|
||
* @param {fabric.Canvas} options.canvas - fabric画布实例
|
||
* @param {Array} options.objects - 要栅格化的对象数组
|
||
* @param {Object} options.strategy - 策略配置
|
||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||
*/
|
||
export function smartRasterize(options = {}) {
|
||
const { canvas, objects = [], strategy = {} } = options;
|
||
|
||
// 分析对象和画布状态
|
||
const analysis = _analyzeRasterizationContext(canvas, objects);
|
||
|
||
// 选择最佳策略
|
||
const selectedStrategy = _selectOptimalStrategy(analysis, strategy);
|
||
|
||
console.log(`🧠 智能栅格化策略: ${selectedStrategy.method}`, {
|
||
reason: selectedStrategy.reason,
|
||
analysis: analysis,
|
||
});
|
||
|
||
// 执行对应的栅格化方法
|
||
switch (selectedStrategy.method) {
|
||
case "canvasCopy":
|
||
return rasterizeCanvasObjects({
|
||
canvas,
|
||
objects,
|
||
...selectedStrategy.options,
|
||
});
|
||
|
||
case "dataURL":
|
||
return _rasterizeUsingDataURL(canvas, objects, selectedStrategy.options);
|
||
|
||
case "advanced":
|
||
return rasterizeCanvasObjectsAdvanced({
|
||
canvas,
|
||
objects,
|
||
...selectedStrategy.options,
|
||
});
|
||
|
||
default:
|
||
return rasterizeCanvasObjects({ canvas, objects, ...options });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 分析栅格化上下文
|
||
* @param {fabric.Canvas} canvas - 画布实例
|
||
* @param {Array} objects - 对象数组
|
||
* @returns {Object} 分析结果
|
||
* @private
|
||
*/
|
||
function _analyzeRasterizationContext(canvas, objects) {
|
||
const zoom = canvas.getZoom();
|
||
const viewportTransform = canvas.viewportTransform;
|
||
const hasTransform = zoom !== 1 || viewportTransform[4] !== 0 || viewportTransform[5] !== 0;
|
||
|
||
// 分析对象类型分布
|
||
const objectTypes = objects.reduce((acc, obj) => {
|
||
const type = obj.type || "unknown";
|
||
acc[type] = (acc[type] || 0) + 1;
|
||
return acc;
|
||
}, {});
|
||
|
||
// 估算复杂度
|
||
const complexity = _estimateRenderingComplexity(objects);
|
||
|
||
// 计算画布利用率
|
||
const canvasArea = canvas.width * canvas.height;
|
||
const objectsBounds = _calculateObjectsBounds(objects);
|
||
const objectsArea = objectsBounds ? objectsBounds.width * objectsBounds.height : 0;
|
||
const utilization = objectsArea / canvasArea;
|
||
|
||
return {
|
||
hasTransform,
|
||
zoom,
|
||
objectCount: objects.length,
|
||
objectTypes,
|
||
complexity,
|
||
utilization,
|
||
canvasSize: { width: canvas.width, height: canvas.height },
|
||
objectsBounds,
|
||
supportsCanvasCopy: !!(fabric.util.copyCanvasElement && canvas.lowerCanvasEl),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 选择最优策略
|
||
* @param {Object} analysis - 分析结果
|
||
* @param {Object} userStrategy - 用户指定策略
|
||
* @returns {Object} 选择的策略
|
||
* @private
|
||
*/
|
||
function _selectOptimalStrategy(analysis, userStrategy = {}) {
|
||
// 用户指定策略优先
|
||
if (userStrategy.force) {
|
||
return {
|
||
method: userStrategy.force,
|
||
reason: "用户强制指定",
|
||
options: userStrategy.options || {},
|
||
};
|
||
}
|
||
|
||
// 画布变换场景
|
||
if (analysis.hasTransform && analysis.supportsCanvasCopy) {
|
||
return {
|
||
method: "canvasCopy",
|
||
reason: "画布有变换,使用画布复制方法保持变换状态",
|
||
options: { trimWhitespace: true },
|
||
};
|
||
}
|
||
|
||
// 高复杂度场景
|
||
if (analysis.complexity > 0.8) {
|
||
return {
|
||
method: "advanced",
|
||
reason: "高复杂度渲染,使用高级栅格化方法",
|
||
options: {
|
||
preserveObjectState: true,
|
||
useBackgroundColor: analysis.utilization > 0.5,
|
||
},
|
||
};
|
||
}
|
||
|
||
// 大量对象场景
|
||
if (analysis.objectCount > 50) {
|
||
return {
|
||
method: "dataURL",
|
||
reason: "大量对象,使用dataURL方法优化性能",
|
||
options: { quality: 0.9 },
|
||
};
|
||
}
|
||
|
||
// 低利用率场景(空白较多)
|
||
if (analysis.utilization < 0.1) {
|
||
return {
|
||
method: "canvasCopy",
|
||
reason: "空白区域较多,使用画布复制+裁剪优化",
|
||
options: { trimWhitespace: true },
|
||
};
|
||
}
|
||
|
||
// 默认策略
|
||
return {
|
||
method: "canvasCopy",
|
||
reason: "标准场景,使用画布复制方法",
|
||
options: { trimWhitespace: true },
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 估算渲染复杂度
|
||
* @param {Array} objects - 对象数组
|
||
* @returns {number} 复杂度分数 0-1
|
||
* @private
|
||
*/
|
||
function _estimateRenderingComplexity(objects) {
|
||
let complexity = 0;
|
||
let totalWeight = 0;
|
||
|
||
objects.forEach((obj) => {
|
||
let objectComplexity = 0;
|
||
let weight = 1;
|
||
|
||
// 基于对象类型的复杂度
|
||
switch (obj.type) {
|
||
case "path":
|
||
objectComplexity = 0.8;
|
||
weight = 2;
|
||
break;
|
||
case "group":
|
||
objectComplexity = 0.7;
|
||
weight = obj.getObjects?.()?.length || 3;
|
||
break;
|
||
case "text":
|
||
case "i-text":
|
||
case "textbox":
|
||
objectComplexity = 0.6;
|
||
weight = (obj.text?.length || 10) / 50;
|
||
break;
|
||
case "image":
|
||
objectComplexity = 0.4;
|
||
break;
|
||
case "rect":
|
||
case "circle":
|
||
case "ellipse":
|
||
objectComplexity = 0.2;
|
||
break;
|
||
default:
|
||
objectComplexity = 0.3;
|
||
}
|
||
|
||
// 考虑变换复杂度
|
||
if (obj.angle && obj.angle !== 0) objectComplexity += 0.1;
|
||
if (obj.scaleX !== 1 || obj.scaleY !== 1) objectComplexity += 0.1;
|
||
if (obj.skewX || obj.skewY) objectComplexity += 0.2;
|
||
|
||
// 考虑样式复杂度
|
||
if (obj.shadow) objectComplexity += 0.2;
|
||
if (obj.stroke) objectComplexity += 0.1;
|
||
if (obj.strokeDashArray?.length) objectComplexity += 0.1;
|
||
|
||
complexity += objectComplexity * weight;
|
||
totalWeight += weight;
|
||
});
|
||
|
||
return totalWeight > 0 ? Math.min(complexity / totalWeight, 1) : 0;
|
||
}
|
||
|
||
/**
|
||
* 栅格化工具集合
|
||
* 提供不同场景下的栅格化方法选择
|
||
*/
|
||
export const RasterizeUtils = {
|
||
// 基础栅格化
|
||
rasterizeCanvasObjects,
|
||
rasterizeLayer,
|
||
batchRasterizeLayers,
|
||
|
||
// 智能栅格化
|
||
smartRasterize,
|
||
|
||
// 兼容性栅格化
|
||
rasterizeCanvasObjectsCompat,
|
||
rasterizeCanvasObjectsAdvanced,
|
||
|
||
// 策略栅格化
|
||
fastRasterize: (canvas, objects) => {
|
||
return _rasterizeUsingDataURL(canvas, objects, { quality: 0.8 });
|
||
},
|
||
|
||
highQualityRasterize: (canvas, objects) => {
|
||
return rasterizeCanvasObjectsAdvanced({
|
||
canvas,
|
||
objects,
|
||
quality: 1,
|
||
trimWhitespace: true,
|
||
preserveObjectState: true,
|
||
});
|
||
},
|
||
|
||
compactRasterize: (canvas, objects) => {
|
||
return rasterizeCanvasObjects({
|
||
canvas,
|
||
objects,
|
||
trimWhitespace: true,
|
||
format: "jpeg",
|
||
quality: 0.9,
|
||
});
|
||
},
|
||
|
||
// 分析工具
|
||
analyzeRasterizationContext: _analyzeRasterizationContext,
|
||
estimateComplexity: _estimateRenderingComplexity,
|
||
calculateObjectsBounds: _calculateObjectsBounds,
|
||
|
||
/**
|
||
* 获取推荐的栅格化方法
|
||
* @param {fabric.Canvas} canvas - 画布实例
|
||
* @param {Array} objects - 对象数组
|
||
* @returns {Object} 推荐结果
|
||
*/
|
||
getRecommendation: (canvas, objects) => {
|
||
const analysis = _analyzeRasterizationContext(canvas, objects);
|
||
const strategy = _selectOptimalStrategy(analysis);
|
||
|
||
return {
|
||
recommendedMethod: strategy.method,
|
||
reason: strategy.reason,
|
||
analysis: analysis,
|
||
alternatives: _getAlternativeMethods(analysis),
|
||
};
|
||
},
|
||
};
|
||
|
||
/**
|
||
* 获取备选方法
|
||
* @param {Object} analysis - 分析结果
|
||
* @returns {Array} 备选方法列表
|
||
* @private
|
||
*/
|
||
function _getAlternativeMethods(analysis) {
|
||
const alternatives = [];
|
||
|
||
if (analysis.supportsCanvasCopy) {
|
||
alternatives.push({
|
||
method: "canvasCopy",
|
||
pros: ["保持变换状态", "高质量输出", "自动裁剪"],
|
||
cons: ["可能较慢"],
|
||
suitable: "有画布变换或需要高质量输出",
|
||
});
|
||
}
|
||
|
||
alternatives.push({
|
||
method: "dataURL",
|
||
pros: ["性能较好", "兼容性强", "处理大量对象"],
|
||
cons: ["不保持变换", "可能有质量损失"],
|
||
suitable: "大量对象或性能优先",
|
||
});
|
||
|
||
alternatives.push({
|
||
method: "advanced",
|
||
pros: ["智能优化", "完整功能", "自适应策略"],
|
||
cons: ["复杂度较高"],
|
||
suitable: "复杂场景或需要最佳效果",
|
||
});
|
||
|
||
return alternatives;
|
||
}
|
||
|
||
/** * 图像模式处理函数
|
||
* 根据不同的图像模式调整图像大小和位置
|
||
* @param {Object} params - 参数对象
|
||
* @param {string} params.imageMode - 图像模式
|
||
* @param {fabric.Image} params.newImage - 新图像对象
|
||
* @param {number} params.canvasWidth - 画布宽度
|
||
* @param {number} params.canvasHeight - 画布高度
|
||
*/
|
||
export const imageModeHandler = ({ imageMode, newImage, canvasWidth, canvasHeight }) => {
|
||
switch (imageMode) {
|
||
case "stretch":
|
||
// 拉伸模式 - 填充整个画布
|
||
newImage.scaleToWidth(canvasWidth);
|
||
newImage.scaleToHeight(canvasHeight);
|
||
break;
|
||
case "tile":
|
||
// 平铺模式 - 保持原始大小
|
||
newImage.scaleX = 1;
|
||
newImage.scaleY = 1;
|
||
break;
|
||
case "stretchTile":
|
||
// 拉伸平铺模式 - 填充整个画布,但保持宽高比
|
||
newImage.scaleToWidth(canvasWidth);
|
||
newImage.scaleToHeight(canvasHeight);
|
||
break;
|
||
case "stretchTileCrop":
|
||
// 拉伸平铺并裁剪模式 - 填充整个画布,可能
|
||
// 会裁剪图像以适应画布
|
||
newImage.scaleToWidth(canvasWidth);
|
||
newImage.scaleToHeight(canvasHeight);
|
||
// 这里可以添加裁剪逻辑,如果需要的话
|
||
// 例如使用fabric.Image.clipPath来裁剪图像
|
||
break;
|
||
case "contains": {
|
||
// 图片缩放后要保证最长边能完全显示在画布内 // 既要考虑画布的宽高比,也要考虑图像的宽高比 // 包含模式 - 保证图像在画布内完整显示
|
||
const canvasAspect = canvasWidth / canvasHeight;
|
||
const imageAspect = newImage.width / newImage.height;
|
||
// 保证图像在画布内完整显示 - 既要考虑画布的宽高比,也要考虑图像的宽高比
|
||
// 图片缩放后要保证最长边能完全显示在画布内
|
||
if (imageAspect > canvasAspect) {
|
||
// 图像更宽
|
||
newImage.scaleToWidth(canvasWidth);
|
||
} else {
|
||
// 图像更高
|
||
newImage.scaleToHeight(canvasHeight);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* 调整图像大小
|
||
* @param {string} base64 - 原始base64字符串
|
||
* @param {number} width - 目标宽度
|
||
* @param {number} height - 目标高度
|
||
* @returns {Promise<string>} 处理后的base64字符串
|
||
*/
|
||
export const resizeImage = async (base64, width, height) => {
|
||
return new Promise((resolve, reject) => {
|
||
const img = new Image();
|
||
img.src = base64;
|
||
img.onload = () => {
|
||
const canvas = document.createElement("canvas");
|
||
canvas.width = width;
|
||
canvas.height = height;
|
||
const ctx = canvas.getContext("2d");
|
||
ctx.drawImage(img, 0, 0, width, height);
|
||
resolve(canvas.toDataURL());
|
||
};
|
||
img.onerror = reject;
|
||
});
|
||
};
|