// 栅格化帮助 import { fabric } from "fabric-with-all"; /** * 创建栅格化图像 - 重构版本 * 采用复制原对象+裁剪路径的方式,保持原始质量和准确位置 * @returns {Promise} 栅格化后的图像对象或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} 栅格化后的图像对象 */ 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} 克隆的对象 */ 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图像对象 */ 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; };