From c4817f681bda9a6b345da7dbd26146249ebceade Mon Sep 17 00:00:00 2001 From: Dafnik Date: Fri, 2 Feb 2024 23:57:40 +0100 Subject: [PATCH] feat: add svg qrcode --- libs/dfts-qrcode/README.md | 49 ++++- libs/dfts-qrcode/src/lib/qrcode.spec.ts | 233 +++++++++++++++++++++++- libs/dfts-qrcode/src/lib/qrcode.ts | 84 ++++++++- 3 files changed, 359 insertions(+), 7 deletions(-) diff --git a/libs/dfts-qrcode/README.md b/libs/dfts-qrcode/README.md index d3797e64..16c9ab0f 100644 --- a/libs/dfts-qrcode/README.md +++ b/libs/dfts-qrcode/README.md @@ -10,7 +10,8 @@ Fully type-safe and esm module compatible. - [Installation](#installation) - [Usage](#usage) - [HTMLImageElement](#htmlimageelement) - - [HTMLCanvasElemtn](#htmlcanvaselement) + - [HTMLCanvasElement](#htmlcanvaselement) + - [SVG](#svg) - [Error Correction Level](#error-correction-level) - [QR Code capacity](#qr-code-capacity) - [Encoding modes](#encoding-modes) @@ -81,7 +82,7 @@ Options: ```typescript import { generateQrCodeCanvas$ } from 'dfts-qrcode'; -generateQrCodeCanvas$('data', {image: {src: './assets/logo.png'}}).then((qrCode) => { +generateQrCodeCanvas$('data', {image: {src: './assets/logo.png'}}).then((qrcode) => { ... }) ``` @@ -91,6 +92,50 @@ Options: - `generateOptions` - `generateWithImageOptions` +### SVG + +#### Without center image + +```typescript +import { generateQrCodeSVG } from 'dfts-qrcode'; + +const { svg, dataUrl } = generateQrCodeSVG('data'); +``` + +Options: + +- `generateOptions` +- `generateWithAccessibleOptions` + +#### Without center image as string + +```typescript +import { generateQrCodeSVGString } from 'dfts-qrcode'; + +const svgString = generateQrCodeSVGString('data'); +``` + +Options: + +- `generateOptions` +- `generateWithAccessibleOptions` + +#### With center image + +```typescript +import { generateQrCodeSVG$ } from 'dfts-qrcode'; + +generateQrCodeSVG$('data', {image: {src: './assets/logo.png'}}).then(({ svg, dataUrl }) => { + ... +}) +``` + +Options: + +- `generateOptions` +- `generateWithAccessibleOptions` +- `generateWithImageOptions` + ### QR-Code Matrix ```typescript diff --git a/libs/dfts-qrcode/src/lib/qrcode.spec.ts b/libs/dfts-qrcode/src/lib/qrcode.spec.ts index 813ed18b..9e5f1280 100644 --- a/libs/dfts-qrcode/src/lib/qrcode.spec.ts +++ b/libs/dfts-qrcode/src/lib/qrcode.spec.ts @@ -1,6 +1,237 @@ -import { generateQrCodeMatrix } from './qrcode'; +import { generateQrCodeMatrix, generateQrCodeSVGString } from "./qrcode"; +import { s_stripWhitespace } from "dfts-helper"; describe('QRCode', () => { + it('generate correct Numeric qrcode svg', () => { + const test = generateQrCodeSVGString('123456798', { mode: 'numeric' }) + console.log(test) + expect(s_stripWhitespace(test)).toEqual(s_stripWhitespace(` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `)); + }); it('generate correct Numeric qrcode matrix', () => { expect(generateQrCodeMatrix('123456798', { mode: 'numeric' })).toStrictEqual([ [1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1], diff --git a/libs/dfts-qrcode/src/lib/qrcode.ts b/libs/dfts-qrcode/src/lib/qrcode.ts index b2148482..909aac8a 100644 --- a/libs/dfts-qrcode/src/lib/qrcode.ts +++ b/libs/dfts-qrcode/src/lib/qrcode.ts @@ -903,6 +903,82 @@ export function generateQrCodeMatrix(data: string | number, options: generateMat return generate(newData, ver, mode, ecclevel, mask); } +export function generateQrCodeSVGString( + data: string | number, + options: generateOptions & generateWithAccessibleOptions = {}, +): string { + const matrix = generateQrCodeMatrix(data, options); + const modsize = options.size ?? 5; + const margin = options.margin ?? 4; + const fgColor = options.colors?.colorLight ?? '#ffffff'; + const bgColor = options.colors?.colorDark ?? '#000000'; + const n = matrix.length; + const size = modsize * (n + 2 * margin); + + const xmls = ['`]; + xmls.push(``); + + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + if (matrix[i][j]) { + xmls.push(``); + } + } + } + + xmls.push(''); + + return xmls.join('\n'); +} + +const parser = new DOMParser(); +export function generateQrCodeSVG( + data: string | number, + options: generateOptions & generateWithAccessibleOptions = {}, +): {svg: HTMLElement, dataUrl: string} { + const svg = generateQrCodeSVGString(data, options); + const base64Svg = btoa(svg); + return {svg: parser.parseFromString(svg, "image/svg+xml").documentElement, dataUrl: `data:image/svg+xml;base64,${base64Svg}` } +} + +export function generateQrCodeSVG$( + data: string | number, + options: generateOptions & generateWithAccessibleOptions & generateWithImageOptions = {}, +): Promise<{svg: HTMLElement, dataUrl: string}> { + const {svg, dataUrl} = generateQrCodeSVG(data, options) + + return new Promise((resolve, reject) => { + if (options.image?.src) { + const centerImageWidth = options.image.width ?? 40; + const centerImageHeight = options.image.height ?? 40; + const centerImage = new Image(centerImageWidth, centerImageHeight); + centerImage.src = options.image.src; + centerImage.onload = () => { + const svgWidth = svg.getAttribute('width')!; + const svgHeight = svg.getAttribute('height')!; + + // Calculate the position to center the image within the SVG + const x = (parseInt(svgWidth) - centerImage.width) / 2; + const y = (parseInt(svgHeight) - centerImage.height) / 2; + + const imageElement = document.createElementNS('http://www.w3.org/2000/svg', 'image'); + imageElement.setAttributeNS(null, 'x', x.toString()); + imageElement.setAttributeNS(null, 'y', y.toString()); + imageElement.setAttributeNS(null, 'width', centerImage.width.toString()); + imageElement.setAttributeNS(null, 'height', centerImage.height.toString()); + imageElement.setAttributeNS('http://www.w3.org/1999/xlink', 'href', options.image!.src!); + + svg.appendChild(imageElement); + + resolve({svg, dataUrl: `data:image/svg+xml;base64,${btoa(new XMLSerializer().serializeToString(svg))}`}); + }; + centerImage.onerror = () => resolve({svg, dataUrl}); + } else { + return resolve({svg, dataUrl}); + } + }); +} + const generateQrCodeCanvasElement = () => document.createElement('canvas'); export function generateQrCodeCanvas( @@ -943,7 +1019,7 @@ export function generateQrCodeImage( options: generateOptions & generateWithAccessibleOptions = {}, canvas: HTMLCanvasElement = generateQrCodeCanvasElement(), image: HTMLImageElement = generateQrCodeImageElement(), -) { +): {image: HTMLImageElement, dataUrl: string} { const dataUrl = generateQrCodeCanvas(data, options, canvas).toDataURL(); image.setAttribute('src', dataUrl); @@ -964,8 +1040,8 @@ export function generateQrCodeCanvas$( return new Promise((resolve) => { if (options.image?.src) { - const centerImageWidth = options.image?.width ?? 40; - const centerImageHeight = options.image?.height ?? 40; + const centerImageWidth = options.image.width ?? 40; + const centerImageHeight = options.image.height ?? 40; const centerImage = new Image(centerImageWidth, centerImageHeight); centerImage.src = options.image.src; centerImage.onload = () => { @@ -990,7 +1066,7 @@ export async function generateQrCodeImage$( options: generateOptions & generateWithAccessibleOptions & generateWithImageOptions = {}, canvas: HTMLCanvasElement = generateQrCodeCanvasElement(), image: HTMLImageElement = generateQrCodeImageElement(), -) { +): Promise<{image: HTMLImageElement, dataUrl: string}> { const dataUrl = (await generateQrCodeCanvas$(data, options, canvas)).toDataURL(); image.setAttribute('src', dataUrl);