Files
aida_front/src/component/Canvas/CanvasEditor/managers/ExportManager.js
2025-06-18 11:05:23 +08:00

883 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { fabric } from "fabric-with-all";
import { findObjectById } from "../utils/helper";
/**
* 图片导出管理器
* 负责处理画布的图片导出功能,支持多种导出选项和图层过滤
*/
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)
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
* @returns {String} 导出的图片数据URL
*/
exportImage(options = {}) {
const {
isContainBg = false,
isContainFixed = false,
layerId = "",
layerIdArray = [],
expPicType = "png",
restoreOpacityInRedGreen = true,
} = options;
try {
// 检查是否为红绿图模式
const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false;
// 如果指定了具体图层ID导出指定图层
if (layerId) {
return this._exportSpecificLayer(
layerId,
expPicType,
isRedGreenMode,
restoreOpacityInRedGreen
);
}
// 如果指定了多个图层ID导出多个图层
if (layerIdArray && layerIdArray.length > 0) {
return this._exportMultipleLayers(
layerIdArray,
expPicType,
isContainBg,
isContainFixed,
isRedGreenMode,
restoreOpacityInRedGreen
);
}
// 默认导出所有可见图层
return this._exportAllLayers(
expPicType,
isContainBg,
isContainFixed,
isRedGreenMode,
restoreOpacityInRedGreen
);
} catch (error) {
console.error("导出图片失败:", error);
throw new Error(`图片导出失败: ${error.message}`);
}
}
/**
* 导出指定单个图层
* @param {String} layerId 图层ID
* @param {String} expPicType 导出类型
* @param {Boolean} isRedGreenMode 是否为红绿图模式
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
* @returns {String} 图片数据URL
* @private
*/
_exportSpecificLayer(
layerId,
expPicType,
isRedGreenMode,
restoreOpacityInRedGreen
) {
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} 不可见,将导出空白图片`);
}
// 收集所有需要导出的对象
const objectsToExport = this._collectObjectsFromLayer(layer);
if (objectsToExport.length === 0) {
console.warn(`图层 ${layer.name} 没有可导出的对象`);
return this._generateEmptyImage(expPicType);
}
// 红绿图模式下使用固定尺寸和裁剪
if (isRedGreenMode) {
return this._exportWithRedGreenMode(
objectsToExport,
expPicType,
restoreOpacityInRedGreen
);
}
// 普通模式使用画布尺寸
return this._exportWithCanvasSize(
objectsToExport,
expPicType,
restoreOpacityInRedGreen
);
}
/**
* 导出多个指定图层
* @param {Array} layerIdArray 图层ID数组
* @param {String} expPicType 导出类型
* @param {Boolean} isContainBg 是否包含背景图层
* @param {Boolean} isContainFixed 是否包含固定图层
* @param {Boolean} isRedGreenMode 是否为红绿图模式
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
* @returns {String} 图片数据URL
* @private
*/
_exportMultipleLayers(
layerIdArray,
expPicType,
isContainBg,
isContainFixed,
isRedGreenMode,
restoreOpacityInRedGreen
) {
if (!this.layerManager) {
throw new Error("图层管理器未初始化");
}
// 按图层顺序收集对象(从底到顶)
const objectsToExport = this._collectObjectsByLayerOrder(
layerIdArray,
isContainBg,
isContainFixed
);
if (objectsToExport.length === 0) {
console.warn("没有可导出的对象");
return this._generateEmptyImage(expPicType);
}
// 红绿图模式下使用固定尺寸和裁剪
if (isRedGreenMode) {
return this._exportWithRedGreenMode(
objectsToExport,
expPicType,
restoreOpacityInRedGreen
);
}
// 普通模式使用画布尺寸
return this._exportWithCanvasSize(
objectsToExport,
expPicType,
restoreOpacityInRedGreen
);
}
/**
* 导出所有图层
* @param {String} expPicType 导出类型
* @param {Boolean} isContainBg 是否包含背景图层
* @param {Boolean} isContainFixed 是否包含固定图层
* @param {Boolean} isRedGreenMode 是否为红绿图模式
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
* @returns {String} 图片数据URL
* @private
*/
_exportAllLayers(
expPicType,
isContainBg,
isContainFixed,
isRedGreenMode,
restoreOpacityInRedGreen
) {
// 按图层顺序收集对象(从底到顶)
const objectsToExport = this._collectObjectsByLayerOrder(
null, // 导出所有图层
isContainBg,
isContainFixed
);
if (objectsToExport.length === 0) {
console.warn("没有可导出的对象");
return this._generateEmptyImage(expPicType);
}
// 红绿图模式下使用固定尺寸和裁剪
if (isRedGreenMode) {
return this._exportWithRedGreenMode(
objectsToExport,
expPicType,
restoreOpacityInRedGreen
);
}
// 普通模式使用画布尺寸
return this._exportWithCanvasSize(
objectsToExport,
expPicType,
restoreOpacityInRedGreen
);
}
/**
* 按图层顺序收集对象(从底到顶)
* @param {Array|null} layerIdArray 图层ID数组null表示所有图层
* @param {Boolean} isContainBg 是否包含背景图层
* @param {Boolean} isContainFixed 是否包含固定图层
* @returns {Array} 按正确顺序排列的对象数组
* @private
*/
_collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed) {
const objectsToExport = [];
const allLayers = this._getAllLayers();
// 图层数组是从顶到底的顺序,需要反向遍历以获得从底到顶的渲染顺序
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;
if (layer.visible) {
const layerObjects = this._collectObjectsFromLayer(layer);
objectsToExport.push(...layerObjects);
}
}
return objectsToExport;
}
/**
* 红绿图模式导出(使用固定图层底图作为画布尺寸和裁剪区域)
* @param {Array} objectsToExport 要导出的对象数组
* @param {String} expPicType 导出类型
* @param {Boolean} restoreOpacityInRedGreen 是否恢复透明度为1
* @returns {String} 图片数据URL
* @private
*/
async _exportWithRedGreenMode(
objectsToExport,
expPicType,
restoreOpacityInRedGreen
) {
// 获取固定图层对象(衣服底图)作为参考
const fixedLayerObject = this._getFixedLayerObject();
if (!fixedLayerObject) {
console.warn("红绿图模式下未找到固定图层对象,使用画布尺寸");
return this._exportWithCanvasSize(
objectsToExport,
expPicType,
restoreOpacityInRedGreen
);
}
// 获取固定图层对象的边界矩形(包含位置、尺寸、缩放等信息)
const fixedBounds = fixedLayerObject.getBoundingRect();
// 使用固定图层的实际显示尺寸作为导出画布尺寸
const canvasWidth = Math.round(fixedBounds.width);
const canvasHeight = Math.round(fixedBounds.height);
console.log(`红绿图模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`);
console.log("固定图层边界:", fixedBounds);
// 创建固定尺寸的临时画布
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,
enableRetinaScaling: true,
imageSmoothingEnabled: true,
});
tempFabricCanvas.setZoom(1);
try {
// 获取图层下标为1的对象作为裁剪路径
const clipPathObject = await this._getLayerClipPathObject(1, fixedBounds);
// 克隆并添加所有对象到临时画布,需要调整位置相对于固定图层
for (let i = 0; i < objectsToExport.length; i++) {
const obj = objectsToExport[i];
const cloned = await this._cloneObjectForExport(
obj,
restoreOpacityInRedGreen && true
);
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);
}
}
/**
* 普通模式导出(使用画布尺寸)
* @param {Array} objectsToExport 要导出的对象数组
* @param {String} expPicType 导出类型
* @param {Boolean} restoreOpacityInRedGreen 是否恢复透明度为1
* @returns {String} 图片数据URL
* @private
*/
async _exportWithCanvasSize(
objectsToExport,
expPicType,
restoreOpacityInRedGreen
) {
// 使用当前画布尺寸
const canvasWidth = this.canvas.width;
const canvasHeight = this.canvas.height;
console.log(`普通模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`);
// 创建与画布相同尺寸的临时画布
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(scaleFactor);
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);
}
}
/**
* 获取固定图层对象
* @returns {Object|null} 固定图层对象
* @private
*/
_getFixedLayerObject() {
const allLayers = this._getAllLayers();
const fixedLayer = allLayers.find((layer) => layer.isFixed);
if (!fixedLayer || !fixedLayer.fabricObject) {
return null;
}
// 如果有ID通过ID查找画布中的实际对象
if (fixedLayer.fabricObject.id) {
const result = findObjectById(this.canvas, fixedLayer.fabricObject.id);
return result.object || fixedLayer.fabricObject;
}
return fixedLayer.fabricObject;
}
/**
* 克隆对象用于导出
* @param {Object} obj fabric对象
* @param {Boolean} forceRestoreOpacity 是否强制恢复透明度为1
* @param {Boolean} removeClipPath 是否移除裁剪路径
* @returns {Promise<Object>} 克隆的对象
* @private
*/
async _cloneObjectForExport(
obj,
forceRestoreOpacity = false,
removeClipPath = true
) {
if (!obj) return null;
return new Promise((resolve, reject) => {
try {
obj.clone(
(cloned) => {
if (cloned) {
// 保持原始位置和属性
cloned.set({
selectable: false,
evented: false,
visible: true,
});
// 如果需要恢复透明度
if (forceRestoreOpacity) {
cloned.set({ opacity: 1 });
}
// 移除裁剪路径以避免绝对路径问题
if (removeClipPath && cloned.clipPath) {
console.log(`移除对象 ${cloned.id || "未知"} 的裁剪路径`);
cloned.clipPath = null;
}
resolve(cloned);
} else {
resolve(null);
}
},
["id", "layerId", "layerName", "name"]
);
} catch (error) {
console.warn("克隆对象失败:", error);
resolve(null);
}
});
}
/**
* 生成高质量数据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 图层对象
* @returns {Array} 对象数组
* @private
*/
_collectObjectsFromLayer(layer) {
if (!layer || !layer.fabricObjects) {
return [];
}
return layer.fabricObjects.filter((obj) => obj && obj.visible !== false);
}
/**
* 检查是否应该包含该图层
* @param {Object} layer 图层对象
* @param {Boolean} isContainBg 是否包含背景图层
* @param {Boolean} isContainFixed 是否包含固定图层
* @returns {Boolean} 是否包含
* @private
*/
_shouldIncludeLayer(layer, isContainBg, isContainFixed) {
// 背景图层
if (layer.isBackground) {
return isContainBg;
}
// 固定图层
if (layer.isFixed) {
return isContainFixed;
}
// 普通图层始终包含
return true;
}
/**
* 计算对象组的边界
* @param {Array} objects 对象数组
* @returns {Object} 边界信息 {left, top, width, height}
* @private
*/
_calculateGroupBounds(objects) {
if (!objects || objects.length === 0) {
return { left: 0, top: 0, width: 100, height: 100 };
}
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
objects.forEach((obj) => {
if (!obj) return;
try {
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);
} catch (error) {
console.warn("计算对象边界失败:", error);
}
});
// 如果没有有效边界,使用默认值
if (minX === Infinity || minY === Infinity) {
return { left: 0, top: 0, width: 100, height: 100 };
}
return {
left: minX,
top: minY,
width: maxX - minX,
height: maxY - minY,
};
}
/**
* 创建高质量导出画布
* @param {Object} bounds 边界信息
* @param {Number} scaleFactor 缩放因子
* @returns {HTMLCanvasElement} 画布元素
* @private
*/
_createHighQualityExportCanvas(bounds, scaleFactor) {
const canvas = document.createElement("canvas");
// 设置画布的实际像素尺寸(用于高清导出)
canvas.width = bounds.width * scaleFactor;
canvas.height = bounds.height * scaleFactor;
// 设置画布的显示尺寸CSS尺寸
canvas.style.width = bounds.width + "px";
canvas.style.height = bounds.height + "px";
// 启用高质量渲染
const ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
return canvas;
}
/**
* 创建高质量临时Fabric画布
* @param {HTMLCanvasElement} canvas 画布元素
* @param {Object} bounds 边界信息
* @param {Number} scaleFactor 缩放因子
* @returns {fabric.StaticCanvas} Fabric画布
* @private
*/
_createHighQualityTempFabricCanvas(canvas, bounds, scaleFactor) {
const tempFabricCanvas = new fabric.StaticCanvas(canvas, {
width: bounds.width,
height: bounds.height,
backgroundColor: null, // 透明背景
});
// 启用高清缩放和图像平滑
tempFabricCanvas.enableRetinaScaling = true;
tempFabricCanvas.imageSmoothingEnabled = true;
tempFabricCanvas.setZoom(scaleFactor);
return tempFabricCanvas;
}
/**
* 克隆对象并添加到临时画布(带偏移处理)
* @param {fabric.StaticCanvas} tempCanvas 临时画布
* @param {Object} obj 原始对象
* @param {Object} bounds 边界信息
* @param {Boolean} isRedGreenMode 是否为红绿图模式
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
* @returns {Promise<Object>} 克隆的对象
* @private
*/
async _cloneAndAddObjectWithOffset(
tempCanvas,
obj,
bounds,
isRedGreenMode,
restoreOpacityInRedGreen
) {
if (!obj) return null;
try {
const cloned = await this._cloneObjectForExport(
obj,
isRedGreenMode && restoreOpacityInRedGreen
);
if (cloned) {
// 调整对象位置,减去边界偏移量,使对象在新画布中正确定位
cloned.set({
left: cloned.left - bounds.left,
top: cloned.top - bounds.top,
});
// 更新对象坐标
cloned.setCoords();
// 添加到临时画布
tempCanvas.add(cloned);
return cloned;
}
} catch (error) {
console.warn("克隆并添加对象失败:", error);
}
return null;
}
/**
* 将对象作为组导出(高质量版本)
* @param {Array} objectsToExport 要导出的对象数组
* @param {String} expPicType 导出类型
* @param {Number} scaleFactor 缩放因子,用于高清导出
* @param {Boolean} isRedGreenMode 是否为红绿图模式
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
* @returns {Promise<String>} 图片数据URL
* @private
*/
async _exportObjectsAsGroup(
objectsToExport,
expPicType,
scaleFactor = 2,
isRedGreenMode = false,
restoreOpacityInRedGreen = true
) {
if (!objectsToExport || objectsToExport.length === 0) {
throw new Error("没有可导出的对象");
}
// 计算所有对象的边界
const bounds = this._calculateGroupBounds(objectsToExport);
console.log("导出边界:", bounds);
// 创建高质量临时画布
const tempCanvas = this._createHighQualityExportCanvas(bounds, scaleFactor);
const tempFabricCanvas = this._createHighQualityTempFabricCanvas(
tempCanvas,
bounds,
scaleFactor
);
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);
}
}
/**
* 获取图层下标为1的对象作为裁剪路径
* @param {Number} layerIndex 图层下标
* @param {Object} fixedBounds 固定图层边界
* @returns {Promise<Object|null>} 裁剪路径对象
* @private
*/
async _getLayerClipPathObject(layerIndex, fixedBounds) {
try {
const allLayers = this._getAllLayers();
// 获取指定下标的图层从底到顶下标0是最底层
const targetLayerIndex = layerIndex;
if (targetLayerIndex < 0 || targetLayerIndex >= allLayers.length) {
console.warn(
`图层下标 ${layerIndex} 超出范围,总图层数: ${allLayers.length}`
);
return null;
}
const targetLayer = allLayers[targetLayerIndex];
if (
!targetLayer ||
!targetLayer.visible ||
!targetLayer.fabricObjects ||
targetLayer.fabricObjects.length === 0
) {
console.warn(`图层下标 ${layerIndex} 不可见或没有对象`);
return null;
}
// 获取图层中的第一个对象作为裁剪路径
const clipObject = targetLayer.fabricObjects[0];
if (!clipObject) {
console.warn(`图层下标 ${layerIndex} 中没有可用的裁剪对象`);
return null;
}
// 克隆对象作为裁剪路径
const clonedClipPath = await this._cloneObjectForExport(
clipObject,
false,
false
);
if (!clonedClipPath) {
console.warn(`无法克隆图层下标 ${layerIndex} 的裁剪对象`);
return null;
}
// 调整裁剪路径的位置相对于固定图层
clonedClipPath.set({
left: clonedClipPath.left - fixedBounds.left,
top: clonedClipPath.top - fixedBounds.top,
absolutePositioned: true, // 使用绝对定位
});
// 更新坐标
clonedClipPath.setCoords();
console.log(`成功创建图层下标 ${layerIndex} 的裁剪路径:`, {
objectType: clonedClipPath.type,
position: { left: clonedClipPath.left, top: clonedClipPath.top },
size: { width: clonedClipPath.width, height: clonedClipPath.height },
});
return clonedClipPath;
} catch (error) {
console.error(`获取图层下标 ${layerIndex} 裁剪路径失败:`, error);
return null;
}
}
}