fix: 修复多个已知问题

This commit is contained in:
bighuixiang
2025-06-29 23:29:47 +08:00
parent 6fc2a8fc57
commit 4a95f27966
41 changed files with 2266 additions and 351 deletions

View File

@@ -19,6 +19,7 @@ export const createRasterizedImage = async ({
scaleFactor = 1, // 高清倍数 - 默认是画布的高清倍数
isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象
preserveOriginalQuality = true, // 是否保持原始质量(新增)
selectionManager = null, // 选区管理器,用于获取羽化值等设置
} = {}) => {
try {
console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象`);
@@ -38,6 +39,7 @@ export const createRasterizedImage = async ({
fabricObjects,
clippingObject,
isReturenDataURL,
selectionManager, // 传递选区管理器
});
}
@@ -77,20 +79,36 @@ const createClippedObjects = async ({
fabricObjects,
clippingObject,
isReturenDataURL,
selectionManager = null, // 新增选区管理器参数
}) => {
try {
console.log("🎯 使用新的裁剪方法创建对象");
console.log("🎯 使用新的图像遮罩裁剪方法创建对象");
// 使用优化后的边界计算,确保包含描边区域
const optimizedBounds = calculateOptimizedBounds(
clippingObject,
fabricObjects
);
console.log("📐 优化后的选区边界框:", optimizedBounds);
// 获取羽化值
let featherAmount = 0;
if (
selectionManager &&
typeof selectionManager.getFeatherAmount === "function"
) {
featherAmount = selectionManager.getFeatherAmount();
console.log(`🌟 应用羽化效果: ${featherAmount}px`);
}
// 获取选区边界框
const selectionBounds = clippingObject.getBoundingRect(true);
console.log("📐 选区边界框:", selectionBounds);
// 方法1如果只需要返回DataURL使用画布裁剪方法
if (isReturenDataURL) {
return await createClippedDataURLByCanvas({
canvas,
fabricObjects,
clippingObject,
selectionBounds,
selectionBounds: optimizedBounds, // 使用优化后的边界框
featherAmount,
});
}
@@ -99,40 +117,44 @@ const createClippedObjects = async ({
canvas,
fabricObjects,
clippingObject,
selectionBounds,
selectionBounds: optimizedBounds, // 使用优化后的边界框
featherAmount,
});
// 将DataURL转换为fabric.Image对象
const fabricImage = await createFabricImageFromDataURL(clippedDataURL);
// 使用fabric原生方法恢复到选区的原始大小和位置
fabricImage.scaleToWidth(selectionBounds.width);
fabricImage.scaleToHeight(selectionBounds.height);
fabricImage.scaleToWidth(optimizedBounds.width);
fabricImage.scaleToHeight(optimizedBounds.height);
// 设置到选区的原始位置(中心点)
fabricImage.set({
left: selectionBounds.left + selectionBounds.width / 2,
top: selectionBounds.top + selectionBounds.height / 2,
left: optimizedBounds.left + optimizedBounds.width / 2,
top: optimizedBounds.top + optimizedBounds.height / 2,
originX: "center",
originY: "center",
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
// hasControls: true,
// hasBorders: true,
custom: {
type: "clipped",
clippedAt: new Date().toISOString(),
hasClipping: true,
preservedQuality: true,
originalBounds: selectionBounds,
originalBounds: optimizedBounds, // 保存优化后的边界框
restoredToOriginalSize: true,
usedImageMask: true, // 标记使用了图像遮罩
featherAmount: featherAmount,
boundaryOptimized: true, // 标记使用了边界优化
},
});
// 更新坐标
fabricImage.setCoords();
console.log("✅ 返回裁剪后的fabric对象已恢复到原始大小和位置");
console.log("✅ 返回裁剪后的fabric对象已恢复到优化后的原始大小和位置");
return fabricImage;
} catch (error) {
console.error("创建裁剪对象失败:", error);
@@ -149,78 +171,66 @@ const createClippedDataURLByCanvas = async ({
fabricObjects,
clippingObject,
selectionBounds,
featherAmount = 0,
}) => {
try {
console.log("🖼️ 使用画布裁剪方法生成DataURL");
console.log("🖼️ 使用图像遮罩裁剪方法生成DataURL");
// 创建临时画布,尺寸与选区完全一致
const tempCanvas = new fabric.StaticCanvas();
// 使用优化后的边界计算,确保包含描边区域
const optimizedBounds = calculateOptimizedBounds(
clippingObject,
fabricObjects
);
// 使用高分辨率以保证质量
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,
});
const canvasWidth = Math.ceil(optimizedBounds.width * qualityMultiplier);
const canvasHeight = Math.ceil(optimizedBounds.height * qualityMultiplier);
console.log(
`📏 临时画布尺寸: ${canvasWidth}x${canvasHeight} (质量倍数: ${qualityMultiplier})`
`📏 优化后画布尺寸: ${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,
console.log("🎯 边界框对比:", {
original: selectionBounds,
optimized: optimizedBounds,
});
// 为整个画布设置裁剪路径
tempCanvas.clipPath = clipPath;
// 步骤1: 先将路径转换为遮罩图像(支持羽化)
const maskImageDataURL =
featherAmount > 0
? await createAdvancedMaskImage({
clippingObject,
selectionBounds: optimizedBounds, // 使用优化后的边界框
qualityMultiplier,
featherAmount,
})
: await createMaskImageFromPath({
clippingObject,
selectionBounds: optimizedBounds, // 使用优化后的边界框
qualityMultiplier,
});
// 渲染画布
tempCanvas.renderAll();
// 生成高质量DataURL
const dataURL = tempCanvas.toDataURL({
format: "png",
quality: 1.0,
multiplier: 1, // 已经通过尺寸处理了缩放
// 步骤2: 渲染原始内容
const contentImageDataURL = await renderContentToImage({
fabricObjects,
selectionBounds: optimizedBounds, // 使用优化后的边界框
qualityMultiplier,
});
// 清理临时画布
tempCanvas.dispose();
// 步骤3: 使用遮罩合成最终结果
const clippedDataURL = await applyImageMask({
contentImageDataURL,
maskImageDataURL,
canvasWidth,
canvasHeight,
});
console.log("✅ 画布裁剪完成生成DataURL");
return dataURL;
console.log("✅ 图像遮罩裁剪完成生成DataURL");
return clippedDataURL;
} catch (error) {
console.error("画布裁剪失败:", error);
console.error("图像遮罩裁剪失败:", error);
throw error;
}
};
@@ -731,3 +741,477 @@ export const getObjectsBounds = (fabricObjects) => {
const { absoluteBounds } = calculateBounds(fabricObjects);
return absoluteBounds;
};
/**
* 将路径对象转换为遮罩图像
* @param {Object} clippingObject 裁剪路径对象
* @param {Object} selectionBounds 选区边界框
* @param {Number} qualityMultiplier 质量倍数
* @returns {Promise<String>} 遮罩图像的DataURL
*/
const createMaskImageFromPath = async ({
clippingObject,
selectionBounds,
qualityMultiplier,
}) => {
try {
console.log("🎭 创建路径遮罩图像");
// 创建专门用于渲染遮罩的画布
const maskCanvas = new fabric.StaticCanvas();
const canvasWidth = Math.ceil(selectionBounds.width * qualityMultiplier);
const canvasHeight = Math.ceil(selectionBounds.height * qualityMultiplier);
maskCanvas.setDimensions({
width: canvasWidth,
height: canvasHeight,
});
// 克隆路径对象并处理描边转填充
const maskPath = await createSolidMaskPath(
clippingObject,
selectionBounds,
qualityMultiplier
);
// 添加路径到遮罩画布
maskCanvas.add(maskPath);
maskCanvas.renderAll();
// 生成遮罩图像
const maskDataURL = maskCanvas.toDataURL({
format: "png",
quality: 1.0,
multiplier: 1,
});
// 清理遮罩画布
maskCanvas.dispose();
console.log("✅ 遮罩图像创建完成");
return maskDataURL;
} catch (error) {
console.error("创建遮罩图像失败:", error);
throw error;
}
};
/**
* 渲染内容对象为图像
* @param {Array} fabricObjects 要渲染的对象数组
* @param {Object} selectionBounds 选区边界框
* @param {Number} qualityMultiplier 质量倍数
* @returns {Promise<String>} 内容图像的DataURL
*/
const renderContentToImage = async ({
fabricObjects,
selectionBounds,
qualityMultiplier,
}) => {
try {
console.log("🖼️ 渲染内容图像");
// 创建内容渲染画布
const contentCanvas = new fabric.StaticCanvas();
const canvasWidth = Math.ceil(selectionBounds.width * qualityMultiplier);
const canvasHeight = Math.ceil(selectionBounds.height * qualityMultiplier);
contentCanvas.setDimensions({
width: canvasWidth,
height: canvasHeight,
});
// 克隆并添加所有需要渲染的对象
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,
selectable: false,
evented: false,
});
contentCanvas.add(clonedObj);
}
// 渲染内容画布
contentCanvas.renderAll();
// 生成内容图像
const contentDataURL = contentCanvas.toDataURL({
format: "png",
quality: 1.0,
multiplier: 1,
});
// 清理内容画布
contentCanvas.dispose();
console.log("✅ 内容图像渲染完成");
return contentDataURL;
} catch (error) {
console.error("渲染内容图像失败:", error);
throw error;
}
};
/**
* 使用遮罩图像对内容图像进行裁剪
* @param {String} contentImageDataURL 内容图像DataURL
* @param {String} maskImageDataURL 遮罩图像DataURL
* @param {Number} canvasWidth 画布宽度
* @param {Number} canvasHeight 画布高度
* @returns {Promise<String>} 裁剪后的图像DataURL
*/
const applyImageMask = async ({
contentImageDataURL,
maskImageDataURL,
canvasWidth,
canvasHeight,
}) => {
try {
console.log("🎯 应用图像遮罩");
return new Promise((resolve, reject) => {
// 创建用于合成的Canvas元素
const compositeCanvas = document.createElement("canvas");
const ctx = compositeCanvas.getContext("2d");
compositeCanvas.width = canvasWidth;
compositeCanvas.height = canvasHeight;
// 加载内容图像
const contentImg = new Image();
contentImg.onload = () => {
// 加载遮罩图像
const maskImg = new Image();
maskImg.onload = () => {
try {
// 先绘制内容图像
ctx.drawImage(contentImg, 0, 0, canvasWidth, canvasHeight);
// 设置合成模式为遮罩模式
ctx.globalCompositeOperation = "destination-in";
// 绘制遮罩图像
ctx.drawImage(maskImg, 0, 0, canvasWidth, canvasHeight);
// 重置合成模式
ctx.globalCompositeOperation = "source-over";
// 获取最终结果
const resultDataURL = compositeCanvas.toDataURL("image/png", 1.0);
console.log("✅ 图像遮罩应用完成");
resolve(resultDataURL);
} catch (error) {
console.error("合成图像失败:", error);
reject(error);
}
};
maskImg.onerror = () => {
reject(new Error("加载遮罩图像失败"));
};
maskImg.src = maskImageDataURL;
};
contentImg.onerror = () => {
reject(new Error("加载内容图像失败"));
};
contentImg.src = contentImageDataURL;
});
} catch (error) {
console.error("应用图像遮罩失败:", error);
throw error;
}
};
/**
* 创建带羽化效果的遮罩图像(高级版本)
* @param {Object} clippingObject 裁剪路径对象
* @param {Object} selectionBounds 选区边界框
* @param {Number} qualityMultiplier 质量倍数
* @param {Number} featherAmount 羽化值
* @returns {Promise<String>} 遮罩图像的DataURL
*/
const createAdvancedMaskImage = async ({
clippingObject,
selectionBounds,
qualityMultiplier,
featherAmount = 0,
}) => {
try {
console.log(`🎭 创建高级遮罩图像 (羽化: ${featherAmount})`);
// 创建专门用于渲染遮罩的画布
const maskCanvas = new fabric.StaticCanvas();
const canvasWidth = Math.ceil(selectionBounds.width * qualityMultiplier);
const canvasHeight = Math.ceil(selectionBounds.height * qualityMultiplier);
maskCanvas.setDimensions({
width: canvasWidth,
height: canvasHeight,
});
// 克隆路径对象并处理描边转填充
const maskPath = await createSolidMaskPath(
clippingObject,
selectionBounds,
qualityMultiplier
);
// 如果有羽化值,添加模糊效果
if (featherAmount > 0) {
const adjustedFeather = featherAmount * qualityMultiplier;
maskPath.shadow = new fabric.Shadow({
color: "#ffffff",
blur: adjustedFeather,
offsetX: 0,
offsetY: 0,
});
}
// 添加路径到遮罩画布
maskCanvas.add(maskPath);
maskCanvas.renderAll();
// 如果有羽化,需要进行后处理
if (featherAmount > 0) {
return await applyCanvasBlur(
maskCanvas,
featherAmount * qualityMultiplier
);
}
// 生成遮罩图像
const maskDataURL = maskCanvas.toDataURL({
format: "png",
quality: 1.0,
multiplier: 1,
});
// 清理遮罩画布
maskCanvas.dispose();
console.log("✅ 高级遮罩图像创建完成");
return maskDataURL;
} catch (error) {
console.error("创建高级遮罩图像失败:", error);
throw error;
}
};
/**
* 对画布应用模糊效果
* @param {fabric.StaticCanvas} canvas 要处理的画布
* @param {Number} blurAmount 模糊值
* @returns {Promise<String>} 处理后的DataURL
*/
const applyCanvasBlur = async (canvas, blurAmount) => {
try {
// 获取原始图像数据
const originalDataURL = canvas.toDataURL({
format: "png",
quality: 1.0,
multiplier: 1,
});
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
// 创建一个新的Canvas进行模糊处理
const blurCanvas = document.createElement("canvas");
const ctx = blurCanvas.getContext("2d");
blurCanvas.width = canvas.width;
blurCanvas.height = canvas.height;
// 应用CSS滤镜模糊
ctx.filter = `blur(${Math.max(1, blurAmount / 2)}px)`;
ctx.drawImage(img, 0, 0);
// 重置滤镜
ctx.filter = "none";
const blurredDataURL = blurCanvas.toDataURL("image/png", 1.0);
resolve(blurredDataURL);
};
img.onerror = () => {
reject(new Error("处理模糊效果失败"));
};
img.src = originalDataURL;
});
} catch (error) {
console.error("应用画布模糊失败:", error);
throw error;
}
};
/**
* 创建实体遮罩路径(将描边转换为填充)
* @param {Object} clippingObject 原始裁剪对象
* @param {Object} selectionBounds 选区边界框
* @param {Number} qualityMultiplier 质量倍数
* @returns {Promise<fabric.Object>} 处理后的遮罩路径对象
*/
const createSolidMaskPath = async (
clippingObject,
selectionBounds,
qualityMultiplier
) => {
try {
console.log("🔧 创建实体遮罩路径,处理描边转填充");
// 克隆原始对象
const maskPath = await cloneObjectAsync(clippingObject);
// 检查是否有描边需要处理
const hasStroke = maskPath.stroke && maskPath.strokeWidth > 0;
if (hasStroke) {
console.log(
`📏 检测到描边: ${maskPath.stroke}, 宽度: ${maskPath.strokeWidth}`
);
// 对于有描边的路径,我们需要更精确的处理
const strokeWidth = maskPath.strokeWidth;
// 方法1: 如果是简单的几何形状(矩形、圆形等),可以通过调整尺寸来补偿描边
if (
maskPath.type === "rect" ||
maskPath.type === "circle" ||
maskPath.type === "ellipse"
) {
// 对于矩形和椭圆,增加宽高来包含描边
const strokeOffset = strokeWidth;
maskPath.set({
left:
(maskPath.left - selectionBounds.left - strokeOffset / 2) *
qualityMultiplier,
top:
(maskPath.top - selectionBounds.top - strokeOffset / 2) *
qualityMultiplier,
scaleX: (maskPath.scaleX || 1) * qualityMultiplier,
scaleY: (maskPath.scaleY || 1) * qualityMultiplier,
width: (maskPath.width || 0) + strokeOffset,
height: (maskPath.height || 0) + strokeOffset,
fill: "#ffffff",
stroke: "",
strokeWidth: 0,
selectable: false,
evented: false,
});
} else {
// 对于复杂路径,使用缩放方式来近似包含描边区域
const pathBounds = maskPath.getBoundingRect(true, true);
const minDimension = Math.min(pathBounds.width, pathBounds.height);
const expandRatio = 1 + (strokeWidth * 2) / minDimension;
const strokeOffset = strokeWidth / 2;
maskPath.set({
left:
(maskPath.left - selectionBounds.left - strokeOffset) *
qualityMultiplier,
top:
(maskPath.top - selectionBounds.top - strokeOffset) *
qualityMultiplier,
scaleX: (maskPath.scaleX || 1) * qualityMultiplier * expandRatio,
scaleY: (maskPath.scaleY || 1) * qualityMultiplier * expandRatio,
fill: "#ffffff",
stroke: "",
strokeWidth: 0,
selectable: false,
evented: false,
});
}
console.log(`✅ 描边已转换为填充,类型: ${maskPath.type}`);
} else {
// 没有描边,直接处理位置和缩放
maskPath.set({
left: (maskPath.left - selectionBounds.left) * qualityMultiplier,
top: (maskPath.top - selectionBounds.top) * qualityMultiplier,
scaleX: (maskPath.scaleX || 1) * qualityMultiplier,
scaleY: (maskPath.scaleY || 1) * qualityMultiplier,
fill: "#ffffff", // 白色表示可见区域
stroke: "", // 确保没有描边
strokeWidth: 0,
selectable: false,
evented: false,
});
}
// 确保对象在画布中心正确对齐
maskPath.setCoords();
return maskPath;
} catch (error) {
console.error("创建实体遮罩路径失败:", error);
throw error;
}
};
/**
* 优化边界计算,确保遮罩和内容对齐
* @param {Object} clippingObject 裁剪对象
* @param {Array} fabricObjects 内容对象数组
* @returns {Object} 优化后的边界框信息
*/
const calculateOptimizedBounds = (clippingObject, fabricObjects) => {
try {
console.log("📐 计算优化后的边界框");
// 获取裁剪对象的边界框(包含描边)
const clippingBounds = clippingObject.getBoundingRect(true, true);
// 如果有描边,需要调整边界框
if (clippingObject.stroke && clippingObject.strokeWidth > 0) {
const strokeWidth = clippingObject.strokeWidth;
const halfStroke = strokeWidth / 2;
// 扩展边界框以包含完整的描边区域
clippingBounds.left -= halfStroke;
clippingBounds.top -= halfStroke;
clippingBounds.width += strokeWidth;
clippingBounds.height += strokeWidth;
console.log(`🖊️ 调整描边边界框,描边宽度: ${strokeWidth}`);
}
// 计算内容对象的边界框
const contentBounds = calculateBounds(fabricObjects);
// 使用裁剪边界框作为最终的选区边界框
const optimizedBounds = {
...clippingBounds,
// 确保边界框不为负数或零
width: Math.max(1, clippingBounds.width),
height: Math.max(1, clippingBounds.height),
};
console.log("✅ 边界框优化完成", {
original: clippingObject.getBoundingRect(true, true),
optimized: optimizedBounds,
hasStroke: !!(clippingObject.stroke && clippingObject.strokeWidth > 0),
});
return optimizedBounds;
} catch (error) {
console.error("计算优化边界框失败:", error);
// 返回原始计算方式作为备选
return clippingObject.getBoundingRect(true, true);
}
};