Skip to content

Commit 7d6f815

Browse files
committed
separate components in different files
1 parent 9a82c5e commit 7d6f815

File tree

4 files changed

+395
-349
lines changed

4 files changed

+395
-349
lines changed

src/canvas.tsx

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/**
2+
* @license qrcode.react
3+
* Copyright (c) Paul O'Shannessy
4+
* SPDX-License-Identifier: ISC
5+
*/
6+
7+
import React, {useRef, useEffect} from 'react';
8+
import qrcodegen from './third-party/qrcodegen';
9+
import {
10+
QRProps,
11+
DEFAULT_PROPS,
12+
MARGIN_SIZE,
13+
ERROR_LEVEL_MAP,
14+
generatePath,
15+
excavateModules,
16+
getImageSettings,
17+
} from './helpers';
18+
19+
// For canvas we're going to switch our drawing mode based on whether or not
20+
// the environment supports Path2D. We only need the constructor to be
21+
// supported, but Edge doesn't actually support the path (string) type
22+
// argument. Luckily it also doesn't support the addPath() method. We can
23+
// treat that as the same thing.
24+
const SUPPORTS_PATH2D = (function () {
25+
try {
26+
new Path2D().addPath(new Path2D());
27+
} catch (e) {
28+
return false;
29+
}
30+
return true;
31+
})();
32+
33+
export default function QRCodeCanvas(props: QRProps) {
34+
const _canvas = useRef<HTMLCanvasElement>(null);
35+
const _image = useRef<HTMLImageElement>(null);
36+
37+
function update() {
38+
const {value, size, level, bgColor, fgColor, includeMargin} = props;
39+
40+
if (_canvas.current != null) {
41+
const canvas = _canvas.current;
42+
43+
const ctx = canvas.getContext('2d');
44+
if (!ctx) {
45+
return;
46+
}
47+
48+
let cells = qrcodegen.QrCode.encodeText(
49+
value,
50+
ERROR_LEVEL_MAP[level]
51+
).getModules();
52+
53+
const margin = includeMargin ? MARGIN_SIZE : 0;
54+
const numCells = cells.length + margin * 2;
55+
const calculatedImageSettings = getImageSettings(props, cells);
56+
57+
const image = _image.current;
58+
const haveImageToRender =
59+
calculatedImageSettings != null &&
60+
image !== null &&
61+
image.complete &&
62+
image.naturalHeight !== 0 &&
63+
image.naturalWidth !== 0;
64+
65+
if (haveImageToRender) {
66+
if (calculatedImageSettings.excavation != null) {
67+
cells = excavateModules(cells, calculatedImageSettings.excavation);
68+
}
69+
}
70+
71+
// We're going to scale this so that the number of drawable units
72+
// matches the number of cells. This avoids rounding issues, but does
73+
// result in some potentially unwanted single pixel issues between
74+
// blocks, only in environments that don't support Path2D.
75+
const pixelRatio = window.devicePixelRatio || 1;
76+
canvas.height = canvas.width = size * pixelRatio;
77+
const scale = (size / numCells) * pixelRatio;
78+
ctx.scale(scale, scale);
79+
80+
// Draw solid background, only paint dark modules.
81+
ctx.fillStyle = bgColor;
82+
ctx.fillRect(0, 0, numCells, numCells);
83+
84+
ctx.fillStyle = fgColor;
85+
if (SUPPORTS_PATH2D) {
86+
// $FlowFixMe: Path2D c'tor doesn't support args yet.
87+
ctx.fill(new Path2D(generatePath(cells, margin)));
88+
} else {
89+
cells.forEach(function (row, rdx) {
90+
row.forEach(function (cell, cdx) {
91+
if (cell) {
92+
ctx.fillRect(cdx + margin, rdx + margin, 1, 1);
93+
}
94+
});
95+
});
96+
}
97+
98+
if (haveImageToRender) {
99+
ctx.drawImage(
100+
image,
101+
calculatedImageSettings.x + margin,
102+
calculatedImageSettings.y + margin,
103+
calculatedImageSettings.w,
104+
calculatedImageSettings.h
105+
);
106+
}
107+
}
108+
}
109+
110+
useEffect(() => {
111+
// Always update the canvas. It's cheap enough and we want to be correct
112+
// with the current state.
113+
update();
114+
});
115+
116+
const {
117+
value,
118+
size,
119+
level,
120+
bgColor,
121+
fgColor,
122+
style,
123+
includeMargin,
124+
imageSettings,
125+
...otherProps
126+
} = props;
127+
const canvasStyle = {height: size, width: size, ...style};
128+
let img = null;
129+
let imgSrc = imageSettings?.src;
130+
if (imgSrc != null) {
131+
img = (
132+
<img
133+
src={imgSrc}
134+
key={imgSrc}
135+
style={{display: 'none'}}
136+
onLoad={() => {
137+
update();
138+
}}
139+
ref={_image}
140+
/>
141+
);
142+
}
143+
return (
144+
<>
145+
<canvas
146+
style={canvasStyle}
147+
height={size}
148+
width={size}
149+
ref={_canvas}
150+
{...otherProps}
151+
/>
152+
{img}
153+
</>
154+
);
155+
}
156+
157+
QRCodeCanvas.defaultProps = DEFAULT_PROPS;
158+

src/helpers.ts

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

0 commit comments

Comments
 (0)