feat(canvas): 适配微信浏览器并优化导出质量

This commit is contained in:
2026-04-21 09:05:33 +08:00
parent 16de547957
commit e771988338

View File

@@ -2006,21 +2006,46 @@
}
}
// 生成画布(高清无损,固定 1080×1920 分辨率)
// 检测是否在微信内置浏览器
function isWechat() {
return /MicroMessenger/i.test(navigator.userAgent);
}
// 生成画布(高清无损,根据设备像素比自适应分辨率)
async function generateCanvas() {
const canvas = exportCanvas;
const ctx = canvas.getContext('2d');
const container = imageContainer;
// 固定高清分辨率 1080×1920
const TARGET_WIDTH = 1080;
const TARGET_HEIGHT = 1920;
console.log('目标画布尺寸:', TARGET_WIDTH, 'x', TARGET_HEIGHT);
// 显示尺寸(用于计算缩放比例)
// 根据设备像素比决定输出分辨率,移动端至少 2x
// 微信等浏览器 canvas 最大尺寸通常为 4096,所以上限设为 2160×3840
const dpr = window.devicePixelRatio || 1;
const minScale = Math.max(dpr, 2); // 至少 2x
const displayWidth = container.clientWidth;
const displayHeight = container.clientHeight;
let TARGET_WIDTH = Math.round(displayWidth * minScale);
let TARGET_HEIGHT = Math.round(displayHeight * minScale);
// 微信特殊处理:微信内置浏览器 canvas 限制更严格
// 实测微信中 4096 可能不稳定,使用 2048 更安全
const MAX_CANVAS_SIZE = isWechat() ? 2048 : 4096;
if (TARGET_HEIGHT > MAX_CANVAS_SIZE) {
const ratio = MAX_CANVAS_SIZE / TARGET_HEIGHT;
TARGET_WIDTH = Math.round(TARGET_WIDTH * ratio);
TARGET_HEIGHT = MAX_CANVAS_SIZE;
}
if (TARGET_WIDTH > MAX_CANVAS_SIZE) {
const ratio = MAX_CANVAS_SIZE / TARGET_WIDTH;
TARGET_HEIGHT = Math.round(TARGET_HEIGHT * ratio);
TARGET_WIDTH = MAX_CANVAS_SIZE;
}
// 确保最小分辨率
TARGET_WIDTH = Math.max(TARGET_WIDTH, 1080);
TARGET_HEIGHT = Math.max(TARGET_HEIGHT, 1920);
console.log('目标画布尺寸:', TARGET_WIDTH, 'x', TARGET_HEIGHT, 'dpr:', dpr, '微信:', isWechat());
console.log('显示容器尺寸:', displayWidth, 'x', displayHeight);
// 临时显示 canvas 以避免浏览器对隐藏 canvas 的尺寸限制
@@ -2122,8 +2147,36 @@
const drawY = (containerHeight - scaledHeight) / 2;
ctx.globalAlpha = bgImageOpacity;
// 使用原始图片绘制,获得最佳清晰度
ctx.drawImage(img, drawX, drawY, scaledWidth, scaledHeight);
// 微信中:如果原始图片比目标区域大,使用原始尺寸绘制以获得最佳质量
// 否则按计算尺寸绘制
if (isWechat() && img.naturalWidth > scaledWidth && img.naturalHeight > scaledHeight) {
// 计算裁剪区域,从原始图片中心裁剪
const srcRatio = scaledWidth / scaledHeight;
let srcWidth = img.naturalWidth;
let srcHeight = img.naturalHeight;
let srcX = 0;
let srcY = 0;
// 如果原始图片比例与目标不同,计算居中裁剪
const naturalRatio = img.naturalWidth / img.naturalHeight;
if (naturalRatio > imgRatio) {
// 原始图片更宽,裁剪左右
srcWidth = img.naturalHeight * imgRatio;
srcX = (img.naturalWidth - srcWidth) / 2;
} else if (naturalRatio < imgRatio) {
// 原始图片更高,裁剪上下
srcHeight = img.naturalWidth / imgRatio;
srcY = (img.naturalHeight - srcHeight) / 2;
}
// 使用 drawImage 的 9 参数版本进行高质量缩放
ctx.drawImage(img, srcX, srcY, srcWidth, srcHeight, drawX, drawY, scaledWidth, scaledHeight);
} else {
// 使用原始图片绘制,获得最佳清晰度
ctx.drawImage(img, drawX, drawY, scaledWidth, scaledHeight);
}
ctx.globalAlpha = 1;
}
@@ -2274,10 +2327,21 @@
const fontSize = parseInt(style.fontSize) * scaleX; // 字体大小也按比例缩放
const lineHeight = parseFloat(style.lineHeight);
// 微信中:使用更粗的字体权重以获得更好的可读性
let fontWeight = style.fontWeight;
if (isWechat() && fontWeight === '400' || fontWeight === 'normal') {
fontWeight = '500'; // 稍微加粗一点
}
// 使用高清字体渲染
ctx.font = `${style.fontWeight} ${fontSize}px ${style.fontFamily}`;
ctx.font = `${fontWeight} ${fontSize}px ${style.fontFamily}`;
ctx.fillStyle = style.color;
ctx.textBaseline = 'alphabetic';
// 微信中:开启文字抗锯齿
if (isWechat()) {
ctx.textRendering = 'optimizeLegibility';
}
// 计算文本垂直偏移量,使其与 DOM 渲染一致
const actualLineHeight = (lineHeight || fontSize * 1.4) * scaleY;
@@ -2309,29 +2373,49 @@
// 在 toBlob 之前再次确认 canvas 尺寸
console.log('toBlob 前 canvas 尺寸:', canvas.width, 'x', canvas.height);
// 尝试使用 toDataURL 获取更高质量的图片
// 微信中使用 toBlob 更稳定,且质量参数 1.0 确保无损
let dataUrl;
let blob;
try {
// 先尝试 toDataURL
dataUrl = canvas.toDataURL('image/png');
console.log('toDataURL 成功,dataUrl 长度:', dataUrl.length);
// 将 dataUrl 转换为 blob
const response = await fetch(dataUrl);
blob = await response.blob();
console.log('dataUrl 转 blob 成功,blob 尺寸:', blob.size, 'bytes');
} catch (err) {
console.log('toDataURL 失败,使用 toBlob:', err);
// 如果 toDataURL 失败,使用 toBlob
blob = await new Promise(resolve => {
canvas.toBlob((b) => {
console.log('toBlob 回调 - blob 尺寸:', b.size, 'bytes');
resolve(b);
}, 'image/png', 1.0);
});
dataUrl = URL.createObjectURL(blob);
// 微信中优先使用 toBlob,避免 toDataURL 的内存问题
if (isWechat()) {
try {
blob = await new Promise((resolve, reject) => {
canvas.toBlob((b) => {
if (b) {
console.log('微信 toBlob 成功,blob 尺寸:', b.size, 'bytes');
resolve(b);
} else {
reject(new Error('toBlob 返回 null'));
}
}, 'image/png', 1.0); // 1.0 = 最高质量无损 PNG
});
dataUrl = URL.createObjectURL(blob);
} catch (err) {
console.log('微信 toBlob 失败,回退到 toDataURL:', err);
dataUrl = canvas.toDataURL('image/png');
const response = await fetch(dataUrl);
blob = await response.blob();
}
} else {
// 非微信环境,先尝试 toDataURL
try {
dataUrl = canvas.toDataURL('image/png');
console.log('toDataURL 成功,dataUrl 长度:', dataUrl.length);
const response = await fetch(dataUrl);
blob = await response.blob();
console.log('dataUrl 转 blob 成功,blob 尺寸:', blob.size, 'bytes');
} catch (err) {
console.log('toDataURL 失败,使用 toBlob:', err);
blob = await new Promise(resolve => {
canvas.toBlob((b) => {
console.log('toBlob 回调 - blob 尺寸:', b.size, 'bytes');
resolve(b);
}, 'image/png', 1.0);
});
dataUrl = URL.createObjectURL(blob);
}
}
console.log('导出图片大小:', (blob.size / 1024).toFixed(2), 'KB');