解决画布打开图片导出图片分辨率不一值问题

This commit is contained in:
李志鹏
2025-09-25 13:52:05 +08:00
parent 15cb0c86e7
commit 564b75de61
7 changed files with 98 additions and 36 deletions

12
components.d.ts vendored
View File

@@ -9,31 +9,19 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
ABadge: typeof import('ant-design-vue/es')['Badge'] ABadge: typeof import('ant-design-vue/es')['Badge']
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox'] ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADrawer: typeof import('ant-design-vue/es')['Drawer'] ADrawer: typeof import('ant-design-vue/es')['Drawer']
AImage: typeof import('ant-design-vue/es')['Image']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber'] AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AModal: typeof import('ant-design-vue/es')['Modal'] AModal: typeof import('ant-design-vue/es')['Modal']
APagination: typeof import('ant-design-vue/es')['Pagination']
APopover: typeof import('ant-design-vue/es')['Popover'] APopover: typeof import('ant-design-vue/es')['Popover']
ARangePicker: typeof import('ant-design-vue/es')['RangePicker'] ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
ASelect: typeof import('ant-design-vue/es')['Select'] ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASlider: typeof import('ant-design-vue/es')['Slider'] ASlider: typeof import('ant-design-vue/es')['Slider']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin'] ASpin: typeof import('ant-design-vue/es')['Spin']
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ASwitch: typeof import('ant-design-vue/es')['Switch'] ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table'] ATable: typeof import('ant-design-vue/es')['Table']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATimeRangePicker: typeof import('ant-design-vue/es')['TimeRangePicker']
AUpload: typeof import('ant-design-vue/es')['Upload'] AUpload: typeof import('ant-design-vue/es')['Upload']
ElCascader: typeof import('element-plus/es')['ElCascader']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
} }

View File

