feat: 添加导出图层功能,支持将图层转换为位图图像并下载

This commit is contained in:
bighuixiang
2025-06-23 15:56:01 +08:00
parent 1fde475806
commit 5d0511e405
10 changed files with 405 additions and 68 deletions

View File

@@ -10,6 +10,7 @@ import {
insertObjectAtZIndex,
} from "../utils/helper";
import { fabric } from "fabric-with-all";
import { imageModeHandler } from "../utils/imageHelper";
/**
* 设置活动图层命令
@@ -725,48 +726,13 @@ export class ChangeFixedImageCommand extends Command {
});
}
// 如果是包含 则需要根据图像模式调整大小
switch (this.imageMode) {
case "stretch":
// 拉伸模式 - 填充整个画布
newImage.scaleToWidth(this.canvasWidth);
newImage.scaleToHeight(this.canvasHeight);
break;
case "tile":
// 平铺模式 - 保持原始大小
newImage.scaleX = 1;
newImage.scaleY = 1;
break;
case "stretchTile":
// 拉伸平铺模式 - 填充整个画布,但保持宽高比
newImage.scaleToWidth(this.canvasWidth);
newImage.scaleToHeight(this.canvasHeight);
break;
case "stretchTileCrop":
// 拉伸平铺并裁剪模式 - 填充整个画布,可能
// 会裁剪图像以适应画布
newImage.scaleToWidth(this.canvasWidth);
newImage.scaleToHeight(this.canvasHeight);
// 这里可以添加裁剪逻辑,如果需要的话
// 例如使用fabric.Image.clipPath来裁剪图像
break;
case "contains":
// 包含模式 - 保证图像在画布内完整显示
// 既要考虑画布的宽高比,也要考虑图像的宽高比
// 图片缩放后要保证最长边能完全显示在画布内
const canvasAspect = this.canvasWidth / this.canvasHeight;
const imageAspect = newImage.width / newImage.height;
// 保证图像在画布内完整显示 - 既要考虑画布的宽高比,也要考虑图像的宽高比
// 图片缩放后要保证最长边能完全显示在画布内
if (imageAspect > canvasAspect) {
// 图像更宽
newImage.scaleToWidth(this.canvasWidth);
} else {
// 图像更高
newImage.scaleToHeight(this.canvasHeight);
}
break;
}
// 通用处理图片模式
imageModeHandler({
imageMode: this.imageMode,
newImage,
canvasWidth: this.canvasWidth,
canvasHeight: this.canvasHeight,
});
// 使用帮助函数在指定z-index位置插入新图像
if (this.previousZIndex !== undefined && this.previousZIndex >= 0) {

View File

@@ -13,6 +13,7 @@ import {
removeCanvasObjectByObject,
} from "../utils/helper";
import { createRasterizedImage } from "../utils/rasterizedImage";
import { message } from "ant-design-vue";
/**
* 栅格化图层命令
@@ -376,3 +377,196 @@ export class RasterizeLayerCommand extends Command {
};
}
}
/**
* 导出图层命令
* 将图层中的所有矢量对象转换为位图图像
* 支持普通图层和组图层的栅格化
*/
export class ExportLayerToImageCommand extends Command {
constructor(options) {
super({
name: "导出图层",
saveState: true,
});
this.canvas = options.canvas;
this.layers = options.layers;
this.layerId = options.layerId; // 指定要栅格化的图层ID
// 是否包含锁定对象
this.hasLocked = options.hasLocked || true;
// 是否包含隐藏对象
this.hasHidden = options.hasHidden || false;
this.activeLayerId = options.activeLayerId;
this.layerManager = options.layerManager;
// 查找目标图层
const { layer, parent } = findLayerRecursively(
this.layers.value,
this.layerId
);
this.layer = layer;
this.parentLayer = parent;
this.isGroupLayer = this.layer?.children && this.layer.children.length > 0;
// 保存原始状态用于撤销
this.originalLayers = [...this.layers.value];
this.originalCanvasObjects = [...this.canvas.getObjects()];
this.originalObjectStates = new Map();
// 栅格化结果
this.rasterizedImage = null;
this.rasterizedImageId = null;
// 生成新图层ID
this.rasterizedLayerId = generateId("rasterized_layer_");
this.resterizedId = generateId("rasterized_");
this.rasterizedLayer = null;
// 要栅格化的图层和对象
this.layersToRasterize = [];
this.objectsToRasterize = [];
}
async execute() {
// 查找目标图层
const { layer, parent } = findLayerRecursively(
this.layers.value,
this.layerId
);
this.layer = layer;
this.parentLayer = parent;
this.isGroupLayer = this.layer?.children && this.layer.children.length > 0;
if (!this.layer) {
throw new Error(`图层 ${this.layerId} 不存在`);
}
try {
// 收集要栅格化的图层和对象
this._collectLayersAndObjects();
if (this.objectsToRasterize.length === 0) {
message.error("图层没有内容可导出");
throw new Error("图层没有内容可导出");
}
// 保存原始对象状态
this.canvas.discardActiveObject();
this.canvas.renderAll();
// 创建图像
const imageBase64 = await createRasterizedImage({
canvas: this.canvas,
fabricObjects: this.objectsToRasterize,
isReturenDataURL: true,
});
// 模拟浏览器下载
const link = document.createElement("a");
link.href = imageBase64;
link.download = `${this.layer.name}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
console.log(`✅ 图层 ${this.layer.name} 导出完成`);
return true;
} catch (error) {
console.error("导出图层失败:", error);
throw error;
}
}
async undo() {
// 导出图片不需要撤销操作
}
/**
* 收集要栅格化的图层和对象
* @private
*/
_collectLayersAndObjects() {
if (this.isGroupLayer) {
// 组图层:收集自身和所有子图层
this.layersToRasterize = this._collectLayersToRasterize(this.layer);
} else {
// 普通图层:只收集自身
this.layersToRasterize = [this.layer];
}
// 收集所有图层的fabricObjects并按画布z-index顺序排序
const allCanvasObjects = this.canvas.getObjects();
const objectsWithZIndex = [];
this.layersToRasterize.forEach((layer) => {
if (layer.fabricObjects && layer.fabricObjects.length > 0) {
layer.fabricObjects.forEach((layerObj) => {
if (layerObj && layerObj.id) {
const { object } = findObjectById(this.canvas, layerObj.id);
if (object) {
// 获取对象在画布中的z-index数组索引
const zIndex = allCanvasObjects.indexOf(object);
objectsWithZIndex.push({
object: object,
zIndex: zIndex,
layerObj: layerObj,
});
}
}
});
}
});
// 按z-index排序确保保持原有的渲染顺序
objectsWithZIndex.sort((a, b) => a.zIndex - b.zIndex);
// 提取排序后的对象
this.objectsToRasterize = objectsWithZIndex.map((item) => item.object);
console.log(
`📊 收集到 ${this.layersToRasterize.length} 个图层,${this.objectsToRasterize.length} 个对象进行栅格化`
);
console.log(
"🔢 对象z-index顺序:",
objectsWithZIndex.map((item) => ({
id: item.object.id,
type: item.object.type,
zIndex: item.zIndex,
}))
);
}
/**
* 收集要栅格化的图层(递归收集子图层)
* @param {Object} sourceLayer 源图层
* @returns {Array} 图层数组
* @private
*/
_collectLayersToRasterize(sourceLayer) {
const result = [sourceLayer];
// 如果是组图层,收集所有子图层
if (sourceLayer.children && sourceLayer.children.length > 0) {
sourceLayer.children.forEach((childLayer) => {
if (childLayer) {
result.push(...this._collectLayersToRasterize(childLayer));
}
});
}
return result;
}
getInfo() {
return {
name: this.name,
originalLayerId: this.layerId,
originalLayerName: this.layer?.name,
rasterizedLayerId: this.rasterizedLayerId,
rasterizedLayerName: this.rasterizedLayer?.name,
isGroupLayer: this.isGroupLayer,
objectCount: this.objectsToRasterize?.length || 0,
};
}
}

View File

@@ -3,6 +3,7 @@ import {
generateId,
optimizeCanvasRendering,
} from "../utils/helper.js";
import { imageModeHandler } from "../utils/imageHelper.js";
import { LayerType, OperationType } from "../utils/layerHelper.js";
import { Command, CompositeCommand } from "./Command.js";
import { fabric } from "fabric-with-all";
@@ -26,6 +27,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
this.redGreenImageUrl = options.redGreenImageUrl;
this.onImageGenerated = options.onImageGenerated;
this.normalLayerOpacity = options.normalLayerOpacity || 0.4;
this.clothingImageOpts = options.clothingImageOpts || null; // 衣服底图选项 - 用于设置图片加载时的选项
// 存储原始状态以便撤销
this.originalCanvasBackground = null;
@@ -398,15 +400,30 @@ export class BatchInitializeRedGreenModeCommand extends Command {
* 设置衣服底图
*/
async _setupClothingImage(img, fixedLayer) {
// 计算图片缩放,保持上下留边距
const margin = 50;
const maxWidth = this.canvas.width - margin * 2;
const maxHeight = this.canvas.height - margin * 2;
const scale = Math.min(maxWidth / img.width, maxHeight / img.height);
if (this.clothingImageOpts?.imageMode) {
// 如果有衣服底图选项,应用这些选项
// 底图加载方式 1.平铺 2.拉伸 3.拉伸平铺 4.拉伸平铺并裁剪 5.包含
// this.clothingImageOpts?.imageMode // 默认不处理 可选 contains, stretch,tile stretchTile, stretchTileCrop
// 通用处理图片模式
imageModeHandler({
imageMode: this.clothingImageOpts?.imageMode,
newImage: img,
canvasWidth: this.canvas.width,
canvasHeight: this.canvas.height,
});
} else {
// 计算图片缩放,保持上下留边距
const margin = 50;
const maxWidth = this.canvas.width - margin * 2;
const maxHeight = this.canvas.height - margin * 2;
const scale = Math.min(maxWidth / img.width, maxHeight / img.height);
img.set({
scaleX: scale,
scaleY: scale,
});
}
img.set({
scaleX: scale,
scaleY: scale,
left: this.canvas.width / 2,
top: this.canvas.height / 2,
originX: "center",