|
| 1 | +import qrcodegen from './third-party/qrcodegen'; |
| 2 | +import type {CSSProperties} from 'react'; |
| 3 | + |
| 4 | +type Modules = ReturnType<qrcodegen.QrCode['getModules']>; |
| 5 | +type Excavation = {x: number; y: number; w: number; h: number}; |
| 6 | + |
| 7 | +export const ERROR_LEVEL_MAP: {[index: string]: qrcodegen.QrCode.Ecc} = { |
| 8 | + L: qrcodegen.QrCode.Ecc.LOW, |
| 9 | + M: qrcodegen.QrCode.Ecc.MEDIUM, |
| 10 | + Q: qrcodegen.QrCode.Ecc.QUARTILE, |
| 11 | + H: qrcodegen.QrCode.Ecc.HIGH, |
| 12 | +}; |
| 13 | + |
| 14 | +export type QRProps = { |
| 15 | + value: string; |
| 16 | + size: number; |
| 17 | + // Should be a real enum, but doesn't seem to be compatible with real code. |
| 18 | + level: string; |
| 19 | + bgColor: string; |
| 20 | + fgColor: string; |
| 21 | + style?: CSSProperties; |
| 22 | + includeMargin: boolean; |
| 23 | + imageSettings?: { |
| 24 | + src: string; |
| 25 | + height: number; |
| 26 | + width: number; |
| 27 | + excavate: boolean; |
| 28 | + x?: number; |
| 29 | + y?: number; |
| 30 | + }; |
| 31 | +}; |
| 32 | + |
| 33 | +export const DEFAULT_PROPS = { |
| 34 | + size: 128, |
| 35 | + level: 'L', |
| 36 | + bgColor: '#FFFFFF', |
| 37 | + fgColor: '#000000', |
| 38 | + includeMargin: false, |
| 39 | +}; |
| 40 | + |
| 41 | +export const MARGIN_SIZE = 4; |
| 42 | + |
| 43 | +// This is *very* rough estimate of max amount of QRCode allowed to be covered. |
| 44 | +// It is "wrong" in a lot of ways (area is a terrible way to estimate, it |
| 45 | +// really should be number of modules covered), but if for some reason we don't |
| 46 | +// get an explicit height or width, I'd rather default to something than throw. |
| 47 | +const DEFAULT_IMG_SCALE = 0.1; |
| 48 | + |
| 49 | +export function generatePath(modules: Modules, margin: number = 0): string { |
| 50 | + const ops: Array<string> = []; |
| 51 | + modules.forEach(function (row, y) { |
| 52 | + let start: number | null = null; |
| 53 | + row.forEach(function (cell, x) { |
| 54 | + if (!cell && start !== null) { |
| 55 | + // M0 0h7v1H0z injects the space with the move and drops the comma, |
| 56 | + // saving a char per operation |
| 57 | + ops.push( |
| 58 | + `M${start + margin} ${y + margin}h${x - start}v1H${start + margin}z` |
| 59 | + ); |
| 60 | + start = null; |
| 61 | + return; |
| 62 | + } |
| 63 | + |
| 64 | + // end of row, clean up or skip |
| 65 | + if (x === row.length - 1) { |
| 66 | + if (!cell) { |
| 67 | + // We would have closed the op above already so this can only mean |
| 68 | + // 2+ light modules in a row. |
| 69 | + return; |
| 70 | + } |
| 71 | + if (start === null) { |
| 72 | + // Just a single dark module. |
| 73 | + ops.push(`M${x + margin},${y + margin} h1v1H${x + margin}z`); |
| 74 | + } else { |
| 75 | + // Otherwise finish the current line. |
| 76 | + ops.push( |
| 77 | + `M${start + margin},${y + margin} h${x + 1 - start}v1H${ |
| 78 | + start + margin |
| 79 | + }z` |
| 80 | + ); |
| 81 | + } |
| 82 | + return; |
| 83 | + } |
| 84 | + |
| 85 | + if (cell && start === null) { |
| 86 | + start = x; |
| 87 | + } |
| 88 | + }); |
| 89 | + }); |
| 90 | + return ops.join(''); |
| 91 | +} |
| 92 | + |
| 93 | +// We could just do this in generatePath, except that we want to support |
| 94 | +// non-Path2D canvas, so we need to keep it an explicit step. |
| 95 | +export function excavateModules(modules: Modules, excavation: Excavation): Modules { |
| 96 | + return modules.slice().map((row, y) => { |
| 97 | + if (y < excavation.y || y >= excavation.y + excavation.h) { |
| 98 | + return row; |
| 99 | + } |
| 100 | + return row.map((cell, x) => { |
| 101 | + if (x < excavation.x || x >= excavation.x + excavation.w) { |
| 102 | + return cell; |
| 103 | + } |
| 104 | + return false; |
| 105 | + }); |
| 106 | + }); |
| 107 | +} |
| 108 | + |
| 109 | +export function getImageSettings( |
| 110 | + props: QRProps, |
| 111 | + cells: Modules |
| 112 | +): null | { |
| 113 | + x: number; |
| 114 | + y: number; |
| 115 | + h: number; |
| 116 | + w: number; |
| 117 | + excavation: Excavation | null; |
| 118 | +} { |
| 119 | + const {imageSettings, size, includeMargin} = props; |
| 120 | + if (imageSettings == null) { |
| 121 | + return null; |
| 122 | + } |
| 123 | + const margin = includeMargin ? MARGIN_SIZE : 0; |
| 124 | + const numCells = cells.length + margin * 2; |
| 125 | + const defaultSize = Math.floor(size * DEFAULT_IMG_SCALE); |
| 126 | + const scale = numCells / size; |
| 127 | + const w = (imageSettings.width || defaultSize) * scale; |
| 128 | + const h = (imageSettings.height || defaultSize) * scale; |
| 129 | + const x = |
| 130 | + imageSettings.x == null |
| 131 | + ? cells.length / 2 - w / 2 |
| 132 | + : imageSettings.x * scale; |
| 133 | + const y = |
| 134 | + imageSettings.y == null |
| 135 | + ? cells.length / 2 - h / 2 |
| 136 | + : imageSettings.y * scale; |
| 137 | + |
| 138 | + let excavation = null; |
| 139 | + if (imageSettings.excavate) { |
| 140 | + let floorX = Math.floor(x); |
| 141 | + let floorY = Math.floor(y); |
| 142 | + let ceilW = Math.ceil(w + x - floorX); |
| 143 | + let ceilH = Math.ceil(h + y - floorY); |
| 144 | + excavation = {x: floorX, y: floorY, w: ceilW, h: ceilH}; |
| 145 | + } |
| 146 | + |
| 147 | + return {x, y, h, w, excavation}; |
| 148 | +} |
0 commit comments