@@ -456,6 +456,13 @@ onMounted(async () => {
// 使用window的resize事件代替ResizeObserver // 使用window的resize事件代替ResizeObserver
// 只有当窗口大小变化时才更新画布尺寸 // 只有当窗口大小变化时才更新画布尺寸
// window.addEventListener("resize", handleWindowResize); // window.addEventListener("resize", handleWindowResize);
if(props.config.initZoom) {
const width = canvasManager.width;
const height = canvasManager.height;
const cwidth = props.config.width;
const cheight = props.config.height;
setZoom(Math.min(width/cwidth,height/cheight)); // 设置画布缩放
}
}); });
watchEffect(() => { watchEffect(() => {
@@ -521,6 +528,19 @@ function resetZoom() {
canvasManager.resetZoom(); canvasManager.resetZoom();
} }
function setZoom(zoom) {
setTimeout(()=>{
if (!canvasManager) return;
const newZoom = Math.max(zoom / 1.1, 0.1); // 减少10%最小0.1倍
// 使用画布中心作为缩放点
const centerPoint = {
x: canvasManager.canvas.width / 2,
y: canvasManager.canvas.height / 2,
};
canvasManager.animateZoom(centerPoint, newZoom);
})
}
function zoomIn() { function zoomIn() {
if (!canvasManager) return; if (!canvasManager) return;
@@ -882,6 +902,7 @@ defineExpose({
layerId = "", // 导出具体图层ID layerId = "", // 导出具体图层ID
layerIdArray = [], // 导出多个图层ID数组 layerIdArray = [], // 导出多个图层ID数组
expPicType = "png", // 导出图片类型 JPG 或 PNG ,SVG expPicType = "png", // 导出图片类型 JPG 或 PNG ,SVG
isEnhanceImg, // 是否是增强图片
} = {}) => { } = {}) => {
return canvasManager.exportImage({ return canvasManager.exportImage({
isContainBg, isContainBg,
@@ -890,6 +911,7 @@ defineExpose({
layerId, layerId,
layerIdArray, layerIdArray,
expPicType, expPicType,
isEnhanceImg,
}); });
}, },
/** /**

View File

@@ -811,6 +811,7 @@ export class CanvasManager {
* @param {Array} options.layerIdArray 导出多个图层ID数组 * @param {Array} options.layerIdArray 导出多个图层ID数组
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg) * @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 * @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
* @param {Boolean} options.isEnhanceImg 是否是增强图片
* @returns {String} 导出的图片数据URL * @returns {String} 导出的图片数据URL
*/ */
async exportImage(options = {}) { async exportImage(options = {}) {

View File

@@ -18,10 +18,12 @@ export class ExportManager {
* @param {Object} options 导出选项 * @param {Object} options 导出选项
* @param {Boolean} options.isContainBg 是否包含背景图层 * @param {Boolean} options.isContainBg 是否包含背景图层
* @param {Boolean} options.isContainFixed 是否包含固定图层 * @param {Boolean} options.isContainFixed 是否包含固定图层
* @param {Boolean} options.isCropByBg 是否使用背景大小裁剪
* @param {String} options.layerId 导出具体图层ID * @param {String} options.layerId 导出具体图层ID
* @param {Array} options.layerIdArray 导出多个图层ID数组 * @param {Array} options.layerIdArray 导出多个图层ID数组
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg) * @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 * @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
* @param {Boolean} options.isEnhanceImg 是否是增强图片
* @returns {String} 导出的图片数据URL * @returns {String} 导出的图片数据URL
*/ */
exportImage(options = {}) { exportImage(options = {}) {
@@ -33,6 +35,7 @@ export class ExportManager {
layerIdArray = [], layerIdArray = [],
expPicType = "png", expPicType = "png",
restoreOpacityInRedGreen = true, restoreOpacityInRedGreen = true,
isEnhanceImg, // 是否是增强图片
} = options; } = options;
try { try {
// 检查是否为红绿图模式 // 检查是否为红绿图模式
@@ -44,7 +47,8 @@ export class ExportManager {
expPicType, expPicType,
isRedGreenMode, isRedGreenMode,
restoreOpacityInRedGreen, restoreOpacityInRedGreen,
isCropByBg isCropByBg,
isEnhanceImg, // 是否是增强图片
); );
} }
@@ -57,7 +61,8 @@ export class ExportManager {
isContainFixed, isContainFixed,
isRedGreenMode, isRedGreenMode,
restoreOpacityInRedGreen, restoreOpacityInRedGreen,
isCropByBg isCropByBg,
isEnhanceImg, // 是否是增强图片
); );
} }
@@ -68,7 +73,8 @@ export class ExportManager {
isContainFixed, isContainFixed,
isRedGreenMode, isRedGreenMode,
restoreOpacityInRedGreen, restoreOpacityInRedGreen,
isCropByBg isCropByBg,
isEnhanceImg, // 是否是增强图片
); );
} catch (error) { } catch (error) {
console.error("导出图片失败:", error); console.error("导出图片失败:", error);
@@ -82,6 +88,8 @@ export class ExportManager {
* @param {String} expPicType 导出类型 * @param {String} expPicType 导出类型
* @param {Boolean} isRedGreenMode 是否为红绿图模式 * @param {Boolean} isRedGreenMode 是否为红绿图模式
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 * @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
* @param {Boolean} isCropByBg 是否使用背景大小裁剪
* @param {Boolean} isEnhanceImg 是否是增强图片
* @returns {String} 图片数据URL * @returns {String} 图片数据URL
* @private * @private
*/ */
@@ -89,7 +97,9 @@ export class ExportManager {
layerId, layerId,
expPicType, expPicType,
isRedGreenMode, isRedGreenMode,
restoreOpacityInRedGreen restoreOpacityInRedGreen,
isCropByBg, // 是否使用背景大小裁剪
isEnhanceImg, // 是否是增强图片
) { ) {
if (!this.layerManager) { if (!this.layerManager) {
throw new Error("图层管理器未初始化"); throw new Error("图层管理器未初始化");
@@ -117,7 +127,9 @@ export class ExportManager {
return this._exportWithRedGreenMode( return this._exportWithRedGreenMode(
objectsToExport, objectsToExport,
expPicType, expPicType,
restoreOpacityInRedGreen restoreOpacityInRedGreen,
isCropByBg, // 是否使用背景大小裁剪
isEnhanceImg, // 是否是增强图片
); );
} }
@@ -137,6 +149,8 @@ export class ExportManager {
* @param {Boolean} isContainFixed 是否包含固定图层 * @param {Boolean} isContainFixed 是否包含固定图层
* @param {Boolean} isRedGreenMode 是否为红绿图模式 * @param {Boolean} isRedGreenMode 是否为红绿图模式
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 * @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
* @param {Boolean} isCropByBg 是否使用背景大小裁剪
* @param {Boolean} isEnhanceImg 是否是增强图片
* @returns {String} 图片数据URL * @returns {String} 图片数据URL
* @private * @private
*/ */
@@ -147,7 +161,8 @@ export class ExportManager {
isContainFixed, isContainFixed,
isRedGreenMode, isRedGreenMode,
restoreOpacityInRedGreen, restoreOpacityInRedGreen,
isCropByBg isCropByBg, // 是否使用背景大小裁剪
isEnhanceImg, // 是否是增强图片
) { ) {
if (!this.layerManager) { if (!this.layerManager) {
throw new Error("图层管理器未初始化"); throw new Error("图层管理器未初始化");
@@ -178,7 +193,9 @@ export class ExportManager {
return await this._exportWithCanvasSize( return await this._exportWithCanvasSize(
objectsToExport, objectsToExport,
expPicType, expPicType,
restoreOpacityInRedGreen restoreOpacityInRedGreen,
isCropByBg, // 是否使用背景大小裁剪
isEnhanceImg, // 是否是增强图片
); );
} }
@@ -189,6 +206,8 @@ export class ExportManager {
* @param {Boolean} isContainFixed 是否包含固定图层 * @param {Boolean} isContainFixed 是否包含固定图层
* @param {Boolean} isRedGreenMode 是否为红绿图模式 * @param {Boolean} isRedGreenMode 是否为红绿图模式
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 * @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
* @param {Boolean} isCropByBg 是否使用背景大小裁剪
* @param {Boolean} isEnhanceImg 是否是增强图片
* @returns {String} 图片数据URL * @returns {String} 图片数据URL
* @private * @private
*/ */
@@ -198,7 +217,8 @@ export class ExportManager {
isContainFixed, isContainFixed,
isRedGreenMode, isRedGreenMode,
restoreOpacityInRedGreen, restoreOpacityInRedGreen,
isCropByBg isCropByBg, // 是否使用背景大小裁剪
isEnhanceImg, // 是否是增强图片
) { ) {
// 按图层顺序收集对象(从底到顶) // 按图层顺序收集对象(从底到顶)
const objectsToExport = this._collectObjectsByLayerOrder( const objectsToExport = this._collectObjectsByLayerOrder(
@@ -251,7 +271,9 @@ export class ExportManager {
objectsToExport, objectsToExport,
expPicType, expPicType,
restoreOpacityInRedGreen, restoreOpacityInRedGreen,
canvasClipPath canvasClipPath,
isCropByBg, // 是否使用背景大小裁剪
isEnhanceImg, // 是否是增强图片
); );
} }
@@ -603,6 +625,9 @@ export class ExportManager {
* @param {Array} objectsToExport 要导出的对象数组 * @param {Array} objectsToExport 要导出的对象数组
* @param {String} expPicType 导出类型 * @param {String} expPicType 导出类型
* @param {Boolean} restoreOpacityInRedGreen 是否恢复透明度为1 * @param {Boolean} restoreOpacityInRedGreen 是否恢复透明度为1
* @param {Object} maskObject 裁剪对象
* @param {Boolean} isCropByBg 是否使用背景大小裁剪
* @param {Boolean} isEnhanceImg 是否是增强图片
* @returns {String} 图片数据URL * @returns {String} 图片数据URL
* @private * @private
*/ */
@@ -610,7 +635,9 @@ export class ExportManager {
objectsToExport, objectsToExport,
expPicType, expPicType,
restoreOpacityInRedGreen, restoreOpacityInRedGreen,
maskObject maskObject, // 裁剪对象
isCropByBg, // 是否使用背景大小裁剪
isEnhanceImg, // 是否是增强图片
) { ) {
// 使用当前画布尺寸 // 使用当前画布尺寸
// const canvasWidth = // const canvasWidth =
@@ -629,6 +656,8 @@ export class ExportManager {
trimWhitespace: true, // 裁剪空白 trimWhitespace: true, // 裁剪空白
trimPadding: 0, // 裁剪边距 trimPadding: 0, // 裁剪边距
restoreOpacityInRedGreen, restoreOpacityInRedGreen,
isCropByBg, // 是否使用背景大小裁剪
isEnhanceImg, // 是否是增强图片
}); });
console.log("导出图片数据URL:", dataURL); console.log("导出图片数据URL:", dataURL);

View File

@@ -21,6 +21,7 @@ export const createRasterizedImage = async ({
preserveOriginalQuality = true, // 是否保持原始质量(新增) preserveOriginalQuality = true, // 是否保持原始质量(新增)
selectionManager = null, // 选区管理器,用于获取羽化值等设置 selectionManager = null, // 选区管理器,用于获取羽化值等设置
restoreOpacityInRedGreen, // 是否在红绿图模式下恢复透明度 restoreOpacityInRedGreen, // 是否在红绿图模式下恢复透明度
isEnhanceImg, // 是否是增强图片
} = {}) => { } = {}) => {
try { try {
console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象`); console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象`);
@@ -41,6 +42,7 @@ export const createRasterizedImage = async ({
clippingObject, clippingObject,
isReturenDataURL, isReturenDataURL,
selectionManager, // 传递选区管理器 selectionManager, // 传递选区管理器
isEnhanceImg, // 是否是增强图片
}); });
} }
@@ -81,6 +83,7 @@ const createClippedObjects = async ({
clippingObject, clippingObject,
isReturenDataURL, isReturenDataURL,
selectionManager = null, // 新增选区管理器参数 selectionManager = null, // 新增选区管理器参数
isEnhanceImg, // 是否是增强图片
}) => { }) => {
try { try {
console.log("🎯 使用新的图像遮罩裁剪方法创建对象"); console.log("🎯 使用新的图像遮罩裁剪方法创建对象");
@@ -110,6 +113,7 @@ const createClippedObjects = async ({
clippingObject, clippingObject,
selectionBounds: optimizedBounds, // 使用优化后的边界框 selectionBounds: optimizedBounds, // 使用优化后的边界框
featherAmount, featherAmount,
isEnhanceImg, // 是否是增强图片
}); });
} }
@@ -120,6 +124,7 @@ const createClippedObjects = async ({
clippingObject, clippingObject,
selectionBounds: optimizedBounds, // 使用优化后的边界框 selectionBounds: optimizedBounds, // 使用优化后的边界框
featherAmount, featherAmount,
isEnhanceImg, // 是否是增强图片
}); });
// 将DataURL转换为fabric.Image对象 // 将DataURL转换为fabric.Image对象
@@ -173,6 +178,7 @@ const createClippedDataURLByCanvas = async ({
clippingObject, clippingObject,
selectionBounds, selectionBounds,
featherAmount = 0, featherAmount = 0,
isEnhanceImg = false, // 是否是增强图片
}) => { }) => {
try { try {
console.log("🖼️ 使用图像遮罩裁剪方法生成DataURL"); console.log("🖼️ 使用图像遮罩裁剪方法生成DataURL");
@@ -185,7 +191,9 @@ const createClippedDataURLByCanvas = async ({
// 使用高分辨率以保证质量 // 使用高分辨率以保证质量
const pixelRatio = window.devicePixelRatio || 1; const pixelRatio = window.devicePixelRatio || 1;
const qualityMultiplier = Math.max(2, pixelRatio); const qualityMultiplier = !!isEnhanceImg ? Math.max(2, pixelRatio) : 1;
console.log("使用高分辨率以保证质量:" + isEnhanceImg, optimizedBounds);
const canvasWidth = Math.ceil(optimizedBounds.width * qualityMultiplier); const canvasWidth = Math.ceil(optimizedBounds.width * qualityMultiplier);
const canvasHeight = Math.ceil(optimizedBounds.height * qualityMultiplier); const canvasHeight = Math.ceil(optimizedBounds.height * qualityMultiplier);
@@ -455,6 +463,8 @@ const createLegacyRasterization = async ({
quality, quality,
format, format,
isReturenDataURL, isReturenDataURL,
isCropByBg, // 是否根据背景裁剪
isEnhanceImg, // 是否是增强图片
}) => { }) => {
console.log("⚠️ 使用兼容的离屏渲染方法"); console.log("⚠️ 使用兼容的离屏渲染方法");
@@ -481,6 +491,8 @@ const createLegacyRasterization = async ({
format, format,
currentZoom, currentZoom,
isReturenDataURL, isReturenDataURL,
isCropByBg, // 是否根据背景裁剪
isEnhanceImg, // 是否是增强图片
}); });
}; };
@@ -571,6 +583,8 @@ const createOffscreenRasterization = async ({
format, format,
currentZoom, currentZoom,
isReturenDataURL, isReturenDataURL,
isCropByBg, // 是否根据背景裁剪
isEnhanceImg, // 是否是增强图片
}) => { }) => {
try { try {
// 创建离屏画布,使用绝对尺寸以保证高质量 // 创建离屏画布,使用绝对尺寸以保证高质量

View File

@@ -113,12 +113,16 @@ export default defineComponent({
return new Promise((res,rev)=>{ return new Promise((res,rev)=>{
let img = new Image() let img = new Image()
img.onload = ()=>{ img.onload = ()=>{
let wH = [1,1] // let wH = [1,1]
let domHeight = dataDom.canvasBox.offsetHeight - 200 // let domHeight = dataDom.canvasBox.offsetHeight - 200
let imgHeight = img.height // let imgHeight = img.height
wH = [1,domHeight/imgHeight] // wH = [1,domHeight/imgHeight]
data.canvasConfig.width = img.width * wH[1] // data.canvasConfig.width = img.width * wH[1]
data.canvasConfig.height = domHeight // data.canvasConfig.height = domHeight
data.canvasConfig.height = img.height
data.canvasConfig.width = img.width
data.canvasConfig.initZoom = true
data.canvasLoad = true data.canvasLoad = true
res('') res('')
} }

View File

@@ -87,7 +87,7 @@ export default defineComponent({
// dataDom.editCanvas.exportImage({isContainBg:props.source == 'detail',isContainFixed:true}).then((rv)=>{ // dataDom.editCanvas.exportImage({isContainBg:props.source == 'detail',isContainFixed:true}).then((rv)=>{
// emit('submitBase64Data',rv) // emit('submitBase64Data',rv)
// }) // })
dataDom.editCanvas.exportImage({isContainBg:true,isContainFixed:true}).then((rv)=>{ dataDom.editCanvas.exportImage({isContainBg:true,isContainFixed:true,isCropByBg:true}).then((rv)=>{
emit('submitBase64Data',rv) emit('submitBase64Data',rv)
}) })
} }
@@ -135,12 +135,16 @@ export default defineComponent({
if(props.imgUrl){ if(props.imgUrl){
let img = new Image() let img = new Image()
img.onload = ()=>{ img.onload = ()=>{
let wH = [1,1] // let wH = [1,1]
let domHeight = dataDom.canvasBox.offsetHeight - 200 // let domHeight = dataDom.canvasBox.offsetHeight - 200
let imgHeight = img.height // let imgHeight = img.height
wH = [1,domHeight/imgHeight] // wH = [1,domHeight/imgHeight]
data.canvasConfig.height = domHeight // data.canvasConfig.height = domHeight
data.canvasConfig.width = wH[1] * img.width // data.canvasConfig.width = wH[1] * img.width
data.canvasConfig.height = img.height
data.canvasConfig.width = img.width
data.canvasConfig.initZoom = true
data.canvasLoad = true data.canvasLoad = true
// setTimeout(()=>{ // setTimeout(()=>{
// canvasLoadAddImg() // canvasLoadAddImg()