Skip to content

Commit 355c41b

Browse files
committed
refactor: qrcode #6315
1 parent 3f5f3ec commit 355c41b

File tree

8 files changed

+1363
-157
lines changed

8 files changed

+1363
-157
lines changed

components/qrcode/QRCodeCanvas.tsx

+275
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import type { CSSProperties } from 'vue';
2+
import { defineComponent, ref, watch, computed, watchEffect } from 'vue';
3+
import { qrProps } from './interface';
4+
5+
import qrcodegen from './qrcodegen';
6+
7+
type Modules = ReturnType<qrcodegen.QrCode['getModules']>;
8+
type Excavation = { x: number; y: number; w: number; h: number };
9+
10+
const ERROR_LEVEL_MAP: { [index: string]: qrcodegen.QrCode.Ecc } = {
11+
L: qrcodegen.QrCode.Ecc.LOW,
12+
M: qrcodegen.QrCode.Ecc.MEDIUM,
13+
Q: qrcodegen.QrCode.Ecc.QUARTILE,
14+
H: qrcodegen.QrCode.Ecc.HIGH,
15+
};
16+
17+
type ImageSettings = {
18+
src: string;
19+
height: number;
20+
width: number;
21+
excavate: boolean;
22+
x?: number;
23+
y?: number;
24+
};
25+
26+
const DEFAULT_SIZE = 128;
27+
const DEFAULT_LEVEL = 'L';
28+
const DEFAULT_BGCOLOR = '#FFFFFF';
29+
const DEFAULT_FGCOLOR = '#000000';
30+
const DEFAULT_INCLUDEMARGIN = false;
31+
32+
const SPEC_MARGIN_SIZE = 4;
33+
const DEFAULT_MARGIN_SIZE = 0;
34+
35+
// This is *very* rough estimate of max amount of QRCode allowed to be covered.
36+
// It is "wrong" in a lot of ways (area is a terrible way to estimate, it
37+
// really should be number of modules covered), but if for some reason we don't
38+
// get an explicit height or width, I'd rather default to something than throw.
39+
const DEFAULT_IMG_SCALE = 0.1;
40+
41+
function generatePath(modules: Modules, margin = 0): string {
42+
const ops: Array<string> = [];
43+
modules.forEach(function (row, y) {
44+
let start: number | null = null;
45+
row.forEach(function (cell, x) {
46+
if (!cell && start !== null) {
47+
// M0 0h7v1H0z injects the space with the move and drops the comma,
48+
// saving a char per operation
49+
ops.push(`M${start + margin} ${y + margin}h${x - start}v1H${start + margin}z`);
50+
start = null;
51+
return;
52+
}
53+
54+
// end of row, clean up or skip
55+
if (x === row.length - 1) {
56+
if (!cell) {
57+
// We would have closed the op above already so this can only mean
58+
// 2+ light modules in a row.
59+
return;
60+
}
61+
if (start === null) {
62+
// Just a single dark module.
63+
ops.push(`M${x + margin},${y + margin} h1v1H${x + margin}z`);
64+
} else {
65+
// Otherwise finish the current line.
66+
ops.push(`M${start + margin},${y + margin} h${x + 1 - start}v1H${start + margin}z`);
67+
}
68+
return;
69+
}
70+
71+
if (cell && start === null) {
72+
start = x;
73+
}
74+
});
75+
});
76+
return ops.join('');
77+
}
78+
79+
// We could just do this in generatePath, except that we want to support
80+
// non-Path2D canvas, so we need to keep it an explicit step.
81+
function excavateModules(modules: Modules, excavation: Excavation): Modules {
82+
return modules.slice().map((row, y) => {
83+
if (y < excavation.y || y >= excavation.y + excavation.h) {
84+
return row;
85+
}
86+
return row.map((cell, x) => {
87+
if (x < excavation.x || x >= excavation.x + excavation.w) {
88+
return cell;
89+
}
90+
return false;
91+
});
92+
});
93+
}
94+
95+
function getImageSettings(
96+
cells: Modules,
97+
size: number,
98+
margin: number,
99+
imageSettings?: ImageSettings,
100+
): null | {
101+
x: number;
102+
y: number;
103+
h: number;
104+
w: number;
105+
excavation: Excavation | null;
106+
} {
107+
if (imageSettings == null) {
108+
return null;
109+
}
110+
const numCells = cells.length + margin * 2;
111+
const defaultSize = Math.floor(size * DEFAULT_IMG_SCALE);
112+
const scale = numCells / size;
113+
const w = (imageSettings.width || defaultSize) * scale;
114+
const h = (imageSettings.height || defaultSize) * scale;
115+
const x = imageSettings.x == null ? cells.length / 2 - w / 2 : imageSettings.x * scale;
116+
const y = imageSettings.y == null ? cells.length / 2 - h / 2 : imageSettings.y * scale;
117+
118+
let excavation = null;
119+
if (imageSettings.excavate) {
120+
const floorX = Math.floor(x);
121+
const floorY = Math.floor(y);
122+
const ceilW = Math.ceil(w + x - floorX);
123+
const ceilH = Math.ceil(h + y - floorY);
124+
excavation = { x: floorX, y: floorY, w: ceilW, h: ceilH };
125+
}
126+
127+
return { x, y, h, w, excavation };
128+
}
129+
130+
function getMarginSize(includeMargin: boolean, marginSize?: number): number {
131+
if (marginSize != null) {
132+
return Math.floor(marginSize);
133+
}
134+
return includeMargin ? SPEC_MARGIN_SIZE : DEFAULT_MARGIN_SIZE;
135+
}
136+
137+
// For canvas we're going to switch our drawing mode based on whether or not
138+
// the environment supports Path2D. We only need the constructor to be
139+
// supported, but Edge doesn't actually support the path (string) type
140+
// argument. Luckily it also doesn't support the addPath() method. We can
141+
// treat that as the same thing.
142+
const SUPPORTS_PATH2D = (function () {
143+
try {
144+
new Path2D().addPath(new Path2D());
145+
} catch (e) {
146+
return false;
147+
}
148+
return true;
149+
})();
150+
151+
export const QRCodeCanvas = defineComponent({
152+
name: 'QRCodeCanvas',
153+
inheritAttrs: false,
154+
props: { ...qrProps(), level: String, bgColor: String, fgColor: String, marginSize: Number },
155+
setup(props, { attrs, expose }) {
156+
const imgSrc = computed(() => props.imageSettings?.src);
157+
const _canvas = ref<HTMLCanvasElement>(null);
158+
const _image = ref<HTMLImageElement>(null);
159+
const isImgLoaded = ref(false);
160+
expose({
161+
toDataURL: (type?: string, quality?: any) => {
162+
return _canvas.value?.toDataURL(type, quality);
163+
},
164+
});
165+
watchEffect(
166+
() => {
167+
const {
168+
value,
169+
size = DEFAULT_SIZE,
170+
level = DEFAULT_LEVEL,
171+
bgColor = DEFAULT_BGCOLOR,
172+
fgColor = DEFAULT_FGCOLOR,
173+
includeMargin = DEFAULT_INCLUDEMARGIN,
174+
marginSize,
175+
imageSettings,
176+
} = props;
177+
if (_canvas.value != null) {
178+
const canvas = _canvas.value;
179+
180+
const ctx = canvas.getContext('2d');
181+
if (!ctx) {
182+
return;
183+
}
184+
185+
let cells = qrcodegen.QrCode.encodeText(value, ERROR_LEVEL_MAP[level]).getModules();
186+
const margin = getMarginSize(includeMargin, marginSize);
187+
const numCells = cells.length + margin * 2;
188+
const calculatedImageSettings = getImageSettings(cells, size, margin, imageSettings);
189+
190+
const image = _image.value;
191+
const haveImageToRender =
192+
isImgLoaded.value &&
193+
calculatedImageSettings != null &&
194+
image !== null &&
195+
image.complete &&
196+
image.naturalHeight !== 0 &&
197+
image.naturalWidth !== 0;
198+
199+
if (haveImageToRender) {
200+
if (calculatedImageSettings.excavation != null) {
201+
cells = excavateModules(cells, calculatedImageSettings.excavation);
202+
}
203+
}
204+
205+
// We're going to scale this so that the number of drawable units
206+
// matches the number of cells. This avoids rounding issues, but does
207+
// result in some potentially unwanted single pixel issues between
208+
// blocks, only in environments that don't support Path2D.
209+
const pixelRatio = window.devicePixelRatio || 1;
210+
canvas.height = canvas.width = size * pixelRatio;
211+
const scale = (size / numCells) * pixelRatio;
212+
ctx.scale(scale, scale);
213+
214+
// Draw solid background, only paint dark modules.
215+
ctx.fillStyle = bgColor;
216+
ctx.fillRect(0, 0, numCells, numCells);
217+
218+
ctx.fillStyle = fgColor;
219+
if (SUPPORTS_PATH2D) {
220+
// $FlowFixMe: Path2D c'tor doesn't support args yet.
221+
ctx.fill(new Path2D(generatePath(cells, margin)));
222+
} else {
223+
cells.forEach(function (row, rdx) {
224+
row.forEach(function (cell, cdx) {
225+
if (cell) {
226+
ctx.fillRect(cdx + margin, rdx + margin, 1, 1);
227+
}
228+
});
229+
});
230+
}
231+
232+
if (haveImageToRender) {
233+
ctx.drawImage(
234+
image,
235+
calculatedImageSettings.x + margin,
236+
calculatedImageSettings.y + margin,
237+
calculatedImageSettings.w,
238+
calculatedImageSettings.h,
239+
);
240+
}
241+
}
242+
},
243+
{ flush: 'post' },
244+
);
245+
watch(imgSrc, () => {
246+
isImgLoaded.value = false;
247+
});
248+
249+
return () => {
250+
const size = props.size ?? DEFAULT_SIZE;
251+
const canvasStyle = { height: size, width: size };
252+
253+
let img = null;
254+
if (imgSrc.value != null) {
255+
img = (
256+
<img
257+
src={imgSrc.value}
258+
key={imgSrc.value}
259+
style={{ display: 'none' }}
260+
onLoad={() => {
261+
isImgLoaded.value = true;
262+
}}
263+
ref={_image}
264+
/>
265+
);
266+
}
267+
return (
268+
<>
269+
<canvas {...attrs} style={[canvasStyle, attrs.style as CSSProperties]} ref={_canvas} />;
270+
{img}
271+
</>
272+
);
273+
};
274+
},
275+
});

