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

734 lines
20 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";
/**
* 创建栅格化图像 - 重构版本
* 采用复制原对象+裁剪路径的方式,保持原始质量和准确位置
* @returns {Promise<fabric.Image|fabric.Group|string>} 栅格化后的图像对象或DataURL
* @private
*/
export const createRasterizedImage = async ({
canvas, // 画布对象 必填
fabricObjects = [], // 要栅格化的对象列表 - 按顺序 必填
maskObject = null, // 用于裁剪的对象 - 可选
clipPath = null, // 裁剪路径对象 - 可选优先级高于maskObject
trimWhitespace = true, // 是否裁剪空白区域
trimPadding = 0, // 裁剪边距
quality = 1.0, // 图像质量
format = "png", // 图像格式
scaleFactor = 1, // 高清倍数 - 默认是画布的高清倍数
isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象
preserveOriginalQuality = true, // 是否保持原始质量(新增)
} = {}) => {
try {
console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象`);
// 确保有对象需要栅格化
if (fabricObjects.length === 0) {
console.warn("⚠️ 没有对象需要栅格化,返回空图像");
return null;
}
// 处理裁剪对象优先使用clipPath
const clippingObject = clipPath || maskObject;
// 如果保持原始质量且有裁剪对象,使用新的裁剪方法
if (preserveOriginalQuality && clippingObject) {
return await createClippedObjects({
canvas,
fabricObjects,
clippingObject,
isReturenDataURL,
});
}
// 如果只是简单复制而不需要裁剪,直接克隆对象
if (!clippingObject) {
return await createSimpleClone({
canvas,
fabricObjects,
isReturenDataURL,
quality,
format,
});
}
// 兼容原有的离屏渲染方法(作为备选方案)
return await createLegacyRasterization({
canvas,
fabricObjects,
clippingObject,
scaleFactor,
quality,
format,
isReturenDataURL,
});
} catch (error) {
console.error("创建栅格化图像失败:", error);
throw new Error(`栅格化失败: ${error.message}`);
}
};
/**
* 创建带裁剪的对象 - 新方法
* 直接复制原对象并应用裁剪路径,保持原始质量
*/
const createClippedObjects = async ({
canvas,
fabricObjects,
clippingObject,
isReturenDataURL,
}) => {
try {
console.log("🎯 使用新的裁剪方法创建对象");
// 获取选区边界框
const selectionBounds = clippingObject.getBoundingRect(true);
console.log("📐 选区边界框:", selectionBounds);
// 方法1如果只需要返回DataURL使用画布裁剪方法
if (isReturenDataURL) {
return await createClippedDataURLByCanvas({
canvas,
fabricObjects,
clippingObject,
selectionBounds,
});
}
// 方法2如果需要返回fabric对象先生成DataURL再转换为fabric对象
const clippedDataURL = await createClippedDataURLByCanvas({
canvas,
fabricObjects,
clippingObject,
selectionBounds,
});
// 将DataURL转换为fabric.Image对象
const fabricImage = await createFabricImageFromDataURL(clippedDataURL);
// 使用fabric原生方法恢复到选区的原始大小和位置
fabricImage.scaleToWidth(selectionBounds.width);
fabricImage.scaleToHeight(selectionBounds.height);
// 设置到选区的原始位置(中心点)
fabricImage.set({
left: selectionBounds.left + selectionBounds.width / 2,
top: selectionBounds.top + selectionBounds.height / 2,
originX: "center",
originY: "center",
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
custom: {
type: "clipped",
clippedAt: new Date().toISOString(),
hasClipping: true,
preservedQuality: true,
originalBounds: selectionBounds,
restoredToOriginalSize: true,
},
});
// 更新坐标
fabricImage.setCoords();
console.log("✅ 返回裁剪后的fabric对象已恢复到原始大小和位置");
return fabricImage;
} catch (error) {
console.error("创建裁剪对象失败:", error);
throw error;
}
};
/**
* 通过画布裁剪生成DataURL
* 裁剪掉选区以外的内容,保持和选区大小一致
*/
const createClippedDataURLByCanvas = async ({
canvas,
fabricObjects,
clippingObject,
selectionBounds,
}) => {
try {
console.log("🖼️ 使用画布裁剪方法生成DataURL");
// 创建临时画布,尺寸与选区完全一致
const tempCanvas = new fabric.StaticCanvas();
// 使用高分辨率以保证质量
const pixelRatio = window.devicePixelRatio || 1;
const qualityMultiplier = Math.max(2, pixelRatio);
const canvasWidth = Math.ceil(selectionBounds.width * qualityMultiplier);
const canvasHeight = Math.ceil(selectionBounds.height * qualityMultiplier);
tempCanvas.setDimensions({
width: canvasWidth,
height: canvasHeight,
});
console.log(
`📏 临时画布尺寸: ${canvasWidth}x${canvasHeight} (质量倍数: ${qualityMultiplier})`
);
// 克隆并添加所有需要裁剪的对象
for (const obj of fabricObjects) {
const clonedObj = await cloneObjectAsync(obj);
// 调整对象位置:将选区左上角作为新的原点(0,0)
// 同时应用质量倍数缩放
clonedObj.set({
left: (clonedObj.left - selectionBounds.left) * qualityMultiplier,
top: (clonedObj.top - selectionBounds.top) * qualityMultiplier,
scaleX: (clonedObj.scaleX || 1) * qualityMultiplier,
scaleY: (clonedObj.scaleY || 1) * qualityMultiplier,
});
tempCanvas.add(clonedObj);
}
// 克隆裁剪路径并调整位置
const clipPath = await cloneObjectAsync(clippingObject);
clipPath.set({
left: (clipPath.left - selectionBounds.left) * qualityMultiplier,
top: (clipPath.top - selectionBounds.top) * qualityMultiplier,
scaleX: (clipPath.scaleX || 1) * qualityMultiplier,
scaleY: (clipPath.scaleY || 1) * qualityMultiplier,
fill: "transparent",
stroke: "",
strokeWidth: 0,
absolutePositioned: true,
});
// 为整个画布设置裁剪路径
tempCanvas.clipPath = clipPath;
// 渲染画布
tempCanvas.renderAll();
// 生成高质量DataURL
const dataURL = tempCanvas.toDataURL({
format: "png",
quality: 1.0,
multiplier: 1, // 已经通过尺寸处理了缩放
});
// 清理临时画布
tempCanvas.dispose();
console.log("✅ 画布裁剪完成生成DataURL");
return dataURL;
} catch (error) {
console.error("画布裁剪失败:", error);
throw error;
}
};
/**
* 创建简单克隆对象
* 当不需要裁剪时,直接克隆原对象
*/
const createSimpleClone = async ({
canvas,
fabricObjects,
isReturenDataURL,
quality,
format,
}) => {
try {
console.log("📋 创建简单克隆对象");
const clonedObjects = [];
// 克隆所有对象
for (const obj of fabricObjects) {
const clonedObj = await cloneObjectAsync(obj);
clonedObj.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
custom: {
...clonedObj.custom,
type: "cloned",
clonedAt: new Date().toISOString(),
preservedQuality: true,
},
});
clonedObjects.push(clonedObj);
}
// 如果需要返回DataURL需要渲染
if (isReturenDataURL) {
return await renderObjectsToDataURL(clonedObjects, quality, format);
}
// 如果只有一个对象,直接返回
if (clonedObjects.length === 1) {
return clonedObjects[0];
}
// 创建组合
const group = new fabric.Group(clonedObjects, {
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
custom: {
type: "clonedGroup",
clonedAt: new Date().toISOString(),
objectCount: clonedObjects.length,
preservedQuality: true,
},
});
return group;
} catch (error) {
console.error("创建简单克隆失败:", error);
throw error;
}
};
/**
* 将对象渲染为DataURL
*/
const renderObjectsToDataURL = async (objects, quality, format) => {
try {
// 计算对象边界框
const bounds = calculateBounds(objects);
if (!bounds.absoluteBounds) {
throw new Error("无法计算对象边界框");
}
// 创建临时画布用于渲染
const tempCanvas = new fabric.StaticCanvas();
const { absoluteBounds } = bounds;
tempCanvas.setDimensions({
width: Math.ceil(absoluteBounds.width),
height: Math.ceil(absoluteBounds.height),
});
// 调整对象位置并添加到临时画布
for (const obj of objects) {
const tempObj = await cloneObjectAsync(obj);
tempObj.set({
left: tempObj.left - absoluteBounds.left,
top: tempObj.top - absoluteBounds.top,
});
tempCanvas.add(tempObj);
}
// 渲染并获取DataURL
tempCanvas.renderAll();
const dataURL = tempCanvas.toDataURL({
format,
quality,
});
// 清理临时画布
tempCanvas.dispose();
return dataURL;
} catch (error) {
console.error("渲染对象为DataURL失败:", error);
throw error;
}
};
/**
* 渲染裁剪后的对象为DataURL
* 专门处理带有裁剪路径的对象渲染(备用方法)
*/
const renderClippedObjectsToDataURL = async (clippedObjects) => {
try {
console.log("🖼️ 渲染裁剪对象为DataURL");
// 计算所有裁剪对象的总边界框
let totalBounds = null;
for (const obj of clippedObjects) {
const objBounds = obj.getBoundingRect(true, true);
if (!totalBounds) {
totalBounds = { ...objBounds };
} else {
const right = Math.max(
totalBounds.left + totalBounds.width,
objBounds.left + objBounds.width
);
const bottom = Math.max(
totalBounds.top + totalBounds.height,
objBounds.top + objBounds.height
);
totalBounds.left = Math.min(totalBounds.left, objBounds.left);
totalBounds.top = Math.min(totalBounds.top, objBounds.top);
totalBounds.width = right - totalBounds.left;
totalBounds.height = bottom - totalBounds.top;
}
}
if (!totalBounds) {
throw new Error("无法计算对象边界框");
}
// 创建临时画布,使用高分辨率
const tempCanvas = new fabric.StaticCanvas();
const pixelRatio = window.devicePixelRatio || 1;
const scaleFactor = Math.max(2, pixelRatio);
const canvasWidth = Math.ceil(totalBounds.width * scaleFactor);
const canvasHeight = Math.ceil(totalBounds.height * scaleFactor);
tempCanvas.setDimensions({
width: canvasWidth,
height: canvasHeight,
});
// 调整对象位置并添加到临时画布
for (const obj of clippedObjects) {
const tempObj = await cloneObjectAsync(obj);
// 调整位置到画布坐标系
tempObj.set({
left: (tempObj.left - totalBounds.left) * scaleFactor,
top: (tempObj.top - totalBounds.top) * scaleFactor,
scaleX: (tempObj.scaleX || 1) * scaleFactor,
scaleY: (tempObj.scaleY || 1) * scaleFactor,
});
// 如果有裁剪路径,也需要调整裁剪路径
if (tempObj.clipPath) {
tempObj.clipPath.set({
scaleX: (tempObj.clipPath.scaleX || 1) * scaleFactor,
scaleY: (tempObj.clipPath.scaleY || 1) * scaleFactor,
});
}
tempCanvas.add(tempObj);
}
// 渲染画布
tempCanvas.renderAll();
// 获取DataURL
const dataURL = tempCanvas.toDataURL({
format: "png",
quality: 1.0,
multiplier: 1, // 已经通过尺寸处理了缩放
});
// 清理临时画布
tempCanvas.dispose();
console.log("✅ 裁剪对象渲染完成");
return dataURL;
} catch (error) {
console.error("渲染裁剪对象失败:", error);
throw error;
}
};
/**
* 兼容的离屏渲染方法(原有逻辑,作为备选)
*/
const createLegacyRasterization = async ({
canvas,
fabricObjects,
clippingObject,
scaleFactor,
quality,
format,
isReturenDataURL,
}) => {
console.log("⚠️ 使用兼容的离屏渲染方法");
// 这里保留原有的离屏渲染逻辑作为备选方案
const currentZoom = canvas.getZoom?.() || 1;
scaleFactor = Math.max(
scaleFactor || canvas?.getRetinaScaling?.(),
currentZoom
);
scaleFactor = Math.min(scaleFactor, 3);
const { absoluteBounds, relativeBounds } = calculateBounds(fabricObjects);
return await createOffscreenRasterization({
canvas,
objects: fabricObjects,
absoluteBounds,
relativeBounds,
scaleFactor,
clippingObject,
trimWhitespace: true,
trimPadding: 1,
quality,
format,
currentZoom,
isReturenDataURL,
});
};
/**
* 计算对象的绝对边界框和相对边界框
* @param {Array} fabricObjects fabric对象数组
* @returns {Object} 包含绝对边界框和相对边界框的对象
*/
const calculateBounds = (fabricObjects) => {
if (fabricObjects.length === 0) {
console.warn("⚠️ 没有对象,无法计算边界框");
return { absoluteBounds: null, relativeBounds: null };
}
let absoluteBounds = null;
let relativeBounds = null;
fabricObjects.forEach((obj, index) => {
// 获取相对边界框(考虑画布缩放和平移)
const relativeBound = obj.getBoundingRect();
// 获取绝对边界框(原始大小和位置)
const absoluteBound = obj.getBoundingRect(true, true);
console.log(`对象 ${obj.id || index} 边界框比较:`, {
relative: relativeBound,
absolute: absoluteBound,
scaleX: obj.scaleX,
scaleY: obj.scaleY,
});
// 计算绝对边界框的累积范围
if (!absoluteBounds) {
absoluteBounds = { ...absoluteBound };
} else {
const right = Math.max(
absoluteBounds.left + absoluteBounds.width,
absoluteBound.left + absoluteBound.width
);
const bottom = Math.max(
absoluteBounds.top + absoluteBounds.height,
absoluteBound.top + absoluteBound.height
);
absoluteBounds.left = Math.min(absoluteBounds.left, absoluteBound.left);
absoluteBounds.top = Math.min(absoluteBounds.top, absoluteBound.top);
absoluteBounds.width = right - absoluteBounds.left;
absoluteBounds.height = bottom - absoluteBounds.top;
}
// 计算相对边界框的累积范围
if (!relativeBounds) {
relativeBounds = { ...relativeBound };
} else {
const right = Math.max(
relativeBounds.left + relativeBounds.width,
relativeBound.left + relativeBound.width
);
const bottom = Math.max(
relativeBounds.top + relativeBounds.height,
relativeBound.top + relativeBound.height
);
relativeBounds.left = Math.min(relativeBounds.left, relativeBound.left);
relativeBounds.top = Math.min(relativeBounds.top, relativeBound.top);
relativeBounds.width = right - relativeBounds.left;
relativeBounds.height = bottom - relativeBounds.top;
}
});
return { absoluteBounds, relativeBounds };
};
/**
* 创建离屏栅格化渲染
* @param {Object} options 渲染选项
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
*/
const createOffscreenRasterization = async ({
canvas,
objects,
absoluteBounds,
relativeBounds,
scaleFactor,
clippingObject,
trimWhitespace,
trimPadding,
quality,
format,
currentZoom,
isReturenDataURL,
}) => {
try {
// 创建离屏画布,使用绝对尺寸以保证高质量
const offscreenCanvas = new fabric.StaticCanvas();
// 如果有裁剪对象,使用裁剪对象的边界框
let renderBounds = absoluteBounds;
if (clippingObject) {
const clippingBounds = clippingObject.getBoundingRect(true, true);
console.log("🎯 使用裁剪对象边界框:", clippingBounds);
renderBounds = clippingBounds;
}
// 设置离屏画布尺寸,并应用高清倍数
const canvasWidth = Math.ceil(renderBounds.width);
const canvasHeight = Math.ceil(renderBounds.height);
offscreenCanvas.setDimensions({
width: canvasWidth,
height: canvasHeight,
});
console.log(
`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`
);
// 克隆对象到离屏画布
const clonedObjects = [];
for (const obj of objects) {
const clonedObj = await cloneObjectAsync(obj);
// 调整对象位置,相对于渲染边界框的左上角
clonedObj.set({
left: clonedObj.left - renderBounds.left,
top: clonedObj.top - renderBounds.top,
});
// 如果有裁剪对象,为每个对象设置裁剪路径
if (clippingObject) {
const clippingPath = await cloneObjectAsync(clippingObject);
clippingPath.set({
left: clippingPath.left - renderBounds.left,
top: clippingPath.top - renderBounds.top,
fill: "",
stroke: "",
absolutePositioned: true,
});
clonedObj.set({
clipPath: clippingPath,
});
}
clonedObjects.push(clonedObj);
offscreenCanvas.add(clonedObj);
}
// 渲染离屏画布
offscreenCanvas.renderAll();
// 生成图像数据
const dataURL = offscreenCanvas.toDataURL({
format,
quality,
multiplier: 1, // 已经通过画布尺寸处理了高清倍数
});
if (isReturenDataURL) {
return dataURL; // 如果需要返回DataURL
}
// 清理离屏画布
offscreenCanvas.dispose();
// 创建fabric.Image对象
const fabricImage = await createFabricImageFromDataURL(dataURL);
// 设置图像位置为裁剪区域的位置
fabricImage.set({
left: renderBounds.left + renderBounds.width / 2,
top: renderBounds.top + renderBounds.height / 2,
originX: "center",
originY: "center",
});
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) => {
obj.clone((cloned) => {
if (cloned) {
resolve(cloned);
} else {
reject(new Error("对象克隆失败"));
}
});
});
};
/**
* 从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 {Object} params 计算参数
* @returns {Object} 变换属性对象
*/
const calculateImageTransform = ({
absoluteBounds,
relativeBounds,
currentZoom,
scaleFactor,
imageWidth,
imageHeight,
}) => {
// 计算缩放比例:相对尺寸 / 绝对尺寸
const scaleX = relativeBounds.width / absoluteBounds.width;
const scaleY = relativeBounds.height / absoluteBounds.height;
// 由于我们生成的图像是基于绝对尺寸的高清版本需要考虑scaleFactor
const finalScaleX = scaleX / scaleFactor;
const finalScaleY = scaleY / scaleFactor;
return {
left: relativeBounds.left + relativeBounds.width / 2, // 设置为中心点
top: relativeBounds.top + relativeBounds.height / 2, // 设置为中心点
// scaleX: finalScaleX,
// scaleY: finalScaleY,
originX: "center",
originY: "center",
};
};
/**
* 应用遮罩到画布已弃用使用clipPath方式
* @param {fabric.Canvas} canvas 目标画布
* @param {fabric.Object} maskObject 遮罩对象
* @param {Object} bounds 边界框
*/
const applyMaskToCanvas = async (canvas, maskObject, bounds) => {
// 此方法已被clipPath方式替代
console.log("⚠️ applyMaskToCanvas已被clipPath方式替代");
};
export const getObjectsBounds = (fabricObjects) => {
const { absoluteBounds } = calculateBounds(fabricObjects);
return absoluteBounds;
};