Files
aida_front/src/component/Canvas/CanvasEditor/utils/rasterizedImage.js
2026-01-19 16:57:11 +08:00

491 lines
14 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 { createStaticCanvas } from "./canvasFactory";
/**
* 创建栅格化图像
* 使用组对象方式,避免边界计算误差
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
* @private
*/
export const createRasterizedImage = async ({
canvas, // 画布对象 必填
fabricObjects = [], // 要栅格化的对象列表 - 按顺序 必填
maskObject = null, // 用于裁剪的对象 - 可选
trimWhitespace = true, // 是否裁剪空白区域
trimPadding = 0, // 裁剪边距
quality = 1.0, // 图像质量
format = "png", // 图像格式
scaleFactor = 2, // 高清倍数 - 默认是画布的高清倍数
isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象
isThumbnail = false, // 是否为缩略图
preserveOriginalQuality = true, // 是否保持原始质量
isGroupWithMask = false, // 是否为带遮罩的组图层
} = {}) => {
try {
// console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象${maskObject ? "(带遮罩)" : ""}`);
// 确保有对象需要栅格化
if (fabricObjects.length === 0) {
console.warn("⚠️ 没有对象需要栅格化,返回空图像");
return null;
}
// 高清倍数
const currentZoom = canvas.getZoom?.() || 1;
scaleFactor = Math.max(scaleFactor || canvas?.getRetinaScaling?.(), currentZoom);
if (isThumbnail) scaleFactor = 0.2; // 缩略图使用较小的高清倍数
// console.log(`高清倍数: ${scaleFactor}, 当前缩放: ${currentZoom}`);
// 如果有遮罩且保持原始质量,使用高质量的遮罩处理方法
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) {
// 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,
},
});
// console.log(`✅ 带遮罩的栅格化图像创建完成`);
return rasterizedImage;
}
// 使用原有的组对象方式创建栅格化图像
const rasterizedImage = await createRasterizedImageWithGroup({
canvas,
objects: fabricObjects,
scaleFactor,
maskObject,
trimWhitespace,
trimPadding,
quality,
format,
currentZoom,
isReturenDataURL,
});
if (!rasterizedImage) {
console.warn("⚠️ 栅格化图像创建失败,返回空图像");
return null;
}
if (isReturenDataURL) {
// console.log("✅ 栅格化图像创建成功返回DataURL");
return rasterizedImage;
}
// 设置栅格化图像的属性
if (rasterizedImage) {
rasterizedImage.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
custom: {
type: "rasterized",
rasterizedAt: new Date().toISOString(),
objectCount: fabricObjects.length,
originalZoom: currentZoom,
},
});
// console.log(`✅ 栅格化图像创建完成`);
}
return rasterizedImage;
} catch (error) {
console.error("创建栅格化图像失败:", error);
throw new Error(`栅格化失败: ${error.message}`);
}
};
/**
* 使用组对象方式创建栅格化图像
* @param {Object} options 渲染选项
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
*/
const createRasterizedImageWithGroup = async ({
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);
}
// 创建组对象
const group = new fabric.Group(clonedObjects, {
originX: "left",
originY: "top",
});
// 获取组的绝对边界框
const groupBounds = group.getBoundingRect(true, true);
// console.log("📏 组边界框:", groupBounds);
// 设置离屏画布尺寸,使用组的边界大小
const canvasWidth = Math.ceil(groupBounds.width * scaleFactor);
const canvasHeight = Math.ceil(groupBounds.height * scaleFactor);
offscreenCanvas.setDimensions({
width: canvasWidth,
height: canvasHeight,
hasControls: false,
});
// console.log(`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`);
// 调整组的位置,让它位于画布的左上角
group.set({
left: 0,
top: 0,
});
// 取消对象激活
group.set({
selectable: false, // 禁用组的选择
evented: false, // 禁用组的事件
});
// 将组添加到离屏画布
offscreenCanvas.add(group);
// 设置离屏画布的缩放
offscreenCanvas.setZoom(scaleFactor);
// 如果有遮罩对象,应用遮罩
if (maskObject) {
await applyMaskToCanvas(offscreenCanvas, maskObject, groupBounds);
}
// 渲染离屏画布
offscreenCanvas.renderAll();
// 生成图像数据
const dataURL = offscreenCanvas.toDataURL({
format,
quality,
multiplier: 1, // 已经通过画布尺寸处理了高清倍数
});
if (isReturenDataURL) {
// 清理离屏画布
offscreenCanvas.dispose();
return dataURL; // 如果需要返回DataURL
}
// 清理离屏画布
offscreenCanvas.dispose();
// 创建fabric.Image对象
const fabricImage = await createFabricImageFromDataURL(dataURL);
// 设置图像的位置和缩放,使其与原始组的位置和大小匹配
fabricImage.set({
scaleX: 1 / scaleFactor, // 由于我们生成的图像是高清版本,需要缩放回原始大小
scaleY: 1 / scaleFactor,
left: groupBounds.left + groupBounds.width / 2, // 设置为组中心点
top: groupBounds.top + groupBounds.height / 2, // 设置为组中心点
originX: "center",
originY: "center",
});
return fabricImage;
} catch (error) {
console.error("组对象栅格化失败:", error);
throw error;
}
};
/**
* 使用遮罩创建栅格化图像 - 专门处理带遮罩的组图层
* 基于遮罩的位置和大小来裁剪内容,确保正确的定位
* @param {Object} options 渲染选项
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
*/
const createRasterizedImageWithMask = async ({
canvas,
objects,
maskObject,
scaleFactor,
quality,
format,
currentZoom,
isReturenDataURL,
isGroupWithMask,
}) => {
try {
// console.log("🎭 使用遮罩创建栅格化图像");
// 获取遮罩的边界框,这将作为最终图像的边界
const maskBounds = maskObject.getBoundingRect(true, true);
// 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,
});
// 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();
// console.log("✅ 带遮罩的栅格化图像创建完成");
return fabricImage;
} catch (error) {
console.error("带遮罩的栅格化失败:", error);
throw error;
}
};
/**
* 异步克隆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}`));
}
});
};
/**
* 从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) {
// console.log("没有遮罩对象,跳过遮罩应用");
return;
}
try {
// 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;
// console.log("✅ 遮罩应用完成");
} catch (error) {
console.error("应用遮罩失败:", error);
}
};
/**
* 获取对象组的边界框
* @param {Array} fabricObjects fabric对象数组
* @returns {Object} 边界框信息
*/
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;
};