-
Notifications
You must be signed in to change notification settings - Fork 335
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
separate components in different files
- Loading branch information
Showing
4 changed files
with
363 additions
and
349 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import React, {useRef, useEffect} from 'react'; | ||
import qrcodegen from './third-party/qrcodegen'; | ||
import { QRProps, DEFAULT_PROPS, MARGIN_SIZE, ERROR_LEVEL_MAP, generatePath, excavateModules, getImageSettings } from './helpers'; | ||
|
||
|
||
// For canvas we're going to switch our drawing mode based on whether or not | ||
// the environment supports Path2D. We only need the constructor to be | ||
// supported, but Edge doesn't actually support the path (string) type | ||
// argument. Luckily it also doesn't support the addPath() method. We can | ||
// treat that as the same thing. | ||
const SUPPORTS_PATH2D = (function () { | ||
try { | ||
new Path2D().addPath(new Path2D()); | ||
} catch (e) { | ||
return false; | ||
} | ||
return true; | ||
})(); | ||
|
||
|
||
export default function QRCodeCanvas(props: QRProps) { | ||
const _canvas = useRef<HTMLCanvasElement>(null); | ||
const _image = useRef<HTMLImageElement>(null); | ||
|
||
function update() { | ||
const {value, size, level, bgColor, fgColor, includeMargin} = props; | ||
|
||
if (_canvas.current != null) { | ||
const canvas = _canvas.current; | ||
|
||
const ctx = canvas.getContext('2d'); | ||
if (!ctx) { | ||
return; | ||
} | ||
|
||
let cells = qrcodegen.QrCode.encodeText( | ||
value, | ||
ERROR_LEVEL_MAP[level] | ||
).getModules(); | ||
|
||
const margin = includeMargin ? MARGIN_SIZE : 0; | ||
const numCells = cells.length + margin * 2; | ||
const calculatedImageSettings = getImageSettings(props, cells); | ||
|
||
const image = _image.current; | ||
const haveImageToRender = | ||
calculatedImageSettings != null && | ||
image !== null && | ||
image.complete && | ||
image.naturalHeight !== 0 && | ||
image.naturalWidth !== 0; | ||
|
||
if (haveImageToRender) { | ||
if (calculatedImageSettings.excavation != null) { | ||
cells = excavateModules(cells, calculatedImageSettings.excavation); | ||
} | ||
} | ||
|
||
// We're going to scale this so that the number of drawable units | ||
// matches the number of cells. This avoids rounding issues, but does | ||
// result in some potentially unwanted single pixel issues between | ||
// blocks, only in environments that don't support Path2D. | ||
const pixelRatio = window.devicePixelRatio || 1; | ||
canvas.height = canvas.width = size * pixelRatio; | ||
const scale = (size / numCells) * pixelRatio; | ||
ctx.scale(scale, scale); | ||
|
||
// Draw solid background, only paint dark modules. | ||
ctx.fillStyle = bgColor; | ||
ctx.fillRect(0, 0, numCells, numCells); | ||
|
||
ctx.fillStyle = fgColor; | ||
if (SUPPORTS_PATH2D) { | ||
// $FlowFixMe: Path2D c'tor doesn't support args yet. | ||
ctx.fill(new Path2D(generatePath(cells, margin))); | ||
} else { | ||
cells.forEach(function (row, rdx) { | ||
row.forEach(function (cell, cdx) { | ||
if (cell) { | ||
ctx.fillRect(cdx + margin, rdx + margin, 1, 1); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
if (haveImageToRender) { | ||
ctx.drawImage( | ||
image, | ||
calculatedImageSettings.x + margin, | ||
calculatedImageSettings.y + margin, | ||
calculatedImageSettings.w, | ||
calculatedImageSettings.h | ||
); | ||
} | ||
} | ||
} | ||
|
||
useEffect(() => { | ||
// Always update the canvas. It's cheap enough and we want to be correct | ||
// with the current state. | ||
update(); | ||
}); | ||
|
||
const { | ||
value, | ||
size, | ||
level, | ||
bgColor, | ||
fgColor, | ||
style, | ||
includeMargin, | ||
imageSettings, | ||
...otherProps | ||
} = props; | ||
const canvasStyle = {height: size, width: size, ...style}; | ||
let img = null; | ||
let imgSrc = imageSettings?.src; | ||
if (imgSrc != null) { | ||
img = ( | ||
<img | ||
src={imgSrc} | ||
key={imgSrc} | ||
style={{display: 'none'}} | ||
onLoad={() => { | ||
update(); | ||
}} | ||
ref={_image} | ||
/> | ||
); | ||
} | ||
return ( | ||
<> | ||
<canvas | ||
style={canvasStyle} | ||
height={size} | ||
width={size} | ||
ref={_canvas} | ||
{...otherProps} | ||
/> | ||
{img} | ||
</> | ||
); | ||
} | ||
|
||
QRCodeCanvas.defaultProps = DEFAULT_PROPS; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import qrcodegen from './third-party/qrcodegen'; | ||
import type {CSSProperties} from 'react'; | ||
|
||
type Modules = ReturnType<qrcodegen.QrCode['getModules']>; | ||
type Excavation = {x: number; y: number; w: number; h: number}; | ||
|
||
export const ERROR_LEVEL_MAP: {[index: string]: qrcodegen.QrCode.Ecc} = { | ||
L: qrcodegen.QrCode.Ecc.LOW, | ||
M: qrcodegen.QrCode.Ecc.MEDIUM, | ||
Q: qrcodegen.QrCode.Ecc.QUARTILE, | ||
H: qrcodegen.QrCode.Ecc.HIGH, | ||
}; | ||
|
||
export type QRProps = { | ||
value: string; | ||
size: number; | ||
// Should be a real enum, but doesn't seem to be compatible with real code. | ||
level: string; | ||
bgColor: string; | ||
fgColor: string; | ||
style?: CSSProperties; | ||
includeMargin: boolean; | ||
imageSettings?: { | ||
src: string; | ||
height: number; | ||
width: number; | ||
excavate: boolean; | ||
x?: number; | ||
y?: number; | ||
}; | ||
}; | ||
|
||
export const DEFAULT_PROPS = { | ||
size: 128, | ||
level: 'L', | ||
bgColor: '#FFFFFF', | ||
fgColor: '#000000', | ||
includeMargin: false, | ||
}; | ||
|
||
export const MARGIN_SIZE = 4; | ||
|
||
// This is *very* rough estimate of max amount of QRCode allowed to be covered. | ||
// It is "wrong" in a lot of ways (area is a terrible way to estimate, it | ||
// really should be number of modules covered), but if for some reason we don't | ||
// get an explicit height or width, I'd rather default to something than throw. | ||
const DEFAULT_IMG_SCALE = 0.1; | ||
|
||
export function generatePath(modules: Modules, margin: number = 0): string { | ||
const ops: Array<string> = []; | ||
modules.forEach(function (row, y) { | ||
let start: number | null = null; | ||
row.forEach(function (cell, x) { | ||
if (!cell && start !== null) { | ||
// M0 0h7v1H0z injects the space with the move and drops the comma, | ||
// saving a char per operation | ||
ops.push( | ||
`M${start + margin} ${y + margin}h${x - start}v1H${start + margin}z` | ||
); | ||
start = null; | ||
return; | ||
} | ||
|
||
// end of row, clean up or skip | ||
if (x === row.length - 1) { | ||
if (!cell) { | ||
// We would have closed the op above already so this can only mean | ||
// 2+ light modules in a row. | ||
return; | ||
} | ||
if (start === null) { | ||
// Just a single dark module. | ||
ops.push(`M${x + margin},${y + margin} h1v1H${x + margin}z`); | ||
} else { | ||
// Otherwise finish the current line. | ||
ops.push( | ||
`M${start + margin},${y + margin} h${x + 1 - start}v1H${ | ||
start + margin | ||
}z` | ||
); | ||
} | ||
return; | ||
} | ||
|
||
if (cell && start === null) { | ||
start = x; | ||
} | ||
}); | ||
}); | ||
return ops.join(''); | ||
} | ||
|
||
// We could just do this in generatePath, except that we want to support | ||
// non-Path2D canvas, so we need to keep it an explicit step. | ||
export function excavateModules(modules: Modules, excavation: Excavation): Modules { | ||
return modules.slice().map((row, y) => { | ||
if (y < excavation.y || y >= excavation.y + excavation.h) { | ||
return row; | ||
} | ||
return row.map((cell, x) => { | ||
if (x < excavation.x || x >= excavation.x + excavation.w) { | ||
return cell; | ||
} | ||
return false; | ||
}); | ||
}); | ||
} | ||
|
||
export function getImageSettings( | ||
props: QRProps, | ||
cells: Modules | ||
): null | { | ||
x: number; | ||
y: number; | ||
h: number; | ||
w: number; | ||
excavation: Excavation | null; | ||
} { | ||
const {imageSettings, size, includeMargin} = props; | ||
if (imageSettings == null) { | ||
return null; | ||
} | ||
const margin = includeMargin ? MARGIN_SIZE : 0; | ||
const numCells = cells.length + margin * 2; | ||
const defaultSize = Math.floor(size * DEFAULT_IMG_SCALE); | ||
const scale = numCells / size; | ||
const w = (imageSettings.width || defaultSize) * scale; | ||
const h = (imageSettings.height || defaultSize) * scale; | ||
const x = | ||
imageSettings.x == null | ||
? cells.length / 2 - w / 2 | ||
: imageSettings.x * scale; | ||
const y = | ||
imageSettings.y == null | ||
? cells.length / 2 - h / 2 | ||
: imageSettings.y * scale; | ||
|
||
let excavation = null; | ||
if (imageSettings.excavate) { | ||
let floorX = Math.floor(x); | ||
let floorY = Math.floor(y); | ||
let ceilW = Math.ceil(w + x - floorX); | ||
let ceilH = Math.ceil(h + y - floorY); | ||
excavation = {x: floorX, y: floorY, w: ceilW, h: ceilH}; | ||
} | ||
|
||
return {x, y, h, w, excavation}; | ||
} |
Oops, something went wrong.