const path = require('path'); const QRCodeStyling = require(path.join(__dirname, '..', 'web', 'public', 'vendor', 'qr-code-styling', 'qr-code-styling.js')); const QR_PNG_WIDTH = 2048; function escapeXml(value) { return String(value === undefined || value === null ? '' : value).replace(/[&<>"']/g, function (character) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[character]; }); } function serializeNode(node) { if (!node) { return ''; } if (node.nodeType === 3) { return escapeXml(node.textContent || ''); } const attributeEntries = Object.keys(node.attributes || {}).map(function (name) { return name + '="' + escapeXml(node.attributes[name]) + '"'; }); if (node.tagName === 'svg' && node.namespaceURI === 'http://www.w3.org/2000/svg' && !Object.prototype.hasOwnProperty.call(node.attributes || {}, 'xmlns')) { attributeEntries.unshift('xmlns="http://www.w3.org/2000/svg"'); } const attributes = attributeEntries.join(' '); const children = (node.childNodes || []).map(serializeNode).join('') + escapeXml(node.textContent || ''); return '<' + node.tagName + (attributes ? ' ' + attributes : '') + '>' + children + ''; } function createDomNode(tagName, namespaceUri) { return { tagName: tagName, namespaceURI: namespaceUri || null, attributes: {}, childNodes: [], parentNode: null, nodeType: 1, textContent: '', style: {}, setAttribute: function (name, value) { this.attributes[name] = String(value); }, appendChild: function (child) { if (child) { this.childNodes.push(child); child.parentNode = this; } return child; }, removeChild: function (child) { this.childNodes = this.childNodes.filter(function (node) { return node !== child; }); return child; }, get firstChild() { return this.childNodes[0] || null; }, get outerHTML() { return serializeNode(this); } }; } function createCanvasContext() { return { fillStyle: '#000000', strokeStyle: '#000000', lineWidth: 1, beginPath: function () {}, closePath: function () {}, clearRect: function () {}, fillRect: function () {}, strokeRect: function () {}, moveTo: function () {}, lineTo: function () {}, arc: function () {}, rect: function () {}, fill: function () {}, stroke: function () {}, save: function () {}, restore: function () {}, translate: function () {}, rotate: function () {}, scale: function () {}, setTransform: function () {}, transform: function () {}, drawImage: function () {}, measureText: function (text) { return { width: String(text === undefined || text === null ? '' : text).length * 8 }; }, createLinearGradient: function () { return { addColorStop: function () {} }; }, createRadialGradient: function () { return { addColorStop: function () {} }; }, createPattern: function () { return null; }, getImageData: function () { return { data: [] }; }, putImageData: function () {}, clip: function () {} }; } function createCanvasNode() { const node = createDomNode('canvas', 'http://www.w3.org/1999/xhtml'); node.width = 0; node.height = 0; node.getContext = function () { return createCanvasContext(); }; node.toDataURL = function () { return ''; }; node.toBlob = function (callback) { if (typeof callback === 'function') { callback(null); } }; return node; } function createQrStylingWindow() { const document = { createElement: function (tagName) { if (String(tagName || '').toLowerCase() === 'canvas') { return createCanvasNode(); } return createDomNode(tagName, 'http://www.w3.org/1999/xhtml'); }, createElementNS: function (namespaceUri, tagName) { return createDomNode(tagName, namespaceUri); }, createTextNode: function (text) { return { nodeType: 3, textContent: String(text === undefined || text === null ? '' : text), parentNode: null }; }, body: createDomNode('body', 'http://www.w3.org/1999/xhtml'), head: createDomNode('head', 'http://www.w3.org/1999/xhtml') }; const window = { document: document, navigator: { userAgent: 'node' }, URL: { createObjectURL: function () { return ''; }, revokeObjectURL: function () {} } }; window.window = window; window.self = window; window.Image = class { constructor() { this.onload = null; this.onerror = null; this.width = 1; this.height = 1; this.naturalWidth = 1; this.naturalHeight = 1; this._src = ''; } set src(value) { this._src = String(value === undefined || value === null ? '' : value); if (typeof this.onload === 'function') { setTimeout(() => this.onload(), 0); } } get src() { return this._src; } }; window.HTMLImageElement = window.Image; window.XMLSerializer = class { serializeToString(node) { return serializeNode(node); } }; return { window: window, document: document }; } async function renderStyledQrRawData(options, format) { const previousWindow = global.window; const previousDocument = global.document; const previousXMLSerializer = global.XMLSerializer; const dom = createQrStylingWindow(); global.window = dom.window; global.document = dom.document; global.XMLSerializer = dom.window.XMLSerializer; try { const qrCode = new QRCodeStyling(options); const blob = await qrCode.getRawData(format); if (!blob) { return ''; } if (typeof blob === 'string') { return blob; } if (typeof blob.text === 'function') { return blob.text(); } if (typeof blob.arrayBuffer === 'function') { const buffer = await blob.arrayBuffer(); const bytes = new Uint8Array(buffer); let binary = ''; for (let index = 0; index < bytes.length; index += 1) { binary += String.fromCharCode(bytes[index]); } return binary; } return ''; } finally { global.window = previousWindow; global.document = previousDocument; global.XMLSerializer = previousXMLSerializer; } } function svgToDataUrl(svg) { const markup = String(svg === undefined || svg === null ? '' : svg).trim(); if (!markup) { return ''; } return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(markup); } function normalizeQrStyleColor(value, fallback) { const raw = String(value === undefined || value === null ? '' : value).trim(); return raw || fallback; } function normalizeQrStyleNumber(value, fallback, min, max) { const parsed = Number(value); if (!Number.isFinite(parsed)) { return fallback; } let next = parsed; if (typeof min === 'number') { next = Math.max(min, next); } if (typeof max === 'number') { next = Math.min(max, next); } return next; } function buildQrStylingOptions(source) { const text = String(source && source.value === undefined ? '' : source && source.value === null ? '' : source.value || '').trim(); const qrStyle = source && typeof source === 'object' ? source : {}; const dotsGradient = qrStyle.qr_dots_color_mode === 'gradient' ? { type: String(qrStyle.qr_dots_gradient_type || 'linear').trim() || 'linear', rotation: normalizeQrStyleNumber(qrStyle.qr_dots_gradient_rotation, 0, undefined, undefined), colorStops: [ { offset: 0, color: normalizeQrStyleColor(qrStyle.qr_dots_gradient_color_1, normalizeQrStyleColor(qrStyle.qr_dots_color, '#000000')) }, { offset: 1, color: normalizeQrStyleColor(qrStyle.qr_dots_gradient_color_2, '#ffffff') } ] } : null; const cornersSquareGradient = qrStyle.qr_corners_square_color_mode === 'gradient' ? { type: String(qrStyle.qr_corners_square_gradient_type || 'linear').trim() || 'linear', rotation: normalizeQrStyleNumber(qrStyle.qr_corners_square_gradient_rotation, 0, undefined, undefined), colorStops: [ { offset: 0, color: normalizeQrStyleColor(qrStyle.qr_corners_square_gradient_color_1, normalizeQrStyleColor(qrStyle.qr_corners_square_color, '#000000')) }, { offset: 1, color: normalizeQrStyleColor(qrStyle.qr_corners_square_gradient_color_2, '#ffffff') } ] } : null; const cornersDotGradient = qrStyle.qr_corners_dot_color_mode === 'gradient' ? { type: String(qrStyle.qr_corners_dot_gradient_type || 'linear').trim() || 'linear', rotation: normalizeQrStyleNumber(qrStyle.qr_corners_dot_gradient_rotation, 0, undefined, undefined), colorStops: [ { offset: 0, color: normalizeQrStyleColor(qrStyle.qr_corners_dot_gradient_color_1, normalizeQrStyleColor(qrStyle.qr_corners_dot_color, '#000000')) }, { offset: 1, color: normalizeQrStyleColor(qrStyle.qr_corners_dot_gradient_color_2, '#ffffff') } ] } : null; return { width: QR_PNG_WIDTH, height: QR_PNG_WIDTH, type: 'canvas', data: text, margin: normalizeQrStyleNumber(qrStyle.qr_margin, 10, 0, undefined), qrOptions: {}, dotsOptions: { type: String(qrStyle.qr_dots_type || 'square').trim() || 'square', color: normalizeQrStyleColor(qrStyle.qr_dots_color, '#000000'), gradient: dotsGradient || undefined }, cornersSquareOptions: { type: String(qrStyle.qr_corners_square_type || 'square').trim() || 'square', color: normalizeQrStyleColor(qrStyle.qr_corners_square_color, '#000000'), gradient: cornersSquareGradient || undefined }, cornersDotOptions: { type: String(qrStyle.qr_corners_dot_type || 'square').trim() || 'square', color: normalizeQrStyleColor(qrStyle.qr_corners_dot_color, '#000000'), gradient: cornersDotGradient || undefined }, backgroundOptions: { color: qrStyle.qr_background_transparent ? 'transparent' : normalizeQrStyleColor(qrStyle.qr_background_color, '#ffffff') }, image: String(qrStyle.qr_image || '').trim() || undefined, imageOptions: { hideBackgroundDots: Boolean(qrStyle.qr_image_hide_background_dots), imageSize: normalizeQrStyleNumber(qrStyle.qr_image_size, 0.4, 0, 1), margin: normalizeQrStyleNumber(qrStyle.qr_image_margin, 5, 0, undefined) } }; } async function createStyledQrCodeDataUrl(value) { const source = value && typeof value === 'object' ? value : { value: value }; const options = buildQrStylingOptions(source); if (!options.data) { return ''; } return svgToDataUrl(await createStyledQrCodeSvg(options.data)); } async function createStyledQrCodeSvg(value) { const source = value && typeof value === 'object' ? value : { value: value }; const options = buildQrStylingOptions(source); if (!options.data) { return ''; } return renderStyledQrRawData(options, 'svg'); } async function createQrCodeSvg(value) { return createStyledQrCodeSvg(value); } async function createQrCodeDataUrl(value) { return createStyledQrCodeDataUrl(value); } async function buildQrCodeContent(value, options) { const source = value && typeof value === 'object' ? value : { value: value }; const forcePreviewRefresh = Boolean(options && options.forcePreviewRefresh); const text = String(source.value === undefined || source.value === null ? '' : source.value).trim(); const hasBooleanField = function (key) { return Object.prototype.hasOwnProperty.call(source, key); }; const margin = Number(source.qr_margin); const normalizeHex = function (value) { const raw = String(value === undefined || value === null ? '' : value).trim(); return raw || undefined; }; const normalizeNumber = function (value, min, max, round) { const parsed = Number(value); if (!Number.isFinite(parsed)) { return undefined; } let next = parsed; if (round) { next = Math.round(next); } if (typeof min === 'number') { next = Math.max(min, next); } if (typeof max === 'number') { next = Math.min(max, next); } return next; }; const style = { qr_margin: Number.isFinite(margin) && margin >= 0 ? Math.round(margin) : undefined, qr_border_radius: Number.isFinite(Number(source.qr_border_radius)) ? Math.max(0, Math.round(Number(source.qr_border_radius))) : undefined, qr_background_color: String(source.qr_background_color === undefined || source.qr_background_color === null ? '' : source.qr_background_color).trim() || undefined, qr_background_transparent: hasBooleanField('qr_background_transparent') ? Boolean(source.qr_background_transparent) : undefined, qr_background_color_mode: 'single', qr_background_gradient_type: String(source.qr_background_gradient_type === undefined || source.qr_background_gradient_type === null ? '' : source.qr_background_gradient_type).trim() || undefined, qr_background_gradient_rotation: normalizeNumber(source.qr_background_gradient_rotation), qr_background_gradient_color_1: normalizeHex(source.qr_background_gradient_color_1), qr_background_gradient_color_2: normalizeHex(source.qr_background_gradient_color_2), qr_dots_color: String(source.qr_dots_color === undefined || source.qr_dots_color === null ? '' : source.qr_dots_color).trim() || undefined, qr_dots_type: String(source.qr_dots_type === undefined || source.qr_dots_type === null ? '' : source.qr_dots_type).trim() || undefined, qr_dots_color_mode: String(source.qr_dots_color_mode === undefined || source.qr_dots_color_mode === null ? '' : source.qr_dots_color_mode).trim() || undefined, qr_dots_gradient_type: String(source.qr_dots_gradient_type === undefined || source.qr_dots_gradient_type === null ? '' : source.qr_dots_gradient_type).trim() || undefined, qr_dots_gradient_rotation: normalizeNumber(source.qr_dots_gradient_rotation), qr_dots_gradient_color_1: normalizeHex(source.qr_dots_gradient_color_1), qr_dots_gradient_color_2: normalizeHex(source.qr_dots_gradient_color_2), qr_corners_square_color: String(source.qr_corners_square_color === undefined || source.qr_corners_square_color === null ? '' : source.qr_corners_square_color).trim() || undefined, qr_corners_square_type: String(source.qr_corners_square_type === undefined || source.qr_corners_square_type === null ? '' : source.qr_corners_square_type).trim() || undefined, qr_corners_square_color_mode: String(source.qr_corners_square_color_mode === undefined || source.qr_corners_square_color_mode === null ? '' : source.qr_corners_square_color_mode).trim() || undefined, qr_corners_square_gradient_type: String(source.qr_corners_square_gradient_type === undefined || source.qr_corners_square_gradient_type === null ? '' : source.qr_corners_square_gradient_type).trim() || undefined, qr_corners_square_gradient_rotation: normalizeNumber(source.qr_corners_square_gradient_rotation), qr_corners_square_gradient_color_1: normalizeHex(source.qr_corners_square_gradient_color_1), qr_corners_square_gradient_color_2: normalizeHex(source.qr_corners_square_gradient_color_2), qr_corners_dot_color: String(source.qr_corners_dot_color === undefined || source.qr_corners_dot_color === null ? '' : source.qr_corners_dot_color).trim() || undefined, qr_corners_dot_type: String(source.qr_corners_dot_type === undefined || source.qr_corners_dot_type === null ? '' : source.qr_corners_dot_type).trim() || undefined, qr_corners_dot_color_mode: String(source.qr_corners_dot_color_mode === undefined || source.qr_corners_dot_color_mode === null ? '' : source.qr_corners_dot_color_mode).trim() || undefined, qr_corners_dot_gradient_type: String(source.qr_corners_dot_gradient_type === undefined || source.qr_corners_dot_gradient_type === null ? '' : source.qr_corners_dot_gradient_type).trim() || undefined, qr_corners_dot_gradient_rotation: normalizeNumber(source.qr_corners_dot_gradient_rotation), qr_corners_dot_gradient_color_1: normalizeHex(source.qr_corners_dot_gradient_color_1), qr_corners_dot_gradient_color_2: normalizeHex(source.qr_corners_dot_gradient_color_2), qr_image: String(source.qr_image === undefined || source.qr_image === null ? '' : source.qr_image).trim() || undefined, qr_image_size: Number.isFinite(Number(source.qr_image_size)) ? Math.max(0, Math.min(1, Number(source.qr_image_size))) : undefined, qr_image_margin: Number.isFinite(Number(source.qr_image_margin)) ? Math.max(0, Math.round(Number(source.qr_image_margin))) : undefined, qr_image_hide_background_dots: hasBooleanField('qr_image_hide_background_dots') ? Boolean(source.qr_image_hide_background_dots) : undefined }; const content = { type: 'qr-code', value: text }; Object.keys(style).forEach((key) => { if (style[key] !== undefined) { content[key] = style[key]; } }); content.qr_background_use_gradient = false; content.qr_dots_use_gradient = content.qr_dots_gradient_type && content.qr_dots_gradient_type !== 'none'; content.qr_corners_square_use_gradient = content.qr_corners_square_gradient_type && content.qr_corners_square_gradient_type !== 'none'; content.qr_corners_dot_use_gradient = content.qr_corners_dot_gradient_type && content.qr_corners_dot_gradient_type !== 'none'; const svg = String(source.qr_svg === undefined || source.qr_svg === null ? '' : source.qr_svg).trim(); if (svg) { content.qr_svg = svg; } const preview = forcePreviewRefresh ? '' : String(source.qr_preview === undefined || source.qr_preview === null ? '' : source.qr_preview).trim(); if (preview) { content.qr_preview = preview; } if (text && !content.qr_preview) { const generatedSvg = await createQrCodeSvg(text); if (generatedSvg) { content.qr_svg = generatedSvg; } const generatedDataUrl = await createStyledQrCodeDataUrl(content); if (generatedDataUrl) { content.qr_preview = generatedDataUrl; } } return content; } module.exports = { createQrCodeSvg, createStyledQrCodeSvg, createStyledQrCodeDataUrl, createQrCodeDataUrl, buildQrCodeContent };