components/qrcode/demo/download.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default defineComponent({
2828
setup() {
2929
const qrcodeCanvasRef = ref();
3030
const dowloadChange = async () => {
31-
const url = await qrcodeCanvasRef.value.toDataUrl();
31+
const url = await qrcodeCanvasRef.value.toDataURL();
3232
const a = document.createElement('a');
3333
a.download = 'QRCode.png';
3434
a.href = url;

components/qrcode/demo/errorLevel.vue

+6-3
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,21 @@ set Error Level.
1515
</docs>
1616

1717
<template>
18-
<a-qrcode v-model:error-level="level" value="http://www.antv.com" />
18+
<a-qrcode
19+
:error-level="level"
20+
value="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
21+
/>
1922
<br />
2023
<br />
2124
<a-segmented v-model:value="level" :options="segmentedData" />
2225
</template>
2326

2427
<script lang="ts">
25-
import { defineComponent, ref, reactive } from 'vue';
28+
import { defineComponent, ref } from 'vue';
2629
2730
export default defineComponent({
2831
setup() {
29-
const segmentedData = reactive(['L', 'M', 'Q', 'H']);
32+
const segmentedData = ['L', 'M', 'Q', 'H'];
3033
const level = ref(segmentedData[0]);
3134
return {
3235
segmentedData,

0 commit comments

Comments
 (0)