Files
aida_front/src/component/Canvas/CanvasEditor/utils/rasterizedImage.js

491 lines
14 KiB
JavaScript
Raw Normal View History

2025-06-22 13:52:28 +08:00
// 栅格化帮助
import { fabric } from "fabric-with-all";
import { createStaticCanvas } from "./canvasFactory";
2025-06-22 13:52:28 +08:00
/**
* 创建栅格化图像
* 使用组对象方式避免边界计算误差
2025-06-22 13:52:28 +08:00
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
* @private
*/
export const createRasterizedImage = async ({
canvas, // 画布对象 必填
fabricObjects = [], // 要栅格化的对象列表 - 按顺序 必填
maskObject = null, // 用于裁剪的对象 - 可选
trimWhitespace = true, // 是否裁剪空白区域
trimPadding = 0, // 裁剪边距
2025-06-22 13:52:28 +08:00
quality = 1.0, // 图像质量
format = "png", // 图像格式
scaleFactor = 2, // 高清倍数 - 默认是画布的高清倍数
isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象
2025-06-29 23:29:47 +08:00
isThumbnail = false, // 是否为缩略图
preserveOriginalQuality = true, // 是否保持原始质量
isGroupWithMask = false, // 是否为带遮罩的组图层
2025-06-22 13:52:28 +08:00
} = {}) => {
try {
2026-01-19 16:57:11 +08:00
// console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象${maskObject ? "(带遮罩)" : ""}`);
2025-06-22 13:52:28 +08:00
// 确保有对象需要栅格化
if (fabricObjects.length === 0) {
console.warn("⚠️ 没有对象需要栅格化,返回空图像");
return null;
}
// 高清倍数
const currentZoom = canvas.getZoom?.() || 1;
scaleFactor = Math.max(scaleFactor || canvas?.getRetinaScaling?.(), currentZoom);
2025-06-22 13:52:28 +08:00
2025-06-29 23:29:47 +08:00
if (isThumbnail) scaleFactor = 0.2; // 缩略图使用较小的高清倍数
2026-01-19 16:57:11 +08:00
// console.log(`高清倍数: ${scaleFactor}, 当前缩放: ${currentZoom}`);
2025-06-22 13:52:28 +08:00
// 如果有遮罩且保持原始质量,使用高质量的遮罩处理方法
if (maskObject && preserveOriginalQuality) {
const rasterizedImage = await createRasterizedImageWithMask({
canvas,
objects: fabricObjects,
maskObject,
scaleFactor,
quality,
format,
currentZoom,
isReturenDataURL,
isGroupWithMask,
});
if (!rasterizedImage) {
console.warn("⚠️ 带遮罩的栅格化图像创建失败,返回空图像");
return null;
}
if (isReturenDataURL) {
2026-01-19 16:57:11 +08:00
// console.log("✅ 带遮罩的栅格化图像创建成功返回DataURL");
return rasterizedImage;
}
// 设置栅格化图像的属性
rasterizedImage.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
custom: {
type: "rasterized",
rasterizedAt: new Date().toISOString(),
objectCount: fabricObjects.length,
originalZoom: currentZoom,
hasMask: true,
},
});
2026-01-19 16:57:11 +08:00
// console.log(`✅ 带遮罩的栅格化图像创建完成`);
return rasterizedImage;
}
// 使用原有的组对象方式创建栅格化图像
const rasterizedImage = await createRasterizedImageWithGroup({
2025-06-22 13:52:28 +08:00
canvas,
objects: fabricObjects,
scaleFactor,
maskObject,
trimWhitespace,
trimPadding,
quality,
format,
currentZoom,
isReturenDataURL,
});
if (!rasterizedImage) {
console.warn("⚠️ 栅格化图像创建失败,返回空图像");
return null;
}
if (isReturenDataURL) {
2026-01-19 16:57:11 +08:00
// console.log("✅ 栅格化图像创建成功返回DataURL");
return rasterizedImage;
2025-06-22 13:52:28 +08:00
}
// 设置栅格化图像的属性
if (rasterizedImage) {
rasterizedImage.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
custom: {
type: "rasterized",
rasterizedAt: new Date().toISOString(),
objectCount: fabricObjects.length,
originalZoom: currentZoom,
},
});
2026-01-19 16:57:11 +08:00
// console.log(`✅ 栅格化图像创建完成`);
2025-06-22 13:52:28 +08:00
}
return rasterizedImage;
} catch (error) {
console.error("创建栅格化图像失败:", error);
throw new Error(`栅格化失败: ${error.message}`);
}
};
/**
* 使用组对象方式创建栅格化图像
2025-06-22 13:52:28 +08:00
* @param {Object} options 渲染选项
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
*/
const createRasterizedImageWithGroup = async ({
2025-06-22 13:52:28 +08:00
canvas,
objects,
scaleFactor,
maskObject,
trimWhitespace,
trimPadding,
quality,
format,
currentZoom,
isReturenDataURL,
}) => {
try {
// 创建离屏画布
const offscreenCanvas = createStaticCanvas();
// 克隆所有对象
const clonedObjects = [];
for (const obj of objects) {
const clonedObj = await cloneObjectAsync(obj);
clonedObj.set({
select: false,
evented: false,
hasControls: false,
clipPath: null, // 确保克隆对象没有clipPath
});
clonedObjects.push(clonedObj);
}
2025-06-22 13:52:28 +08:00
// 创建组对象
const group = new fabric.Group(clonedObjects, {
originX: "left",
originY: "top",
});
// 获取组的绝对边界框
const groupBounds = group.getBoundingRect(true, true);
2026-01-19 16:57:11 +08:00
// console.log("📏 组边界框:", groupBounds);
// 设置离屏画布尺寸,使用组的边界大小
const canvasWidth = Math.ceil(groupBounds.width * scaleFactor);
const canvasHeight = Math.ceil(groupBounds.height * scaleFactor);
2025-06-22 13:52:28 +08:00
offscreenCanvas.setDimensions({
width: canvasWidth,
height: canvasHeight,
hasControls: false,
2025-06-22 13:52:28 +08:00
});
2026-01-19 16:57:11 +08:00
// console.log(`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`);
2025-06-22 13:52:28 +08:00
// 调整组的位置,让它位于画布的左上角
group.set({
left: 0,
top: 0,
});
2025-06-22 13:52:28 +08:00
// 取消对象激活
group.set({
selectable: false, // 禁用组的选择
evented: false, // 禁用组的事件
});
2025-06-22 13:52:28 +08:00
// 将组添加到离屏画布
offscreenCanvas.add(group);
2025-06-22 13:52:28 +08:00
// 设置离屏画布的缩放
offscreenCanvas.setZoom(scaleFactor);
2025-06-22 13:52:28 +08:00
// 如果有遮罩对象,应用遮罩
if (maskObject) {
await applyMaskToCanvas(offscreenCanvas, maskObject, groupBounds);
2025-06-22 13:52:28 +08:00
}
// 渲染离屏画布
offscreenCanvas.renderAll();
2025-06-22 13:52:28 +08:00
// 生成图像数据
const dataURL = offscreenCanvas.toDataURL({
format,
quality,
multiplier: 1, // 已经通过画布尺寸处理了高清倍数
});
if (isReturenDataURL) {
// 清理离屏画布
offscreenCanvas.dispose();
2025-06-22 13:52:28 +08:00
return dataURL; // 如果需要返回DataURL
}
2025-06-22 13:52:28 +08:00
// 清理离屏画布
offscreenCanvas.dispose();
// 创建fabric.Image对象
const fabricImage = await createFabricImageFromDataURL(dataURL);
// 设置图像的位置和缩放,使其与原始组的位置和大小匹配
2025-06-22 13:52:28 +08:00
fabricImage.set({
scaleX: 1 / scaleFactor, // 由于我们生成的图像是高清版本,需要缩放回原始大小
scaleY: 1 / scaleFactor,
left: groupBounds.left + groupBounds.width / 2, // 设置为组中心点
top: groupBounds.top + groupBounds.height / 2, // 设置为组中心点
originX: "center",
originY: "center",
2025-06-22 13:52:28 +08:00
});
return fabricImage;
} catch (error) {
console.error("组对象栅格化失败:", error);
2025-06-22 13:52:28 +08:00
throw error;
}
};
/**
* 使用遮罩创建栅格化图像 - 专门处理带遮罩的组图层
* 基于遮罩的位置和大小来裁剪内容确保正确的定位
* @param {Object} options 渲染选项
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
*/
const createRasterizedImageWithMask = async ({
canvas,
objects,
maskObject,
scaleFactor,
quality,
format,
currentZoom,
isReturenDataURL,
isGroupWithMask,
}) => {
try {
2026-01-19 16:57:11 +08:00
// console.log("🎭 使用遮罩创建栅格化图像");
// 获取遮罩的边界框,这将作为最终图像的边界
const maskBounds = maskObject.getBoundingRect(true, true);
2026-01-19 16:57:11 +08:00
// console.log("📏 遮罩边界框:", maskBounds);
// 克隆所有对象,并清除它们的遮罩,避免重复应用
const clonedObjects = [];
for (const obj of objects) {
const clonedObj = await cloneObjectAsync(obj);
clonedObj.set({
select: false,
evented: false,
hasControls: false,
clipPath: null, // 清除clipPath避免重复应用遮罩
});
clonedObjects.push(clonedObj);
}
// 克隆遮罩对象用于裁剪
const clonedMask = await cloneObjectAsync(maskObject);
clonedMask.set({
select: false,
evented: false,
hasControls: false,
absolutePositioned: false, // 设置为绝对定位
// fill: "#ffffff", // 遮罩使用白色
// stroke: "", // 确保没有描边
// strokeWidth: 0,
});
clonedMask.clipPath = null; // 确保克隆的遮罩没有clipPath
// 创建离屏画布,使用遮罩的边界大小
const offscreenCanvas = createStaticCanvas();
const canvasWidth = Math.ceil(maskBounds.width * scaleFactor);
const canvasHeight = Math.ceil(maskBounds.height * scaleFactor);
offscreenCanvas.setDimensions({
width: canvasWidth,
height: canvasHeight,
});
2026-01-19 16:57:11 +08:00
// console.log(`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`);
// 调整对象位置,相对于遮罩边界重新定位
clonedObjects.forEach((obj) => {
obj.set({
left: (obj.left - maskBounds.left) * scaleFactor,
top: (obj.top - maskBounds.top) * scaleFactor,
scaleX: (obj.scaleX || 1) * scaleFactor,
scaleY: (obj.scaleY || 1) * scaleFactor,
});
obj.setCoords();
});
// 调整遮罩位置
clonedMask.set({
left: 0, // 遮罩从画布左上角开始
top: 0,
scaleX: (clonedMask.scaleX || 1) * scaleFactor,
scaleY: (clonedMask.scaleY || 1) * scaleFactor,
originX: "left",
originY: "top",
absolutePositioned: true, // 设置为绝对定位
});
clonedMask.setCoords();
// 添加所有对象到离屏画布
clonedObjects.forEach((obj) => {
offscreenCanvas.add(obj);
});
// 使用遮罩作为画布的clipPath来裁剪内容
offscreenCanvas.clipPath = clonedMask;
// 渲染离屏画布
offscreenCanvas.renderAll();
// 生成图像数据
const dataURL = offscreenCanvas.toDataURL({
format,
quality,
multiplier: 1, // 已经通过画布尺寸和对象缩放处理了高清倍数
});
if (isReturenDataURL) {
// 清理离屏画布
offscreenCanvas.dispose();
return dataURL;
}
// 清理离屏画布
offscreenCanvas.dispose();
// 创建fabric.Image对象
const fabricImage = await createFabricImageFromDataURL(dataURL);
// 设置图像的位置,使其与原始遮罩的位置匹配
fabricImage.set({
scaleX: 1 / scaleFactor, // 由于我们生成的图像是高清版本,需要缩放回原始大小
scaleY: 1 / scaleFactor,
left: maskBounds.left + maskBounds.width / 2, // 设置为遮罩中心点
top: maskBounds.top + maskBounds.height / 2, // 设置为遮罩中心点
originX: "center",
originY: "center",
});
// 确保图像位置正确
fabricImage.setCoords();
2026-01-19 16:57:11 +08:00
// console.log("✅ 带遮罩的栅格化图像创建完成");
return fabricImage;
} catch (error) {
console.error("带遮罩的栅格化失败:", error);
throw error;
}
};
2025-06-22 13:52:28 +08:00
/**
* 异步克隆fabric对象
* @param {fabric.Object} obj 要克隆的对象
* @returns {Promise<fabric.Object>} 克隆的对象
*/
const cloneObjectAsync = (obj) => {
return new Promise((resolve, reject) => {
try {
obj?.clone?.((cloned) => {
if (cloned) {
resolve(cloned);
} else {
reject(new Error("对象克隆失败"));
}
}, {});
} catch (error) {
reject(new Error(`克隆对象时发生错误: ${error.message}`));
}
2025-06-22 13:52:28 +08:00
});
};
/**
* 从DataURL创建fabric.Image对象
* @param {string} dataURL 图像数据URL
* @returns {Promise<fabric.Image>} fabric图像对象
*/
const createFabricImageFromDataURL = (dataURL) => {
return new Promise((resolve, reject) => {
fabric.Image.fromURL(dataURL, (img) => {
if (img) {
resolve(img);
} else {
reject(new Error("无法从DataURL创建图像"));
}
});
});
};
/**
* 应用遮罩到画布如果需要
* @param {fabric.Canvas} canvas 目标画布
* @param {fabric.Object} maskObject 遮罩对象
* @param {Object} bounds 边界框
*/
const applyMaskToCanvas = async (canvas, maskObject, bounds) => {
if (!maskObject) {
2026-01-19 16:57:11 +08:00
// console.log("没有遮罩对象,跳过遮罩应用");
return;
}
try {
2026-01-19 16:57:11 +08:00
// console.log("🎭 应用遮罩到画布");
// 克隆遮罩对象,避免影响原对象
const clonedMask = await cloneObjectAsync(maskObject);
// 设置遮罩属性
clonedMask.set({
fill: "#ffffff", // 遮罩使用白色
stroke: "", // 确保没有描边
strokeWidth: 0,
selectable: false,
evented: false,
absolutePositioned: true, // 设置为绝对定位
});
// 调整遮罩位置,相对于画布边界重新定位
clonedMask.set({
left: clonedMask.left - bounds.left,
top: clonedMask.top - bounds.top,
});
// 将遮罩设置为画布的clipPath
canvas.clipPath = clonedMask;
2026-01-19 16:57:11 +08:00
// console.log("✅ 遮罩应用完成");
} catch (error) {
console.error("应用遮罩失败:", error);
}
2025-06-22 13:52:28 +08:00
};
/**
* 获取对象组的边界框
* @param {Array} fabricObjects fabric对象数组
* @returns {Object} 边界框信息
*/
2025-06-22 13:52:28 +08:00
export const getObjectsBounds = (fabricObjects) => {
if (fabricObjects.length === 0) {
return null;
}
// 创建临时组来获取准确的边界框
const tempGroup = new fabric.Group([...fabricObjects], {
originX: "left",
originY: "top",
});
const bounds = tempGroup.getBoundingRect(true, true);
// 清理临时组
tempGroup.destroy();
return bounds;
2025-06-22 13:52:28 +08:00
};