@wtechtec/qr-generator-core
Version:
A powerful QR code generator with customizable styling options
524 lines (519 loc) • 18.8 kB
JavaScript
'use strict';
var QRCodeStyling = require('qr-code-styling');
var html2canvas = require('html2canvas');
// 默认配置
const defaultConfig = {
text: 'Hello World',
width: 800,
height: 600,
qrOptions: {
typeNumber: 0,
mode: 'Byte',
errorCorrectionLevel: 'M',
},
imageOptions: {
hideBackgroundDots: true,
imageSize: 0.4,
margin: 4,
crossOrigin: 'anonymous',
},
dotsOptions: {
color: '#0000ff',
type: 'square',
},
backgroundOptions: {
color: '#ffffff',
},
cornersSquareOptions: {
color: '#0000ff',
type: 'square',
},
cornersDotOptions: {
color: '#000000',
type: 'square',
},
exportOptions: {
format: 'png',
quality: 0.9,
borderRadius: 0,
},
texts: [],
backgrounds: [],
htmlModules: [],
};
// 计算默认二维码位置和尺寸的辅助函数
const calculateDefaultQRSettings = (canvasWidth, canvasHeight, qrRatio = 0.6) => {
const qrSize = Math.min(canvasWidth, canvasHeight) * qrRatio;
return {
qrPosition: {
x: (canvasWidth - qrSize) / 2,
y: (canvasHeight - qrSize) / 2
},
qrSize: {
width: qrSize,
height: qrSize
}
};
};
// 类型导出
// export type { PlatformQRGeneratorConfig };
// 主要的QR生成器类
class QRGenerator {
constructor(config) {
this.container = null;
this.qrCode = null;
this.isRendered = false;
// 合并配置,如果没有提供qrPosition和qrSize,则自动计算
const mergedConfig = { ...defaultConfig, ...config };
// 如果没有明确设置二维码位置和尺寸,则根据画布尺寸自动计算
if (!config.qrPosition || !config.qrSize) {
const autoSettings = calculateDefaultQRSettings(mergedConfig.width, mergedConfig.height);
if (!config.qrPosition) {
mergedConfig.qrPosition = autoSettings.qrPosition;
}
if (!config.qrSize) {
mergedConfig.qrSize = autoSettings.qrSize;
}
}
this.config = mergedConfig;
}
// 创建画布容器
createCanvas() {
const canvas = document.createElement('div');
canvas.style.cssText = `
position: relative;
width: ${this.config.width}px;
height: ${this.config.height}px;
background: ${this.config.backgroundOptions.color};
overflow: hidden;
font-family: Arial, sans-serif;
`;
// 应用渐变背景
if (this.config.backgroundOptions.gradient) {
const gradient = this.createGradientStyle(this.config.backgroundOptions.gradient);
canvas.style.background = gradient;
}
// 应用圆角
if (this.config.exportOptions.borderRadius > 0) {
canvas.style.borderRadius = `${this.config.exportOptions.borderRadius}px`;
}
return canvas;
}
// 创建渐变样式
createGradientStyle(gradient) {
const { type, rotation, colorStops } = gradient;
const stops = colorStops.map((stop) => `${stop.color} ${stop.offset * 100}%`).join(', ');
if (type === 'linear') {
return `linear-gradient(${rotation}deg, ${stops})`;
}
else {
return `radial-gradient(circle, ${stops})`;
}
}
// 获取object-fit样式
getObjectFitStyle(mode) {
switch (mode) {
case 'fill':
case 'stretch':
return 'fill';
case 'contain':
return 'contain';
case 'cover':
return 'cover';
default:
return 'fill';
}
}
// 添加背景图片
async addBackgrounds(canvas) {
if (!this.config.backgrounds || this.config.backgrounds.length === 0)
return;
const loadPromises = this.config.backgrounds.map(async (bg, index) => {
return new Promise((resolve) => {
const img = document.createElement('img');
img.style.cssText = `
position: absolute;
left: ${bg.position.x}px;
top: ${bg.position.y}px;
width: ${bg.size.width}px;
height: ${bg.size.height}px;
z-index: ${bg.zIndex};
opacity: ${bg.opacity};
object-fit: ${this.getObjectFitStyle(bg.mode)};
pointer-events: none;
background-repeat: no-repeat;
`;
img.onload = () => {
console.log(`背景图片 ${index + 1} 加载成功`);
canvas.appendChild(img);
resolve();
};
img.onerror = (error) => {
console.warn(`背景图片 ${index + 1} 加载失败:`, error);
resolve(); // 继续执行,不阻塞其他元素
};
// 设置crossOrigin在设置src之前
img.crossOrigin = 'anonymous';
img.src = bg.src;
// 添加超时处理
setTimeout(() => {
if (!img.complete) {
console.warn(`背景图片 ${index + 1} 加载超时`);
resolve();
}
}, 10000); // 10秒超时
});
});
await Promise.all(loadPromises);
}
// 添加二维码 - 使用配置中的位置和尺寸
async addQRCode(canvas) {
const QRCodeStylingClass = QRCodeStyling;
return new Promise((resolve, reject) => {
try {
// 使用配置中的二维码位置和尺寸
const qrX = this.config.qrPosition.x;
const qrY = this.config.qrPosition.y;
const qrWidth = this.config.qrSize.width;
const qrHeight = this.config.qrSize.height;
console.log(`二维码配置: 位置(${qrX}, ${qrY}), 尺寸(${qrWidth}x${qrHeight})`);
// 创建二维码容器
const qrContainer = document.createElement('div');
qrContainer.style.cssText = `
position: absolute;
left: ${qrX}px;
top: ${qrY}px;
width: ${qrWidth}px;
height: ${qrHeight}px;
z-index: 100;
`;
// 配置二维码样式
const qrConfig = {
width: qrWidth,
height: qrHeight,
type: 'canvas',
data: this.config.text,
image: this.config.logo?.src,
qrOptions: this.config.qrOptions,
imageOptions: {
...this.config.imageOptions,
imageSize: this.config.logo?.size ? this.config.logo.size / 100 : this.config.imageOptions.imageSize,
},
dotsOptions: this.config.dotsOptions,
backgroundOptions: this.config.backgroundOptions,
cornersSquareOptions: this.config.cornersSquareOptions,
cornersDotOptions: this.config.cornersDotOptions,
};
this.qrCode = new QRCodeStylingClass(qrConfig);
// 渲染二维码
this.qrCode.append(qrContainer);
canvas.appendChild(qrContainer);
// 等待二维码渲染完成
setTimeout(() => {
console.log('二维码渲染完成');
resolve();
}, 500);
}
catch (error) {
console.error('二维码创建失败:', error);
reject(error);
}
});
}
// 添加文本
addTexts(canvas) {
if (!this.config.texts || this.config.texts.length === 0)
return;
this.config.texts.forEach((text, index) => {
const textElement = document.createElement('div');
textElement.textContent = text.content;
textElement.style.cssText = `
position: absolute;
left: ${text.position.x}px;
top: ${text.position.y}px;
font-size: ${text.fontSize}px;
color: ${text.color};
font-family: ${text.fontFamily};
font-weight: ${text.fontWeight};
z-index: ${text.zIndex};
opacity: ${text.opacity};
text-align: ${text.textAlign || 'left'};
line-height: ${text.lineHeight || 1.2};
white-space: pre-wrap;
word-break: break-word;
pointer-events: none;
`;
console.log(`添加文本 ${index + 1}: "${text.content}" 位置(${text.position.x}, ${text.position.y})`);
canvas.appendChild(textElement);
});
}
// 添加HTML模块
addHtmlModules(canvas) {
if (!this.config.htmlModules || this.config.htmlModules.length === 0)
return;
this.config.htmlModules.forEach((module, index) => {
const htmlElement = document.createElement('div');
htmlElement.innerHTML = module.content;
htmlElement.style.cssText = `
position: absolute;
left: ${module.position.x}px;
top: ${module.position.y}px;
width: ${module.size.width}px;
height: ${module.size.height}px;
z-index: ${module.zIndex};
opacity: ${module.opacity};
overflow: hidden;
pointer-events: none;
`;
console.log(`添加HTML模块 ${index + 1} 位置(${module.position.x}, ${module.position.y})`);
canvas.appendChild(htmlElement);
});
}
// 渲染完整画布
async render() {
// 创建主画布
this.container = this.createCanvas();
// 添加到DOM(隐藏位置)
this.container.style.position = 'absolute';
this.container.style.left = '-9999px';
this.container.style.top = '-9999px';
document.body.appendChild(this.container);
try {
console.log('开始渲染画布...', {
canvas: `${this.config.width}x${this.config.height}`,
qrPosition: this.config.qrPosition,
qrSize: this.config.qrSize
});
// 按层级顺序添加元素
console.log('添加背景图片...');
await this.addBackgrounds(this.container);
console.log('添加二维码...');
await this.addQRCode(this.container);
console.log('添加文本...');
this.addTexts(this.container);
console.log('添加HTML模块...');
this.addHtmlModules(this.container);
this.isRendered = true;
console.log('画布渲染完成');
return this.container;
}
catch (error) {
console.error('画布渲染失败:', error);
this.cleanup();
throw error;
}
}
// 导出为PNG
async exportAsPNG(options) {
if (!this.isRendered) {
await this.render();
}
if (!this.container) {
throw new Error('画布未创建');
}
const html2canvasOptions = {
scale: options?.scale || 2,
useCORS: options?.useCORS !== false,
allowTaint: options?.allowTaint !== false,
backgroundColor: null,
logging: false,
width: this.config.width,
height: this.config.height,
};
try {
console.log('开始导出PNG...', html2canvasOptions);
const canvas = await html2canvas(this.container, html2canvasOptions);
console.log('html2canvas完成,画布尺寸:', canvas.width, 'x', canvas.height);
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) {
console.log('PNG导出成功,大小:', blob.size, 'bytes');
resolve(blob);
}
else {
reject(new Error('无法创建PNG Blob'));
}
}, 'image/png', options?.quality || this.config.exportOptions.quality);
});
}
catch (error) {
console.error('导出PNG失败:', error);
throw new Error(`导出PNG失败: ${error}`);
}
}
// 导出为DataURL
async exportAsDataURL(options) {
if (!this.isRendered) {
await this.render();
}
if (!this.container) {
throw new Error('画布未创建');
}
const html2canvasOptions = {
scale: options?.scale || 2,
useCORS: options?.useCORS !== false,
allowTaint: options?.allowTaint !== false,
backgroundColor: null,
logging: false,
width: this.config.width,
height: this.config.height,
};
try {
const canvas = await html2canvas(this.container, html2canvasOptions);
return canvas.toDataURL('image/png', options?.quality || this.config.exportOptions.quality);
}
catch (error) {
throw new Error(`导出DataURL失败: ${error}`);
}
}
// 下载PNG文件
async downloadAsPNG(filename, options) {
const blob = await this.exportAsPNG(options);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename || `qr-code-${Date.now()}.png`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// 获取预览元素(用于显示)
getPreviewElement() {
return this.container;
}
// 更新配置
updateConfig(newConfig) {
// 如果更新了画布尺寸但没有更新二维码位置/尺寸,则自动重新计算
if ((newConfig.width || newConfig.height) && (!newConfig.qrPosition && !newConfig.qrSize)) {
const width = newConfig.width || this.config.width;
const height = newConfig.height || this.config.height;
const autoSettings = calculateDefaultQRSettings(width, height);
newConfig.qrPosition = autoSettings.qrPosition;
newConfig.qrSize = autoSettings.qrSize;
}
this.config = { ...this.config, ...newConfig };
this.isRendered = false;
}
// 获取当前配置
getConfig() {
return { ...this.config };
}
// 获取配置JSON字符串
getConfigString() {
return JSON.stringify(this.config, null, 2);
}
// 从配置字符串加载
loadFromConfigString(configString) {
try {
const config = JSON.parse(configString);
this.updateConfig(config);
}
catch (error) {
throw new Error(`无法解析配置字符串: ${error}`);
}
}
// 验证配置
validateConfig() {
const errors = [];
if (!this.config.text) {
errors.push('二维码内容不能为空');
}
if (this.config.width <= 0 || this.config.height <= 0) {
errors.push('画布尺寸必须大于0');
}
// 验证二维码位置和尺寸
if (!this.config.qrPosition) {
errors.push('二维码位置未设置');
}
else {
if (this.config.qrPosition.x < 0 || this.config.qrPosition.y < 0) {
errors.push('二维码位置不能为负数');
}
if (this.config.qrPosition.x >= this.config.width || this.config.qrPosition.y >= this.config.height) {
errors.push('二维码位置超出画布范围');
}
}
if (!this.config.qrSize) {
errors.push('二维码尺寸未设置');
}
else {
if (this.config.qrSize.width <= 0 || this.config.qrSize.height <= 0) {
errors.push('二维码尺寸必须大于0');
}
if (this.config.qrPosition &&
(this.config.qrPosition.x + this.config.qrSize.width > this.config.width ||
this.config.qrPosition.y + this.config.qrSize.height > this.config.height)) {
errors.push('二维码超出画布边界');
}
}
if (this.config.exportOptions.quality < 0 || this.config.exportOptions.quality > 1) {
errors.push('导出质量必须在0-1之间');
}
// 验证背景图片配置
if (this.config.backgrounds && this.config.backgrounds.length > 0) {
this.config.backgrounds.forEach((bg, index) => {
if (!bg.src) {
errors.push(`背景图片 ${index + 1} 缺少src属性`);
}
if (bg.size.width <= 0 || bg.size.height <= 0) {
errors.push(`背景图片 ${index + 1} 尺寸必须大于0`);
}
if (bg.opacity < 0 || bg.opacity > 1) {
errors.push(`背景图片 ${index + 1} 透明度必须在0-1之间`);
}
});
}
return {
isValid: errors.length === 0,
errors,
};
}
// 清理资源
cleanup() {
if (this.container && this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
}
this.container = null;
this.qrCode = null;
this.isRendered = false;
}
// 销毁实例
destroy() {
this.cleanup();
}
}
// 便捷函数:创建QR生成器
const createQRGenerator = (config) => {
return new QRGenerator(config);
};
// 便捷函数:快速生成并导出DataURL
const generateQRAsDataURL = async (config, exportOptions) => {
const generator = createQRGenerator(config);
try {
return await generator.exportAsDataURL(exportOptions);
}
finally {
generator.destroy();
}
};
// 便捷函数:快速生成并下载
const generateAndDownloadQR = async (config, filename, exportOptions) => {
const generator = createQRGenerator(config);
try {
await generator.downloadAsPNG(filename, exportOptions);
}
finally {
generator.destroy();
}
};
// 类型导出
// export type { QRGeneratorConfig };
// 版本信息
const version = '1.0.0';
exports.QRGenerator = QRGenerator;
exports.createQRGenerator = createQRGenerator;
exports.defaultConfig = defaultConfig;
exports.generateAndDownloadQR = generateAndDownloadQR;
exports.generateQRAsDataURL = generateQRAsDataURL;
exports.version = version;