Files
aida_front/src/component/Canvas/CanvasEditor/managers/ExportManager.js

953 lines
28 KiB
JavaScript
Raw Normal View History

2025-06-18 11:05:23 +08:00
import { fabric } from "fabric-with-all";
import { findObjectById } from "../utils/helper";
import { createRasterizedImage } from "../utils/selectionToImage";
2025-06-18 11:05:23 +08:00
2025-06-09 10:25:54 +08:00
/**
* 图片导出管理器
* 负责处理画布的图片导出功能支持多种导出选项和图层过滤
*/
export class ExportManager {
constructor(canvasManager, layerManager) {
this.canvasManager = canvasManager;
this.layerManager = layerManager;
this.canvas = canvasManager.canvas;
}
/**
* 导出图片
* @param {Object} options 导出选项
* @param {Boolean} options.isContainBg 是否包含背景图层
* @param {Boolean} options.isContainFixed 是否包含固定图层
* @param {String} options.layerId 导出具体图层ID
* @param {Array} options.layerIdArray 导出多个图层ID数组
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
2025-06-18 11:05:23 +08:00
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
2025-06-09 10:25:54 +08:00
* @returns {String} 导出的图片数据URL
*/
exportImage(options = {}) {
const {
isContainBg = false,
isContainFixed = false,
isCropByBg = false, // 是否使用背景大小裁剪
2025-06-09 10:25:54 +08:00
layerId = "",
layerIdArray = [],
2025-06-18 11:05:23 +08:00
expPicType = "png",
restoreOpacityInRedGreen = true,
2025-06-09 10:25:54 +08:00
} = options;
try {
2025-06-18 11:05:23 +08:00
// 检查是否为红绿图模式
const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false;
2025-06-09 10:25:54 +08:00
// 如果指定了具体图层ID导出指定图层
if (layerId) {
2025-06-18 11:05:23 +08:00
return this._exportSpecificLayer(
layerId,
expPicType,
isRedGreenMode,
restoreOpacityInRedGreen,
isCropByBg
2025-06-18 11:05:23 +08:00
);
2025-06-09 10:25:54 +08:00
}
// 如果指定了多个图层ID导出多个图层
if (layerIdArray && layerIdArray.length > 0) {
2025-06-18 11:05:23 +08:00
return this._exportMultipleLayers(
layerIdArray,
expPicType,
isContainFixed,
isRedGreenMode,
restoreOpacityInRedGreen,
isContainBg,
isCropByBg
2025-06-18 11:05:23 +08:00
);
2025-06-09 10:25:54 +08:00
}
// 默认导出所有可见图层
2025-06-18 11:05:23 +08:00
return this._exportAllLayers(
expPicType,
isContainBg,
isContainFixed,
isRedGreenMode,
restoreOpacityInRedGreen,
isCropByBg
2025-06-18 11:05:23 +08:00
);
2025-06-09 10:25:54 +08:00
} catch (error) {
console.error("导出图片失败:", error);
throw new Error(`图片导出失败: ${error.message}`);
}
}
/**
* 导出指定单个图层
* @param {String} layerId 图层ID
* @param {String} expPicType 导出类型
2025-06-18 11:05:23 +08:00
* @param {Boolean} isRedGreenMode 是否为红绿图模式
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
2025-06-09 10:25:54 +08:00
* @returns {String} 图片数据URL
* @private
*/
_exportSpecificLayer(layerId, expPicType, isRedGreenMode, restoreOpacityInRedGreen) {
2025-06-09 10:25:54 +08:00
if (!this.layerManager) {
throw new Error("图层管理器未初始化");
}
const layer = this._getLayerById(layerId);
if (!layer) {
throw new Error(`未找到ID为 ${layerId} 的图层`);
}
if (!layer.visible) {
console.warn(`图层 ${layer.name} 不可见,将导出空白图片`);
}
2025-06-18 11:05:23 +08:00
// 收集所有需要导出的对象
const objectsToExport = this._collectObjectsFromLayer(layer);
2025-06-09 10:25:54 +08:00
2025-06-18 11:05:23 +08:00
if (objectsToExport.length === 0) {
console.warn(`图层 ${layer.name} 没有可导出的对象`);
return this._generateEmptyImage(expPicType);
}
2025-06-09 10:25:54 +08:00
2025-06-18 11:05:23 +08:00
// 红绿图模式下使用固定尺寸和裁剪
if (isRedGreenMode) {
return this._exportWithRedGreenMode(objectsToExport, expPicType, restoreOpacityInRedGreen);
2025-06-09 10:25:54 +08:00
}
2025-06-18 11:05:23 +08:00
// 普通模式使用画布尺寸
return this._exportWithCanvasSize(objectsToExport, expPicType, restoreOpacityInRedGreen);
2025-06-09 10:25:54 +08:00
}
/**
* 导出多个指定图层
* @param {Array} layerIdArray 图层ID数组
* @param {String} expPicType 导出类型
* @param {Boolean} isContainBg 是否包含背景图层
* @param {Boolean} isContainFixed 是否包含固定图层
2025-06-18 11:05:23 +08:00
* @param {Boolean} isRedGreenMode 是否为红绿图模式
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
2025-06-09 10:25:54 +08:00
* @returns {String} 图片数据URL
* @private
*/
2025-06-18 11:05:23 +08:00
_exportMultipleLayers(
layerIdArray,
expPicType,
isContainBg,
isContainFixed,
isRedGreenMode,
restoreOpacityInRedGreen,
isCropByBg
2025-06-18 11:05:23 +08:00
) {
2025-06-09 10:25:54 +08:00
if (!this.layerManager) {
throw new Error("图层管理器未初始化");
}
2025-06-18 11:05:23 +08:00
// 按图层顺序收集对象(从底到顶)
const objectsToExport = this._collectObjectsByLayerOrder(
layerIdArray,
isContainBg,
isContainFixed
);
2025-06-09 10:25:54 +08:00
2025-06-18 11:05:23 +08:00
if (objectsToExport.length === 0) {
console.warn("没有可导出的对象");
return this._generateEmptyImage(expPicType);
}
2025-06-09 10:25:54 +08:00
2025-06-18 11:05:23 +08:00
// 红绿图模式下使用固定尺寸和裁剪
if (isRedGreenMode) {
return this._exportWithRedGreenMode(objectsToExport, expPicType, restoreOpacityInRedGreen);
2025-06-09 10:25:54 +08:00
}
2025-06-18 11:05:23 +08:00
// 普通模式使用画布尺寸
return this._exportWithCanvasSize(objectsToExport, expPicType, restoreOpacityInRedGreen);
2025-06-09 10:25:54 +08:00
}
/**
* 导出所有图层
* @param {String} expPicType 导出类型
* @param {Boolean} isContainBg 是否包含背景图层
* @param {Boolean} isContainFixed 是否包含固定图层
2025-06-18 11:05:23 +08:00
* @param {Boolean} isRedGreenMode 是否为红绿图模式
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
2025-06-09 10:25:54 +08:00
* @returns {String} 图片数据URL
* @private
*/
2025-06-18 11:05:23 +08:00
_exportAllLayers(
expPicType,
isContainBg,
isContainFixed,
isRedGreenMode,
restoreOpacityInRedGreen,
isCropByBg
2025-06-18 11:05:23 +08:00
) {
// 按图层顺序收集对象(从底到顶)
const objectsToExport = this._collectObjectsByLayerOrder(
null, // 导出所有图层
isContainBg,
isContainFixed
);
if (objectsToExport.length === 0) {
console.warn("没有可导出的对象");
return this._generateEmptyImage(expPicType);
}
2025-06-09 10:25:54 +08:00
2025-06-18 11:05:23 +08:00
// 红绿图模式下使用固定尺寸和裁剪
if (isRedGreenMode) {
return this._exportWithRedGreenMode(objectsToExport, expPicType, restoreOpacityInRedGreen);
2025-06-09 10:25:54 +08:00
}
let canvasClipPath = this.canvas.clipPath;
if (isCropByBg) {
const cropWidth =
this.canvasManager?.canvasWidth?.value || this.canvas?.canvasWidth || this.canvas.width;
const cropHeight =
this.canvasManager?.canvasHeight?.value || this.canvas?.canvasHeight || this.canvas.height;
canvasClipPath = new fabric.Rect({
left: this.canvas.width / 2,
top: this.canvas.height / 2,
width: cropWidth,
height: cropHeight,
originX: "center",
originY: "center",
fill: "#fff",
stroke: "transparent",
strokeWidth: 0,
});
canvasClipPath.set({
absolutePositioned: true,
});
canvasClipPath.setCoords();
}
2025-06-18 11:05:23 +08:00
// 普通模式使用画布尺寸
return this._exportWithCanvasSize(
objectsToExport,
expPicType,
2025-06-23 00:40:45 +08:00
restoreOpacityInRedGreen,
canvasClipPath
2025-06-18 11:05:23 +08:00
);
2025-06-09 10:25:54 +08:00
}
/**
2025-06-22 13:52:28 +08:00
* 从图层收集对象优化版本 - 通过ID查找画布中的真实对象
* @param {Object} layer 图层对象
* @returns {Array} 画布中的真实对象数组
* @private
*/
_collectObjectsFromLayer(layer) {
if (!layer) {
return [];
}
const realObjects = [];
// 收集当前图层的对象
if (layer.fabricObjects && layer.fabricObjects.length > 0) {
for (const layerObj of layer.fabricObjects) {
if (!layerObj || !layerObj.id) continue;
// 通过ID在画布中查找真实对象
const realObj = this._findRealObjectById(layerObj.id);
if (realObj && realObj.visible !== false) {
realObjects.push(realObj);
}
}
}
2025-06-23 00:40:45 +08:00
if (layer.fabricObject) {
// 通过ID在画布中查找真实对象
const realObj = this._findRealObjectById(layer.fabricObject.id);
if (realObj && realObj.visible !== false) {
realObjects.push(realObj);
}
}
2025-06-22 13:52:28 +08:00
// 递归收集子图层的对象
if (layer.children && layer.children.length > 0) {
for (let i = layer.children.length - 1; i >= 0; i--) {
const childLayer = layer.children[i];
2025-06-22 13:52:28 +08:00
const childObjects = this._collectObjectsFromLayer(childLayer);
realObjects.push(...childObjects);
}
}
return realObjects;
}
/**
* 通过ID在画布中查找真实对象
* @param {String} objectId 对象ID
* @returns {Object|null} 画布中的真实对象
* @private
*/
_findRealObjectById(objectId) {
if (!objectId || !this.canvas) {
return null;
}
try {
// 使用helper工具查找对象
const result = findObjectById(this.canvas, objectId);
return result?.object || null;
} catch (error) {
console.warn(`查找对象 ${objectId} 失败:`, error);
return null;
}
}
/**
* 导出对象
* @param {Object} obj fabric对象
* @param {String} expPicType 导出类型
* @param {Boolean} isRedGreenMode 是否为红绿图模式
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
* @returns {String} 图片数据URL
* @private
*/
async _exportObject(obj, expPicType, isRedGreenMode, restoreOpacityInRedGreen) {
2025-06-22 13:52:28 +08:00
// 红绿图模式下使用固定尺寸和裁剪
if (isRedGreenMode) {
return this._exportWithRedGreenMode([obj], expPicType, restoreOpacityInRedGreen);
2025-06-22 13:52:28 +08:00
}
// 普通模式使用画布尺寸
return this._exportWithCanvasSize([obj], expPicType, restoreOpacityInRedGreen);
2025-06-22 13:52:28 +08:00
}
/**
* 按图层顺序收集对象优化版本 - 从底到顶
2025-06-18 11:05:23 +08:00
* @param {Array|null} layerIdArray 图层ID数组null表示所有图层
2025-06-09 10:25:54 +08:00
* @param {Boolean} isContainBg 是否包含背景图层
* @param {Boolean} isContainFixed 是否包含固定图层
2025-06-22 13:52:28 +08:00
* @returns {Array} 按正确顺序排列的真实对象数组
2025-06-09 10:25:54 +08:00
* @private
*/
2025-06-18 11:05:23 +08:00
_collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed) {
const objectsToExport = [];
2025-06-22 13:52:28 +08:00
const allLayers = this._getAllLayersFlattened(); // 获取扁平化的图层列表
2025-06-09 10:25:54 +08:00
2025-06-18 11:05:23 +08:00
// 图层数组是从顶到底的顺序,需要反向遍历以获得从底到顶的渲染顺序
for (let i = allLayers.length - 1; i >= 0; i--) {
const layer = allLayers[i];
// 如果指定了图层ID数组只处理指定的图层
if (layerIdArray && !layerIdArray.includes(layer.id)) continue;
// 检查图层类型过滤条件
if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed)) continue;
2025-06-18 11:05:23 +08:00
if (layer.visible) {
const layerObjects = this._collectObjectsFromLayer(layer);
objectsToExport.push(...layerObjects);
}
2025-06-09 10:25:54 +08:00
}
2025-06-18 11:05:23 +08:00
return objectsToExport;
2025-06-09 10:25:54 +08:00
}
2025-06-22 13:52:28 +08:00
/**
* 获取扁平化的图层列表包含子图层
* @returns {Array} 扁平化的图层数组
* @private
*/
_getAllLayersFlattened() {
const flattenedLayers = [];
const rootLayers = this._getAllLayers();
const flattenLayer = (layer) => {
flattenedLayers.push(layer);
// 递归处理子图层
if (layer.children && layer.children.length > 0) {
for (const childLayer of layer.children) {
flattenLayer(childLayer);
}
}
};
// 处理所有根图层
for (const layer of rootLayers) {
flattenLayer(layer);
}
return flattenedLayers;
}
/**
* 计算对象组的边界
* @param {Array} objects 对象数组
* @returns {Object} 边界信息 {left, top, width, height}
* @private
*/
_calculateGroupBounds(objects) {
if (!objects || objects.length === 0) {
return { left: 0, top: 0, width: 1, height: 1 };
}
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 { left: 0, top: 0, width: 1, height: 1 };
}
// 添加小量边距避免边缘裁切
const padding = 2;
return {
left: minX - padding,
top: minY - padding,
width: maxX - minX + padding * 2,
height: maxY - minY + padding * 2,
};
}
/**
* 克隆对象并添加到临时画布调整位置偏移
* @param {fabric.Canvas} tempCanvas 临时画布
* @param {Object} obj 要克隆的对象
* @param {Object} bounds 边界信息
* @param {Boolean} isRedGreenMode 是否为红绿图模式
* @param {Boolean} restoreOpacityInRedGreen 是否恢复透明度
* @returns {Promise<Object>} 克隆的对象
* @private
*/
async _cloneAndAddObjectWithOffset(
tempCanvas,
obj,
bounds,
isRedGreenMode,
restoreOpacityInRedGreen
) {
try {
const cloned = await this._cloneObjectForExport(
obj,
isRedGreenMode && restoreOpacityInRedGreen
);
if (cloned) {
// 获取对象当前边界
const objBounds = obj.getBoundingRect();
// 计算相对于组边界的偏移
const offsetX = objBounds.left - bounds.left;
const offsetY = objBounds.top - bounds.top;
// 设置新位置(相对于临时画布的原点)
cloned.set({
left: offsetX + objBounds.width / 2,
top: offsetY + objBounds.height / 2,
originX: "center",
originY: "center",
});
cloned.setCoords();
tempCanvas.add(cloned);
return cloned;
}
} catch (error) {
console.warn(`克隆对象失败: ${obj?.id || "未知"}`, error);
}
return null;
}
2025-06-09 10:25:54 +08:00
/**
2025-06-18 11:05:23 +08:00
* 红绿图模式导出使用固定图层底图作为画布尺寸和裁剪区域
* @param {Array} objectsToExport 要导出的对象数组
* @param {String} expPicType 导出类型
* @param {Boolean} restoreOpacityInRedGreen 是否恢复透明度为1
* @returns {String} 图片数据URL
2025-06-09 10:25:54 +08:00
* @private
*/
async _exportWithRedGreenMode(objectsToExport, expPicType, restoreOpacityInRedGreen) {
2025-06-18 11:05:23 +08:00
// 获取固定图层对象(衣服底图)作为参考
const fixedLayerObject = this._getFixedLayerObject() ?? this.canvas.clipPath;
2025-06-18 11:05:23 +08:00
if (!fixedLayerObject) {
console.warn("红绿图模式下未找到固定图层对象,使用画布尺寸");
return this._exportWithCanvasSize(objectsToExport, expPicType, restoreOpacityInRedGreen);
2025-06-18 11:05:23 +08:00
}
// 获取固定图层对象的边界矩形(包含位置、尺寸、缩放等信息)
2025-06-22 13:52:28 +08:00
const fixedBounds = fixedLayerObject?.getBoundingRect?.();
2025-06-18 11:05:23 +08:00
// 使用固定图层的实际显示尺寸作为导出画布尺寸
const canvasWidth = Math.round(fixedBounds.width);
const canvasHeight = Math.round(fixedBounds.height);
console.log(`红绿图模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`);
console.log("固定图层边界:", fixedBounds);
// 创建固定尺寸的临时画布
const scaleFactor = 2; // 高清导出
2025-06-09 10:25:54 +08:00
const tempCanvas = document.createElement("canvas");
2025-06-18 11:05:23 +08:00
tempCanvas.width = canvasWidth * scaleFactor;
tempCanvas.height = canvasHeight * scaleFactor;
tempCanvas.style.width = canvasWidth + "px";
tempCanvas.style.height = canvasHeight + "px";
const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas, {
width: canvasWidth,
height: canvasHeight,
backgroundColor: null,
enableRetinaScaling: true,
imageSmoothingEnabled: true,
});
tempFabricCanvas.setZoom(1);
try {
2025-06-22 13:52:28 +08:00
// 获取裁剪路径对象(如果存在)
const clipPathObject = await this._getClipPathObject(fixedBounds);
2025-06-18 11:05:23 +08:00
// 克隆并添加所有对象到临时画布,需要调整位置相对于固定图层
for (let i = 0; i < objectsToExport.length; i++) {
const obj = objectsToExport[i];
const cloned = await this._cloneObjectForExport(obj, restoreOpacityInRedGreen && true);
2025-06-18 11:05:23 +08:00
if (cloned) {
// 调整对象位置:将原画布坐标转换为以固定图层为原点的相对坐标
cloned.set({
left: cloned.left - fixedBounds.left,
top: cloned.top - fixedBounds.top,
});
// 更新对象坐标
cloned.setCoords();
// 设置裁剪路径到对象
if (clipPathObject) {
cloned.clipPath = clipPathObject;
}
tempFabricCanvas.add(cloned);
}
}
// 渲染画布
tempFabricCanvas.renderAll();
// 生成图片
return this._generateHighQualityDataURL(tempCanvas, expPicType);
} finally {
this._cleanupTempCanvas(tempFabricCanvas);
}
2025-06-09 10:25:54 +08:00
}
/**
2025-06-18 11:05:23 +08:00
* 普通模式导出使用画布尺寸
* @param {Array} objectsToExport 要导出的对象数组
* @param {String} expPicType 导出类型
* @param {Boolean} restoreOpacityInRedGreen 是否恢复透明度为1
* @returns {String} 图片数据URL
2025-06-09 10:25:54 +08:00
* @private
*/
async _exportWithCanvasSize(objectsToExport, expPicType, restoreOpacityInRedGreen, maskObject) {
2025-06-18 11:05:23 +08:00
// 使用当前画布尺寸
2025-06-23 00:40:45 +08:00
// const canvasWidth =
// this.canvasManager?.canvasWidth?.value || this.canvas.width;
// const canvasHeight =
// this.canvasManager?.canvasHeight?.value || this.canvas.height;
// console.log(`普通模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`);
// 使用图层栅格化的方法导出图片
const dataURL = await createRasterizedImage({
canvas: this.canvas,
fabricObjects: objectsToExport,
format: expPicType, // 导出格式
isReturenDataURL: true, // 返回数据URL
maskObject: maskObject ?? null, // 使用裁剪对象
trimWhitespace: true, // 裁剪空白
trimPadding: 0, // 裁剪边距
});
2025-06-18 11:05:23 +08:00
2025-06-23 00:40:45 +08:00
console.log("导出图片数据URL:", dataURL);
return dataURL;
2025-06-18 11:05:23 +08:00
// // 创建与画布相同尺寸的临时画布
// const scaleFactor = 2; // 高清导出
// const tempCanvas = document.createElement("canvas");
// tempCanvas.width = canvasWidth * scaleFactor;
// tempCanvas.height = canvasHeight * scaleFactor;
// tempCanvas.style.width = canvasWidth + "px";
// tempCanvas.style.height = canvasHeight + "px";
// const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas, {
// width: canvasWidth,
// height: canvasHeight,
// backgroundColor: null,
// });
// tempFabricCanvas.enableRetinaScaling = true;
// tempFabricCanvas.imageSmoothingEnabled = true;
// tempFabricCanvas.setZoom(1);
// try {
// // 克隆并添加所有对象到临时画布
// for (const obj of objectsToExport) {
// const cloned = await this._cloneObjectForExport(
// obj,
// restoreOpacityInRedGreen && false, // 普通模式不强制恢复透明度
// );
// if (cloned) {
// tempFabricCanvas.add(cloned);
// }
// }
// // 渲染画布
// tempFabricCanvas.renderAll();
// // 生成图片
// return this._generateHighQualityDataURL(tempCanvas, expPicType);
// } finally {
// this._cleanupTempCanvas(tempFabricCanvas);
// }
2025-06-09 10:25:54 +08:00
}
/**
2025-06-18 11:05:23 +08:00
* 获取固定图层对象
* @returns {Object|null} 固定图层对象
2025-06-09 10:25:54 +08:00
* @private
*/
2025-06-18 11:05:23 +08:00
_getFixedLayerObject() {
const allLayers = this._getAllLayers();
const fixedLayer = allLayers.find((layer) => layer.isFixed);
2025-06-09 10:25:54 +08:00
2025-06-18 11:05:23 +08:00
if (!fixedLayer || !fixedLayer.fabricObject) {
return null;
2025-06-09 10:25:54 +08:00
}
2025-06-18 11:05:23 +08:00
// 如果有ID通过ID查找画布中的实际对象
if (fixedLayer.fabricObject.id) {
const result = findObjectById(this.canvas, fixedLayer.fabricObject.id);
return result.object || fixedLayer.fabricObject;
2025-06-09 10:25:54 +08:00
}
2025-06-18 11:05:23 +08:00
return fixedLayer.fabricObject;
2025-06-09 10:25:54 +08:00
}
/**
2025-06-22 13:52:28 +08:00
* 异步克隆fabric对象参照createRasterizedImage的方法
* @param {fabric.Object} obj 要克隆的对象
* @param {Array} propertiesToInclude 要包含的属性
* @returns {Promise<fabric.Object>} 克隆的对象
2025-06-09 10:25:54 +08:00
* @private
*/
_cloneObjectAsync(obj, propertiesToInclude = ["id", "layerId", "layerName", "name"]) {
2025-06-18 11:05:23 +08:00
return new Promise((resolve, reject) => {
2025-06-22 13:52:28 +08:00
if (!obj) {
2025-06-18 11:05:23 +08:00
resolve(null);
2025-06-22 13:52:28 +08:00
return;
2025-06-18 11:05:23 +08:00
}
try {
2025-06-22 13:52:28 +08:00
obj.clone((cloned) => {
if (cloned) {
resolve(cloned);
} else {
reject(new Error("对象克隆失败"));
}
}, propertiesToInclude);
2025-06-18 11:05:23 +08:00
} catch (error) {
2025-06-22 13:52:28 +08:00
console.warn("克隆对象失败:", error);
resolve(null);
2025-06-18 11:05:23 +08:00
}
});
}
/**
2025-06-22 13:52:28 +08:00
* 克隆对象用于导出优化版本
* @param {Object} obj fabric对象
* @param {Boolean} forceRestoreOpacity 是否强制恢复透明度为1
* @param {Boolean} removeClipPath 是否移除裁剪路径
2025-06-18 11:05:23 +08:00
* @returns {Promise<Object>} 克隆的对象
* @private
*/
async _cloneObjectForExport(obj, forceRestoreOpacity = false, removeClipPath = true) {
2025-06-18 11:05:23 +08:00
if (!obj) return null;
try {
2025-06-22 13:52:28 +08:00
// 使用异步克隆方法
const cloned = await this._cloneObjectAsync(obj);
2025-06-18 11:05:23 +08:00
if (cloned) {
2025-06-22 13:52:28 +08:00
// 保持原始位置和属性
2025-06-18 11:05:23 +08:00
cloned.set({
2025-06-22 13:52:28 +08:00
selectable: false,
evented: false,
visible: true,
2025-06-18 11:05:23 +08:00
});
2025-06-22 13:52:28 +08:00
// 如果需要恢复透明度
if (forceRestoreOpacity) {
cloned.set({ opacity: 1 });
}
2025-06-18 11:05:23 +08:00
2025-06-22 13:52:28 +08:00
// 移除裁剪路径以避免绝对路径问题
if (removeClipPath && cloned.clipPath) {
console.log(`移除对象 ${cloned.id || "未知"} 的裁剪路径`);
cloned.clipPath = null;
}
2025-06-18 11:05:23 +08:00
return cloned;
}
} catch (error) {
2025-06-22 13:52:28 +08:00
console.warn("克隆对象失败:", error);
2025-06-18 11:05:23 +08:00
}
return null;
}
/**
2025-06-22 13:52:28 +08:00
* 导出对象组
2025-06-18 11:05:23 +08:00
* @param {Array} objectsToExport 要导出的对象数组
* @param {String} expPicType 导出类型
* @param {Boolean} isRedGreenMode 是否为红绿图模式
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
* @returns {Promise<String>} 图片数据URL
* @private
*/
async _exportObjectsAsGroup(
objectsToExport,
expPicType,
isRedGreenMode = false,
restoreOpacityInRedGreen = true
) {
if (!objectsToExport || objectsToExport.length === 0) {
throw new Error("没有可导出的对象");
}
// 计算所有对象的边界
const bounds = this._calculateGroupBounds(objectsToExport);
console.log("导出边界:", bounds);
// 创建高质量临时画布
2025-06-22 13:52:28 +08:00
const scaleFactor = 2; // 高清导出
const tempCanvas = document.createElement("canvas");
tempCanvas.width = bounds.width * scaleFactor;
tempCanvas.height = bounds.height * scaleFactor;
tempCanvas.style.width = bounds.width + "px";
tempCanvas.style.height = bounds.height + "px";
const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas, {
width: bounds.width,
height: bounds.height,
backgroundColor: null, // 透明背景
});
// 启用高清缩放和图像平滑
tempFabricCanvas.enableRetinaScaling = true;
tempFabricCanvas.imageSmoothingEnabled = true;
tempFabricCanvas.setZoom(scaleFactor);
2025-06-18 11:05:23 +08:00
try {
// 克隆所有对象并添加到临时画布
const clonedObjects = [];
for (const obj of objectsToExport) {
const cloned = await this._cloneAndAddObjectWithOffset(
tempFabricCanvas,
obj,
bounds,
isRedGreenMode,
restoreOpacityInRedGreen
);
if (cloned) {
clonedObjects.push(cloned);
}
}
console.log(`成功克隆 ${clonedObjects.length} 个对象进行导出`);
// 渲染画布
tempFabricCanvas.renderAll();
// 生成高质量数据URL
return this._generateHighQualityDataURL(tempCanvas, expPicType);
} finally {
this._cleanupTempCanvas(tempFabricCanvas);
}
}
/**
2025-06-22 13:52:28 +08:00
* 获取裁剪路径对象优化版本
2025-06-18 11:05:23 +08:00
* @param {Object} fixedBounds 固定图层边界
* @returns {Promise<Object|null>} 裁剪路径对象
* @private
*/
2025-06-22 13:52:28 +08:00
async _getClipPathObject(fixedBounds) {
2025-06-18 11:05:23 +08:00
try {
2025-06-22 13:52:28 +08:00
// const allLayers = this._getAllLayers();
// // 查找第一个有裁剪遮罩的图层
// let clipObject = null;
// for (const layer of allLayers) {
// if (layer.clippingMask?.id) {
// const result = findObjectById(this.canvas, layer.clippingMask.id);
// if (result?.object) {
// clipObject = result.object;
// break;
// }
// }
// }
const clipObject = this.canvas?.clipPath;
2025-06-18 11:05:23 +08:00
if (!clipObject) {
2025-06-22 13:52:28 +08:00
console.warn("未找到可用的裁剪对象");
2025-06-18 11:05:23 +08:00
return null;
}
// 克隆对象作为裁剪路径
const clonedClipPath = await this._cloneObjectForExport(clipObject, false, false);
2025-06-18 11:05:23 +08:00
if (!clonedClipPath) {
2025-06-22 13:52:28 +08:00
console.warn("无法克隆裁剪对象");
2025-06-18 11:05:23 +08:00
return null;
}
// 调整裁剪路径的位置相对于固定图层
clonedClipPath.set({
left: clonedClipPath.left - fixedBounds.left,
top: clonedClipPath.top - fixedBounds.top,
absolutePositioned: true, // 使用绝对定位
});
// 更新坐标
clonedClipPath.setCoords();
2025-06-22 13:52:28 +08:00
console.log("成功创建裁剪路径:", {
2025-06-18 11:05:23 +08:00
objectType: clonedClipPath.type,
position: { left: clonedClipPath.left, top: clonedClipPath.top },
size: { width: clonedClipPath.width, height: clonedClipPath.height },
});
return clonedClipPath;
} catch (error) {
2025-06-22 13:52:28 +08:00
console.error("获取裁剪路径失败:", error);
2025-06-18 11:05:23 +08:00
return null;
}
2025-06-09 10:25:54 +08:00
}
2025-06-22 13:52:28 +08:00
/**
* 生成高质量数据URL
* @param {HTMLCanvasElement} canvas 画布元素
* @param {String} expPicType 导出类型
* @returns {String} 数据URL
* @private
*/
_generateHighQualityDataURL(canvas, expPicType) {
const format = expPicType.toLowerCase();
switch (format) {
case "jpg":
case "jpeg":
// 对于JPEG使用较高质量但JPEG不支持透明背景
return canvas.toDataURL("image/jpeg", 0.95);
case "svg":
// SVG导出需要特殊处理这里先返回高质量PNG
console.warn("SVG导出暂未实现返回高质量PNG格式");
return canvas.toDataURL("image/png", 1.0);
case "png":
default:
// PNG使用最高质量支持透明背景
return canvas.toDataURL("image/png", 1.0);
}
}
/**
* 生成空白图片
* @param {String} expPicType 导出类型
* @returns {String} 空白图片数据URL
* @private
*/
_generateEmptyImage(expPicType) {
const emptyCanvas = document.createElement("canvas");
emptyCanvas.width = 1;
emptyCanvas.height = 1;
// 确保透明背景
const ctx = emptyCanvas.getContext("2d");
ctx.clearRect(0, 0, 1, 1);
return this._generateHighQualityDataURL(emptyCanvas, expPicType);
}
/**
* 清理临时画布资源
* @param {fabric.StaticCanvas} tempFabricCanvas 临时Fabric画布
* @private
*/
_cleanupTempCanvas(tempFabricCanvas) {
if (tempFabricCanvas) {
try {
tempFabricCanvas.dispose();
} catch (error) {
console.warn("清理临时画布失败:", error);
}
}
}
/**
* 获取所有图层
* @returns {Array} 图层数组
* @private
*/
_getAllLayers() {
if (this.layerManager && this.layerManager.layers) {
return this.layerManager.layers.value || [];
}
return [];
}
/**
* 根据ID获取图层
* @param {String} layerId 图层ID
* @returns {Object|null} 图层对象
* @private
*/
_getLayerById(layerId) {
if (this.layerManager && this.layerManager.getLayerById) {
return this.layerManager.getLayerById(layerId);
}
// 备用方法:直接从图层数组中查找
const allLayers = this._getAllLayers();
return allLayers.find((layer) => layer.id === layerId) || null;
}
/**
* 检查图层是否应该包含在导出中
* @param {Object} layer 图层对象
* @param {Boolean} isContainBg 是否包含背景图层
* @param {Boolean} isContainFixed 是否包含固定图层
* @returns {Boolean} 是否应该包含
* @private
*/
_shouldIncludeLayer(layer, isContainBg, isContainFixed) {
if (!layer) return false;
// 检查背景图层
if (layer.isBackground) {
return isContainBg;
}
// 检查固定图层
if (layer.isFixed) {
return isContainFixed;
}
// 普通图层总是包含
return true;
}
2025-06-18 11:05:23 +08:00
}