From fcfc0107c275023badc84512de8cd89d105c73a7 Mon Sep 17 00:00:00 2001 From: Mike D Sutton Date: Wed, 12 Feb 2025 23:23:09 +0000 Subject: [PATCH] Added svg-round renderer --- README.md | 14 +++-- lib/renderer/svg-round.js | 118 ++++++++++++++++++++++++++++++++++++++ lib/server.js | 7 +++ 3 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 lib/renderer/svg-round.js diff --git a/README.md b/README.md index 510e6b9f..e2ffaf74 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ QR Code options: -m, --mask Mask pattern (0 - 7) [number] Renderer options: - -t, --type Output type [choices: "png", "svg", "utf8"] + -t, --type Output type [choices: "png", "svg", "svg-round", "utf8"] -w, --width Image width (px) [number] -s, --scale Scale factor [number] -q, --qzone Quiet zone size [number] @@ -513,7 +513,7 @@ Text to encode or a list of objects describing segments. Default: `utf8` Output format.
- Possible values are: `terminal`,`utf8`, and `svg`. + Possible values are: `terminal`, `utf8`, `svg` and `svg-round`. See [Options](#options) for other settings. @@ -578,7 +578,7 @@ Callback function called on finish. #### `toString(text, [options], [cb(error, string)])` Returns a string representation of the QR Code.
-If choosen output format is `svg` it will returns a string containing xml code. +If chosen output format is `svg` or `svg-round` it will return a string containing xml code. ##### `text` Type: `String|Array` @@ -591,7 +591,7 @@ Text to encode or a list of objects describing segments. Default: `utf8` Output format.
- Possible values are: `utf8`, `svg`, `terminal`. + Possible values are: `utf8`, `svg`, `svg-round`, `terminal`. See [Options](#options) for other settings. @@ -752,6 +752,12 @@ Default: `#ffffffff` Color of light module. Value must be in hex format (RGBA).
+##### `rendererOpts.corners.topLeft`, `rendererOpts.corners.topRight`, `rendererOpts.corners.bottomLeft`, `rendererOpts.corners.bottomRight` +Type: `Number`
+Default: `0` + +Radius of top-left, top-right, bottom-left and bottom-right corners respectively. Value should be between `0.0` - `1.0`, however larger values will be scaled down proportionally. Only works with `svg-round` format for now. +
## GS1 QR Codes diff --git a/lib/renderer/svg-round.js b/lib/renderer/svg-round.js new file mode 100644 index 00000000..fd94a9dd --- /dev/null +++ b/lib/renderer/svg-round.js @@ -0,0 +1,118 @@ +const Utils = require('./utils') + +function getColorAttrib (color, attrib) { + const alpha = color.a / 255 + const str = attrib + '="' + color.hex + '"' + + return alpha < 1 + ? str + ' ' + attrib + '-opacity="' + alpha.toFixed(2).slice(1) + '"' + : str +} + +/** Converts a number to string with leading and trailing zeros stripped @param {number} num */ +function svgNum(num) { + return num.toFixed(3).replace(/\.?0+$/, "").replace(/^0(?=\.)/, ""); +} + +/** @param {{ tl:number, tr:number, bl:number, br:number }} corners */ +function qrToPathCorners(data, size, margin, corners) { + let path = ""; + let run = 0, i = 0; + + // Calculate scale ratio and corner sizes + const s = Math.min(1, 1 / Math.max(corners.tl + corners.tr, corners.bl + corners.br)); + const tl = Math.max(corners.tl, 0) * s, stl = svgNum(tl); + const tr = Math.max(corners.tr, 0) * s, str = svgNum(tr); + const bl = Math.max(corners.bl, 0) * s, sbl = svgNum(bl); + const br = Math.max(corners.br, 0) * s, sbr = svgNum(br); + + for (let y = 0; y < size; ++y) { + for (let x = 0; x < size; ++x, ++i) { + if (!data[i]) { continue; } + + const top = (y === 0) || !data[i - size]; + const bot = (y === (size - 1)) || !data[i + size]; + + if (!run++) { + path += (bot && bl) + ? `M${svgNum(x + margin + bl)},${y + margin + 1}a${sbl},${sbl} 0 0 1 -${sbl},-${sbl}` + : `M${svgNum(x + margin)},${y + margin + 1}` + (bl ? `v-${sbl}` : ""); + + path += (top && tl) + ? `V${svgNum(y + margin + tl)}a${stl},${stl} 0 0 1 ${stl},-${stl}` + : `V${svgNum(y + margin)}`; + } + + if ((x === (size - 1)) || !data[i + 1]) { + path += (top && tr) + ? `H${svgNum(x + margin + 1 - tr)}a${str},${str} 0 0 1 ${str},${str}` + : `H${svgNum(x + margin + 1)}` + (tr ? `v${str}` : ""); + + path += (bot && br) + ? `V${svgNum(y + margin + 1 - br)}a${sbr},${sbr} 0 0 1 -${sbr},${sbr}z` + : `V${svgNum(y + margin + 1)}z`; + + run = 0; + } + } + } + + return path; +} + +exports.render = function render (qrData, options, cb) { + const opts = Utils.getOptions(options) + const size = qrData.modules.size + const data = qrData.modules.data + const qrcodesize = size + opts.margin * 2 + + const bg = !opts.color.light.a + ? '' + : `` + + const path = ``; + + const viewBox = `viewBox="0 0 ${qrcodesize} ${qrcodesize}"`; + const width = !opts.width ? '' : `width="${opts.width}" height="${opts.width}" `; + const svgTag = `${bg}${path}\n`; + + if (typeof cb === 'function') { + cb(null, svgTag) + } + + return svgTag +} + +exports.renderToDataURL = function renderToDataURL (qrData, options, cb) { + if (typeof cb === 'undefined') { + cb = options + options = undefined + } + + const svgTag = exports.render(qrData, options); + const uriStr = `data:image/svg+xml;base64,${Buffer.from(svgTag).toString('base64')}`; + + cb(null, uriStr) +}; + +exports.renderToFile = function renderToFile (path, qrData, options, cb) { + if (typeof cb === 'undefined') { + cb = options + options = undefined + } + + const fs = require('fs') + const svgTag = exports.render(qrData, options) + + const xmlStr = '' + + '' + + svgTag + + fs.writeFile(path, xmlStr, cb) +} diff --git a/lib/server.js b/lib/server.js index 619e398e..60a4302a 100644 --- a/lib/server.js +++ b/lib/server.js @@ -4,6 +4,7 @@ const PngRenderer = require('./renderer/png') const Utf8Renderer = require('./renderer/utf8') const TerminalRenderer = require('./renderer/terminal') const SvgRenderer = require('./renderer/svg') +const SvgRoundRenderer = require('./renderer/svg-round') function checkParams (text, opts, cb) { if (typeof text === 'undefined') { @@ -39,6 +40,9 @@ function getRendererFromType (type) { case 'svg': return SvgRenderer + case 'svg-round': + return SvgRoundRenderer + case 'txt': case 'utf8': return Utf8Renderer @@ -55,6 +59,9 @@ function getStringRendererFromType (type) { case 'svg': return SvgRenderer + case 'svg-round': + return SvgRoundRenderer + case 'terminal': return TerminalRenderer