UNPKG

@akamfoad/qrcode

Version:

The library is generating QR codes as SVG, HTML5 Canvas, PNG and JPG files, or text.

284 lines (241 loc) 6.94 kB
import type { OptionsType as ParentOptionsType } from './AbstractQRCodeWithImage'; import AbstractQRCodeWithImage from './AbstractQRCodeWithImage'; import type { ImageConfigType } from './AbstractQRCodeWithImage'; const TYPE_INT_WHITE = 0; const TYPE_INT_BLACK = 1; const TYPE_INT_PROCESSED = 2; export type DataIntType = ( | typeof TYPE_INT_WHITE | typeof TYPE_INT_BLACK | typeof TYPE_INT_PROCESSED )[][]; type RectType = { x: number; y: number; width?: number; height?: number; id?: string | null; }; type RectsMapItemType = { count: number; rect: RectType; id: string | null; relative: boolean; }; export type OptionsType = ParentOptionsType & { fgColor: string; bgColor?: string; width?: number; height?: number; }; const DEFAULT_OPTIONS = { fgColor: '#000', bgColor: '#FFF', }; export default class QRCodeSVG extends AbstractQRCodeWithImage { fgColor: string; bgColor: string; qrCodeSVG: string | null = null; height?: number; width?: number; qrCodeDataUrl: string | null = null; constructor(value: string, options: Partial<OptionsType> = {}) { super(value, options); const params = { ...DEFAULT_OPTIONS, ...options }; this.fgColor = params.fgColor; this.bgColor = params.bgColor; this.width = params.width; this.height = params.height; } _clearCache(): void { super._clearCache(); this.qrCodeSVG = null; this.qrCodeDataUrl = null; } _getDataInt(): DataIntType | null { const data = this.getData(); if (!data) { return null; } // copy boolean[][] to number[][] return data.map((row) => { return row.map((isBlack) => { return isBlack ? TYPE_INT_BLACK : TYPE_INT_WHITE; }); }); } _getRects(): RectType[] | null { const dataInt = this._getDataInt(); if (!dataInt) { return null; } const rects: RectType[] = []; const count = dataInt.length - 1; for (let y = 0; y <= count; y += 1) { let begX = -1; for (let x = 0; x <= count; x += 1) { const intType = dataInt[y][x]; const isLast = x === count; // will check processed items too // const isBlack = intType !== TYPE_INT_WHITE; // or will skip processed items const isBlack = intType === TYPE_INT_BLACK; if (isBlack && begX === -1) { begX = x; } if (begX !== -1 && (isLast || !isBlack)) { const endX = x - (isBlack ? 0 : 1); const rect = this._processRect(dataInt, begX, endX, y); if (rect) { rects.push(rect); } begX = -1; } } } return rects; } _processRect( dataInt: DataIntType, begX: number, endX: number, begY: number, ): RectType | null { const count = dataInt.length - 1; let isNewRect = false; let isStopped = false; let height = 0; for (let y = begY; y <= count; y += 1) { for (let x = begX; x <= endX; x += 1) { const intType = dataInt[y][x]; if (intType === TYPE_INT_WHITE) { isStopped = true; break; } } if (isStopped) { break; } for (let x = begX; x <= endX; x += 1) { if (dataInt[y][x] === TYPE_INT_BLACK) { isNewRect = true; dataInt[y][x] = TYPE_INT_PROCESSED; } } height += 1; } if (!isNewRect) { return null; } return { x: begX, y: begY, width: endX - begX + 1, height, }; } _getRelativeRects(): RectType[] | null { const rects = this._getRects(); if (!rects) { return null; } const relativeRects: RectType[] = []; const rectsMap: Record<string, RectsMapItemType> = {}; let seqRectId = 0; rects.forEach((rect: RectType) => { const key = `${rect.width}:${rect.height}`; if (rectsMap[key]) { rectsMap[key].count += 1; if (!rectsMap[key].id) { rectsMap[key].id = `i${seqRectId.toString(32)}`; seqRectId += 1; } } else { rectsMap[key] = { count: 1, rect, relative: false, id: null }; } }); rects.forEach((rect: RectType) => { const key = `${rect.width}:${rect.height}`; const rectsMapItem: RectsMapItemType = rectsMap[key]; if (rectsMapItem.relative) { relativeRects.push({ id: rectsMapItem.id, x: rect.x - rectsMapItem.rect.x, y: rect.y - rectsMapItem.rect.y, }); } else { if (rectsMapItem.id) { rect.id = rectsMapItem.id; rectsMapItem.relative = true; } relativeRects.push(rect); } }); return relativeRects; } _buildSVG(rects: RectType[]): string { const size = this.getDataSize(); const tags = [ '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ' + `shape-rendering="crispEdges" viewBox="0 0 ${size} ${size}"${ this.width ? ` width=${this.width}` : '' }${this.height ? ` height=${this.height}` : ''} >`, ]; if (this.bgColor) { tags.push( `<rect x="0" y="0" height="${size}" width="${size}" fill="${this.bgColor}"/>`, ); } rects.forEach((rect: RectType) => { if (rect.width && rect.height) { const rectId = rect.id ? `id="${rect.id}" ` : ''; tags.push( `<rect ${rectId}x="${rect.x}" y="${rect.y}" height="${rect.height}" width="${rect.width}" fill="${this.fgColor}"/>`, ); } else { tags.push( `<use xlink:href="#${rect.id}" x="${rect.x}" y="${rect.y}"/>`, ); } }); // @ts-expect-error make types stronger const imageConfig: ImageConfigType = this._getImageConfig(); if (imageConfig && imageConfig.width && imageConfig.height) { tags.push( `<image xlink:href="${imageConfig.source}" x="${imageConfig.x}" y="${imageConfig.y}" width="${imageConfig.width}" height="${imageConfig.height}"/>`, ); } tags.push('</svg>'); return tags.join(''); } toString(): null | string { if (!this.qrCodeSVG) { const dataSize = this.getDataSize(); if (!dataSize) { return null; } const rects = this._getRects(); if (!rects) { return null; } this.qrCodeSVG = this._buildSVG(rects); } return this.qrCodeSVG; } toDataUrl(): null | string { if (!this.qrCodeDataUrl) { const dataSize = this.getDataSize(); if (!dataSize) { return null; } const relativeRects = this._getRelativeRects(); if (!relativeRects) { return null; } // svg based on relative rects has min 20% less length const svg = this._buildSVG(relativeRects); this.qrCodeDataUrl = `data:image/svg+xml;base64,${btoa(svg)}`; } return this.qrCodeDataUrl; } }