@vkontakte/vk-qr 2.1.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +22 -3
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/figures.d.ts +9 -0
- package/dist/figures.d.ts.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1311 -0
- package/dist/index.js.map +1 -0
- package/dist/qr.d.ts +71 -0
- package/dist/qr.d.ts.map +1 -0
- package/dist/svg-path.d.ts +8 -0
- package/dist/svg-path.d.ts.map +1 -0
- package/dist/svg.d.ts +6 -0
- package/dist/svg.d.ts.map +1 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +55 -80
- package/src/constants.ts +9 -0
- package/src/figures.ts +106 -0
- package/src/index.ts +45 -27
- package/src/qr.ts +711 -837
- package/src/svg-path.ts +254 -0
- package/src/svg.ts +132 -257
- package/src/types.ts +30 -12
- package/dist/cjs/index.js +0 -1185
- package/dist/es/index.js +0 -1183
- package/dist/types/index.d.ts +0 -20
- package/dist/types/qr.d.ts +0 -87
- package/dist/types/svg.d.ts +0 -9
- package/dist/types/types.d.ts +0 -51
- package/dist/umd/qrCodeGenerator.js +0 -1191
- package/dist/umd/qrCodeGenerator.min.js +0 -1
package/src/svg-path.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { MODULE_CORNER_RADIUS, TILE_SIZE } from './constants.ts';
|
|
2
|
+
import type { Figure } from './figures.ts';
|
|
3
|
+
|
|
4
|
+
interface SvgPathOptions {
|
|
5
|
+
pixelSize?: number;
|
|
6
|
+
radius?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Point {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Edge {
|
|
15
|
+
from: Point;
|
|
16
|
+
to: Point;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type CornerRadius = (prev: Point, current: Point, next: Point) => number;
|
|
20
|
+
|
|
21
|
+
export function figureToSvgPath(figure: Figure, options: SvgPathOptions = {}): string {
|
|
22
|
+
const pixelSize = options.pixelSize ?? TILE_SIZE;
|
|
23
|
+
const radius = options.radius ?? MODULE_CORNER_RADIUS;
|
|
24
|
+
const contours = buildContours(getBoundaryEdges(figure, pixelSize));
|
|
25
|
+
const cornerRadius = createCornerRadiusResolver(figure, pixelSize, radius);
|
|
26
|
+
|
|
27
|
+
return contours.map((contour) => contourToRoundedPath(contour, cornerRadius)).join(' ');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasPixel(pixels: boolean[][], x: number, y: number): boolean {
|
|
31
|
+
return pixels[y]?.[x] ?? false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getBoundaryEdges(figure: Figure, pixelSize: number): Edge[] {
|
|
35
|
+
const pixels = figure.pixels;
|
|
36
|
+
const height = pixels.length;
|
|
37
|
+
const width = pixels[0]?.length ?? 0;
|
|
38
|
+
|
|
39
|
+
const edges: Edge[] = [];
|
|
40
|
+
|
|
41
|
+
function px(x: number): number {
|
|
42
|
+
return (figure.x + x) * pixelSize;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function py(y: number): number {
|
|
46
|
+
return (figure.y + y) * pixelSize;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Trace exposed edges with the filled module on their right.
|
|
50
|
+
for (let y = 0; y < height; y++) {
|
|
51
|
+
for (let x = 0; x < width; x++) {
|
|
52
|
+
if (!pixels[y][x]) continue;
|
|
53
|
+
|
|
54
|
+
const left = px(x);
|
|
55
|
+
const right = px(x + 1);
|
|
56
|
+
const top = py(y);
|
|
57
|
+
const bottom = py(y + 1);
|
|
58
|
+
|
|
59
|
+
if (!hasPixel(pixels, x, y - 1)) {
|
|
60
|
+
edges.push({
|
|
61
|
+
from: { x: left, y: top },
|
|
62
|
+
to: { x: right, y: top },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!hasPixel(pixels, x + 1, y)) {
|
|
67
|
+
edges.push({
|
|
68
|
+
from: { x: right, y: top },
|
|
69
|
+
to: { x: right, y: bottom },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!hasPixel(pixels, x, y + 1)) {
|
|
74
|
+
edges.push({
|
|
75
|
+
from: { x: right, y: bottom },
|
|
76
|
+
to: { x: left, y: bottom },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!hasPixel(pixels, x - 1, y)) {
|
|
81
|
+
edges.push({
|
|
82
|
+
from: { x: left, y: bottom },
|
|
83
|
+
to: { x: left, y: top },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return edges;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hasFilledCorner(
|
|
93
|
+
pixels: boolean[][],
|
|
94
|
+
x: number,
|
|
95
|
+
y: number,
|
|
96
|
+
dx: number,
|
|
97
|
+
dy: number,
|
|
98
|
+
): boolean {
|
|
99
|
+
return (
|
|
100
|
+
hasPixel(pixels, x + dx, y) && hasPixel(pixels, x, y + dy) && hasPixel(pixels, x + dx, y + dy)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createCornerRadiusResolver(
|
|
105
|
+
figure: Figure,
|
|
106
|
+
pixelSize: number,
|
|
107
|
+
radius: number,
|
|
108
|
+
): CornerRadius {
|
|
109
|
+
return (prev: Point, current: Point, next: Point): number => {
|
|
110
|
+
if (getTurn(prev, current, next) < 0) {
|
|
111
|
+
const x = Math.round(Math.min(prev.x, current.x, next.x) / pixelSize) - figure.x;
|
|
112
|
+
const y = Math.round(Math.min(prev.y, current.y, next.y) / pixelSize) - figure.y;
|
|
113
|
+
|
|
114
|
+
// Each old selector assignment reset the previous match when it failed.
|
|
115
|
+
// Consequently only n_rb could survive the final assignment.
|
|
116
|
+
const topLeft = hasFilledCorner(figure.pixels, x, y, -1, -1);
|
|
117
|
+
const bottomLeft = hasFilledCorner(figure.pixels, x, y, -1, 1);
|
|
118
|
+
const topRight = hasFilledCorner(figure.pixels, x, y, 1, -1);
|
|
119
|
+
const bottomRight = hasFilledCorner(figure.pixels, x, y, 1, 1);
|
|
120
|
+
const roundsBottomRight = bottomRight && (!topRight || (bottomLeft && !topLeft));
|
|
121
|
+
|
|
122
|
+
return roundsBottomRight &&
|
|
123
|
+
current.x === (figure.x + (x + 1)) * pixelSize &&
|
|
124
|
+
current.y === (figure.y + (y + 1)) * pixelSize
|
|
125
|
+
? radius
|
|
126
|
+
: 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return radius;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildContours(edges: Edge[]): Point[][] {
|
|
134
|
+
const edgeMap = new Map<string, Point[]>();
|
|
135
|
+
|
|
136
|
+
for (const edge of edges) {
|
|
137
|
+
const key = pointKey(edge.from);
|
|
138
|
+
const list = edgeMap.get(key);
|
|
139
|
+
if (list) {
|
|
140
|
+
list.push(edge.to);
|
|
141
|
+
} else {
|
|
142
|
+
edgeMap.set(key, [edge.to]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const contours: Point[][] = [];
|
|
147
|
+
|
|
148
|
+
while (true) {
|
|
149
|
+
const startKey = edgeMap.keys().next().value;
|
|
150
|
+
if (startKey === undefined) break;
|
|
151
|
+
|
|
152
|
+
const [x, y] = startKey.split(',').map(Number);
|
|
153
|
+
const start = { x, y };
|
|
154
|
+
|
|
155
|
+
const contour: Point[] = [start];
|
|
156
|
+
let current = start;
|
|
157
|
+
|
|
158
|
+
while (true) {
|
|
159
|
+
const candidates = edgeMap.get(pointKey(current));
|
|
160
|
+
|
|
161
|
+
if (!candidates || candidates.length === 0) {
|
|
162
|
+
throw new Error('Broken contour: edge chain is not closed');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// At a diagonal touch, keep the filled pixel on the right of the edge.
|
|
166
|
+
// Otherwise two convex corners become concave and a false hole is traced.
|
|
167
|
+
let nextIndex = 0;
|
|
168
|
+
if (candidates.length > 1 && contour.length > 1) {
|
|
169
|
+
const prev = contour[contour.length - 2];
|
|
170
|
+
const rightTurn = candidates.findIndex(
|
|
171
|
+
(candidate) => getTurn(prev, current, candidate) > 0,
|
|
172
|
+
);
|
|
173
|
+
if (rightTurn !== -1) nextIndex = rightTurn;
|
|
174
|
+
}
|
|
175
|
+
const [next] = candidates.splice(nextIndex, 1);
|
|
176
|
+
if (candidates.length === 0) edgeMap.delete(pointKey(current));
|
|
177
|
+
|
|
178
|
+
if (samePoint(next, start)) {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
contour.push(next);
|
|
183
|
+
current = next;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
contours.push(contour);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return contours;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function contourToRoundedPath(contour: Point[], cornerRadius: CornerRadius): string {
|
|
193
|
+
const n = contour.length;
|
|
194
|
+
|
|
195
|
+
if (n === 0) return '';
|
|
196
|
+
|
|
197
|
+
const commands: string[] = [];
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < n; i++) {
|
|
200
|
+
const prev = contour[(i - 1 + n) % n];
|
|
201
|
+
const current = contour[i];
|
|
202
|
+
const next = contour[(i + 1) % n];
|
|
203
|
+
|
|
204
|
+
const incomingLength = distance(current, prev);
|
|
205
|
+
const outgoingLength = distance(current, next);
|
|
206
|
+
|
|
207
|
+
const r = Math.min(cornerRadius(prev, current, next), incomingLength / 2, outgoingLength / 2);
|
|
208
|
+
|
|
209
|
+
const beforeCorner = moveTowards(current, prev, r);
|
|
210
|
+
const afterCorner = moveTowards(current, next, r);
|
|
211
|
+
|
|
212
|
+
commands.push(`${i === 0 ? 'M' : 'L'} ${beforeCorner.x} ${beforeCorner.y}`);
|
|
213
|
+
|
|
214
|
+
if (r > 0) {
|
|
215
|
+
commands.push(`Q ${current.x} ${current.y} ${afterCorner.x} ${afterCorner.y}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
commands.push('Z');
|
|
220
|
+
|
|
221
|
+
return commands.join(' ');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function moveTowards(from: Point, to: Point, distanceValue: number): Point {
|
|
225
|
+
const length = distance(from, to);
|
|
226
|
+
|
|
227
|
+
if (length === 0) {
|
|
228
|
+
return { ...from };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const ratio = distanceValue / length;
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
x: from.x + (to.x - from.x) * ratio,
|
|
235
|
+
y: from.y + (to.y - from.y) * ratio,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function distance(a: Point, b: Point): number {
|
|
240
|
+
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function pointKey(point: Point): string {
|
|
244
|
+
return `${point.x},${point.y}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function samePoint(a: Point, b: Point): boolean {
|
|
248
|
+
return a.x === b.x && a.y === b.y;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Positive for a right turn in SVG's downward-pointing Y axis. */
|
|
252
|
+
function getTurn(prev: Point, current: Point, next: Point): number {
|
|
253
|
+
return (current.x - prev.x) * (next.y - current.y) - (current.y - prev.y) * (next.x - current.x);
|
|
254
|
+
}
|
package/src/svg.ts
CHANGED
|
@@ -1,70 +1,25 @@
|
|
|
1
|
-
import { QrCode } from './qr';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (x < QR_BORDER && y >= size - QR_BORDER) {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (isLogoShowed) {
|
|
29
|
-
let imageTiles = QR_BORDER + 2;
|
|
30
|
-
if (size <= SMALL_QR_SIZE) {
|
|
31
|
-
imageTiles--;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
let paddingTiles = (size - QR_BORDER * 2 - imageTiles) / 2 - 1;
|
|
35
|
-
if (size <= SMALL_QR_SIZE) {
|
|
36
|
-
paddingTiles++;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
x > QR_BORDER + paddingTiles &&
|
|
41
|
-
x < size - QR_BORDER - paddingTiles - 1 &&
|
|
42
|
-
y > QR_BORDER + paddingTiles &&
|
|
43
|
-
y < size - QR_BORDER - paddingTiles - 1
|
|
44
|
-
) {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return 0 <= x && x < size && 0 <= y && y < size && modules[y][x];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return modules[y] && modules[y][x];
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const getNeighbors = (x: number, y: number, size: number, modules: boolean[][], isLogoShowed = true) => ({
|
|
55
|
-
l: getPixel(x - 1, y, size, modules, isLogoShowed),
|
|
56
|
-
r: getPixel(x + 1, y, size, modules, isLogoShowed),
|
|
57
|
-
t: getPixel(x, y - 1, size, modules, isLogoShowed),
|
|
58
|
-
b: getPixel(x, y + 1, size, modules, isLogoShowed),
|
|
59
|
-
current: getPixel(x, y, size, modules, isLogoShowed)
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Converts QrCode instance to SVG code
|
|
64
|
-
* @param qrCode QrCode instance
|
|
65
|
-
* @param options Convertation options
|
|
66
|
-
*/
|
|
67
|
-
export const convertSegmentsToSvgString = (qrCode: QrCode, options: Required<QrOptions>): string => {
|
|
1
|
+
import type { QrCode } from './qr.ts';
|
|
2
|
+
import type { RequiredQrOptions } from './types.ts';
|
|
3
|
+
import { extractFigures, isFinderPattern } from './figures.ts';
|
|
4
|
+
import { figureToSvgPath } from './svg-path.ts';
|
|
5
|
+
import {
|
|
6
|
+
FINDER_PATTERN_SIZE,
|
|
7
|
+
TILE_SIZE,
|
|
8
|
+
SMALL_QR_SIZE,
|
|
9
|
+
FINDER_CORNER_RADIUS,
|
|
10
|
+
} from './constants.ts';
|
|
11
|
+
|
|
12
|
+
export const INC_TILE_SIZE = TILE_SIZE;
|
|
13
|
+
|
|
14
|
+
interface BackgroundLayout {
|
|
15
|
+
size: number;
|
|
16
|
+
padding: number;
|
|
17
|
+
radius: number;
|
|
18
|
+
hideQr: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Converts a QR module matrix to SVG. */
|
|
22
|
+
export function convertSegmentsToSvgString(qrCode: QrCode, options: RequiredQrOptions): string {
|
|
68
23
|
if (typeof options.qrSize !== 'number') {
|
|
69
24
|
throw new Error('Size should be a number');
|
|
70
25
|
}
|
|
@@ -73,219 +28,146 @@ export const convertSegmentsToSvgString = (qrCode: QrCode, options: Required<QrO
|
|
|
73
28
|
throw new Error('Classname should be a string');
|
|
74
29
|
}
|
|
75
30
|
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
const _14_7 = 14.7 * MULTI;
|
|
80
|
-
const _14_8 = 14.8 * MULTI;
|
|
81
|
-
const _15_9 = 15.9 * MULTI;
|
|
82
|
-
const _28_6 = 28.6 * MULTI;
|
|
83
|
-
const _30_5 = 30.5 * MULTI;
|
|
84
|
-
const _84_7 = 84.7776815 * MULTI;
|
|
85
|
-
const _87_3 = 87.3 * MULTI;
|
|
86
|
-
const _71_4 = 71.4 * MULTI;
|
|
87
|
-
const _42_9 = 42.9 * MULTI;
|
|
88
|
-
const _87_2 = 87.2 * MULTI;
|
|
89
|
-
const _85_2 = 85.2 * MULTI;
|
|
90
|
-
const _85_3 = 85.3 * MULTI;
|
|
91
|
-
const _69_5 = 69.5 * MULTI;
|
|
92
|
-
const _98 = 98 * MULTI;
|
|
93
|
-
const _100 = 100 * MULTI;
|
|
94
|
-
|
|
95
|
-
const parts = [];
|
|
96
|
-
let leftPadding = 0;
|
|
97
|
-
let topPadding = 0;
|
|
98
|
-
let xCoord = 0;
|
|
99
|
-
let yCoord = 0;
|
|
100
|
-
|
|
101
|
-
for (let y = 0; y < qrCode.size; y++) {
|
|
102
|
-
leftPadding = 0;
|
|
103
|
-
for (let x = 0; x < qrCode.size; x++) {
|
|
104
|
-
xCoord = x + leftPadding;
|
|
105
|
-
leftPadding += TILE_SIZE;
|
|
106
|
-
yCoord = y + topPadding;
|
|
31
|
+
const background = getBackgroundLayout(qrCode.size, options);
|
|
32
|
+
const parts = renderFinderPatterns(qrCode.size, options);
|
|
33
|
+
if (options.isShowLogo) parts.push(renderLogo(qrCode.size, options));
|
|
107
34
|
|
|
108
|
-
|
|
35
|
+
return `
|
|
36
|
+
<svg
|
|
37
|
+
version="1.1"
|
|
38
|
+
viewBox="${-background.padding} ${-background.padding} ${background.size} ${background.size}"
|
|
39
|
+
width="${options.qrSize}px"
|
|
40
|
+
height="${options.qrSize}px"
|
|
41
|
+
style="display: block"
|
|
42
|
+
${options.className ? `class="${options.className}"` : ''}
|
|
43
|
+
xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
44
|
+
>
|
|
45
|
+
${renderDefinitions(options)}
|
|
109
46
|
|
|
110
|
-
|
|
111
|
-
let selector = '';
|
|
47
|
+
${renderBackground(background, options)}
|
|
112
48
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
selector += neighbors.l ? 'l' : neighbors.r ? 'r' : '';
|
|
118
|
-
selector += neighbors.t ? 't' : neighbors.b ? 'b' : '';
|
|
119
|
-
if (!selector) {
|
|
120
|
-
selector = 'empty';
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
} else {
|
|
124
|
-
selector =
|
|
125
|
-
!selector &&
|
|
126
|
-
neighbors.l &&
|
|
127
|
-
neighbors.t &&
|
|
128
|
-
getPixel(x - 1, y - 1, qrCode.size, qrCode.modules, options.isShowLogo)
|
|
129
|
-
? 'n_lt'
|
|
130
|
-
: '';
|
|
131
|
-
selector =
|
|
132
|
-
!selector &&
|
|
133
|
-
neighbors.l &&
|
|
134
|
-
neighbors.b &&
|
|
135
|
-
getPixel(x - 1, y + 1, qrCode.size, qrCode.modules, options.isShowLogo)
|
|
136
|
-
? 'n_lb'
|
|
137
|
-
: '';
|
|
138
|
-
selector =
|
|
139
|
-
!selector &&
|
|
140
|
-
neighbors.r &&
|
|
141
|
-
neighbors.t &&
|
|
142
|
-
getPixel(x + 1, y - 1, qrCode.size, qrCode.modules, options.isShowLogo)
|
|
143
|
-
? 'n_rt'
|
|
144
|
-
: '';
|
|
145
|
-
selector =
|
|
146
|
-
!selector &&
|
|
147
|
-
neighbors.r &&
|
|
148
|
-
neighbors.b &&
|
|
149
|
-
getPixel(x + 1, y + 1, qrCode.size, qrCode.modules, options.isShowLogo)
|
|
150
|
-
? 'n_rb'
|
|
151
|
-
: '';
|
|
152
|
-
}
|
|
49
|
+
<g${background.hideQr ? ' display="none"' : ''}>
|
|
50
|
+
${renderModules(qrCode, options)}
|
|
51
|
+
${parts.join('\n')}
|
|
52
|
+
</g>
|
|
153
53
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
54
|
+
</svg>`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getVisibleModules(qrCode: QrCode, isShowLogo: boolean): boolean[][] {
|
|
58
|
+
if (!isShowLogo) return qrCode.modules;
|
|
59
|
+
|
|
60
|
+
// Clear the logo area before tracing contours so transparent logos retain
|
|
61
|
+
// their quiet zone and neighboring modules do not connect through it.
|
|
62
|
+
const logoAreaSize = qrCode.size <= SMALL_QR_SIZE ? FINDER_PATTERN_SIZE : FINDER_PATTERN_SIZE + 2;
|
|
63
|
+
const logoAreaStart = (qrCode.size - logoAreaSize) / 2;
|
|
64
|
+
return qrCode.modules.map((row, y) =>
|
|
65
|
+
row.map(
|
|
66
|
+
(pixel, x) =>
|
|
67
|
+
pixel &&
|
|
68
|
+
!(
|
|
69
|
+
x >= logoAreaStart &&
|
|
70
|
+
x < logoAreaStart + logoAreaSize &&
|
|
71
|
+
y >= logoAreaStart &&
|
|
72
|
+
y < logoAreaStart + logoAreaSize
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
162
77
|
|
|
78
|
+
function renderModules(qrCode: QrCode, options: RequiredQrOptions): string {
|
|
79
|
+
return extractFigures(getVisibleModules(qrCode, options.isShowLogo))
|
|
80
|
+
.filter((figure) => !isFinderPattern(figure, qrCode.size))
|
|
81
|
+
.map(
|
|
82
|
+
(figure) => `
|
|
83
|
+
<path
|
|
84
|
+
d="${figureToSvgPath(figure)}"
|
|
85
|
+
fill="${options.foregroundColor}"
|
|
86
|
+
fill-rule="evenodd"
|
|
87
|
+
/>`,
|
|
88
|
+
)
|
|
89
|
+
.join('');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function renderFinderPatterns(size: number, options: RequiredQrOptions): string[] {
|
|
93
|
+
const pointPosition = (size - FINDER_PATTERN_SIZE) * TILE_SIZE;
|
|
94
|
+
return [
|
|
95
|
+
`<use fill-rule="evenodd" transform="translate(0,0)" xlink:href="#point-${options.suffix}"/>`,
|
|
96
|
+
`<use fill-rule="evenodd" transform="translate(${pointPosition},0)" xlink:href="#point-${options.suffix}"/>`,
|
|
97
|
+
`<use fill-rule="evenodd" transform="translate(0,${pointPosition})" xlink:href="#point-${options.suffix}"/>`,
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function renderLogo(size: number, options: RequiredQrOptions): string {
|
|
163
102
|
let scale = '';
|
|
164
|
-
let position =
|
|
165
|
-
|
|
103
|
+
let position =
|
|
104
|
+
((size - FINDER_PATTERN_SIZE * 3) / 2) * TILE_SIZE + TILE_SIZE * FINDER_PATTERN_SIZE - 10;
|
|
105
|
+
if (size <= SMALL_QR_SIZE) {
|
|
166
106
|
scale = 'scale(0.85)';
|
|
167
107
|
position += 50;
|
|
168
108
|
}
|
|
169
109
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
parts.push(
|
|
173
|
-
`<use fill-rule="evenodd" transform="translate(${pointPosition},0)" xlink:href="#point-${options.suffix}"/>`
|
|
174
|
-
);
|
|
175
|
-
parts.push(
|
|
176
|
-
`<use fill-rule="evenodd" transform="translate(0,${pointPosition})" xlink:href="#point-${options.suffix}"/>`
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
if (options.isShowLogo) {
|
|
180
|
-
if (!options.logoData) {
|
|
181
|
-
parts.push(`
|
|
110
|
+
if (!options.logoData) {
|
|
111
|
+
return `
|
|
182
112
|
<use style="width: 750px; height: 750px;" width="750" height="750"
|
|
183
113
|
fill="none"
|
|
184
114
|
fill-rule="evenodd"
|
|
185
115
|
transform="translate(${position},${position}) ${scale}" xlink:href="#vk_logo-${options.suffix}"
|
|
186
116
|
/>
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return `
|
|
190
121
|
<image
|
|
191
122
|
preserveAspectRatio="xMidYMid slice" clip-path="url(#logo-mask-${options.suffix})"
|
|
192
123
|
style="width: 750px; height: 750px;" width="750" height="750"
|
|
193
124
|
transform="translate(${position},${position}) ${scale}"
|
|
194
125
|
xlink:href="${options.logoData}"
|
|
195
126
|
/>
|
|
196
|
-
|
|
197
|
-
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getBackgroundLayout(size: number, options: RequiredQrOptions): BackgroundLayout {
|
|
131
|
+
const contentSize = TILE_SIZE * size;
|
|
132
|
+
if (!options.isShowBackground) {
|
|
133
|
+
return { size: contentSize, padding: 0, radius: 0, hideQr: false };
|
|
198
134
|
}
|
|
199
135
|
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
136
|
+
const frameWidth = Math.min(options.backgroundPadding, options.qrSize / 2);
|
|
137
|
+
const contentWidth = options.qrSize - frameWidth * 2;
|
|
138
|
+
const hideQr = contentWidth <= 0;
|
|
139
|
+
const backgroundSize = hideQr ? contentSize : (contentSize * options.qrSize) / contentWidth;
|
|
140
|
+
const padding = (backgroundSize - contentSize) / 2;
|
|
141
|
+
const radius =
|
|
142
|
+
(Math.min(options.backgroundRadius, options.qrSize / 2) / options.qrSize) * backgroundSize;
|
|
203
143
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const padding = (backgroundSize / options.qrSize) * 21;
|
|
207
|
-
const radius = Math.ceil(backgroundSize / (options.qrSize / 36)); // 7.11111111 = 256px/36px radius
|
|
144
|
+
return { size: backgroundSize, padding, radius, hideQr };
|
|
145
|
+
}
|
|
208
146
|
|
|
209
|
-
|
|
147
|
+
function renderBackground(layout: BackgroundLayout, options: RequiredQrOptions): string {
|
|
148
|
+
if (!options.isShowBackground) return '';
|
|
149
|
+
|
|
150
|
+
const { size, padding, radius } = layout;
|
|
151
|
+
return `
|
|
210
152
|
<rect
|
|
211
|
-
x="
|
|
212
|
-
|
|
213
|
-
|
|
153
|
+
x="${-padding}"
|
|
154
|
+
y="${-padding}"
|
|
155
|
+
width="${size}"
|
|
156
|
+
height="${size}"
|
|
214
157
|
rx="${radius}"
|
|
215
158
|
fill="${options.backgroundColor}"
|
|
216
159
|
/>`;
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// eslint-disable-next-line
|
|
221
|
-
const oneSide = `M0,0 L66,0 C${_84_7},-3.44940413e-15 ${_100},15.2223185 ${_100},34 L${_100},66 C${_100},${_84_7} \
|
|
222
|
-
${_84_7},${_100} 66,${_100} L0,${_100} L0,0 Z`;
|
|
223
|
-
const twoSides = `M0,0 L${_100},0 L${_100},66 C${_100},${_84_7} ${_84_7},${_100} 66,${_100} L0,${_100} L0,0 Z`;
|
|
160
|
+
}
|
|
224
161
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
viewBox="0 0 ${backgroundSize} ${backgroundSize}"
|
|
229
|
-
width="${options.qrSize}px"
|
|
230
|
-
height="${options.qrSize}px"
|
|
231
|
-
${options.className ? `class="${options.className}"` : ''}
|
|
232
|
-
xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
233
|
-
>
|
|
234
|
-
<defs>
|
|
235
|
-
<rect id="rect-${options.suffix}" width="100" height="100" fill="${options.foregroundColor}"/>
|
|
236
|
-
<path
|
|
237
|
-
id="empty-${options.suffix}"
|
|
238
|
-
d="M0,${_28_6}v${_42_9}C0,${_87_3},${_12_8},${_100},${_28_6},${_100}h${_42_9}c${_15_9},0,${_28_6}-${_12_8},\
|
|
239
|
-
${_28_6}-${_28_6}V${_28_6}C${_100},${_12_7},${_87_2},0,${_71_4},0H${_28_6} C${_12_8},0,0,${_12_8},0,${_28_6}z"
|
|
240
|
-
fill="${options.foregroundColor}"
|
|
241
|
-
/>
|
|
242
|
-
<path id="b-${options.suffix}" d="${oneSide}" transform="rotate(-90 50 50)" fill="${options.foregroundColor}"/>
|
|
243
|
-
<path id="r-${options.suffix}" d="${oneSide}" transform="rotate(-180 50 50)" fill="${options.foregroundColor}"/>
|
|
244
|
-
<path id="l-${options.suffix}" d="${oneSide}" fill="${options.foregroundColor}"/>
|
|
245
|
-
<path id="t-${options.suffix}" d="${oneSide}" transform="rotate(90 50 50)" fill="${options.foregroundColor}"/>
|
|
246
|
-
<path id="l-${options.suffix}" d="${twoSides}" transform="rotate(-90 50 50)" fill="${options.foregroundColor}"/>
|
|
247
|
-
<path id="lt-${options.suffix}" d="${twoSides}" fill="${options.foregroundColor}"/>
|
|
248
|
-
<path id="lb-${options.suffix}" d="${twoSides}" transform="rotate(-90 50 50)" fill="${options.foregroundColor}"/>
|
|
249
|
-
<path id="rb-${options.suffix}" d="${twoSides}" transform="rotate(-180 50 50)" fill="${options.foregroundColor}"/>
|
|
250
|
-
<path id="rt-${options.suffix}" d="${twoSides}" transform="rotate(90 50 50)" fill="${options.foregroundColor}"/>
|
|
251
|
-
<path
|
|
252
|
-
id="n_lt-${options.suffix}"
|
|
253
|
-
d="M${_30_5},${_2}V0H0v${_30_5}h${_2}C${_2},${_14_7},${_14_8},${_2},${_30_5},${_2}z"
|
|
254
|
-
fill="${options.foregroundColor}"
|
|
255
|
-
/>
|
|
256
|
-
<path
|
|
257
|
-
id="n_lb-${options.suffix}"
|
|
258
|
-
d="M${_2},${_69_5}H0V${_100}h${_30_5}v-${_2}C${_14_7},${_98},${_2},${_85_2},${_2},${_69_5}z"
|
|
259
|
-
fill="${options.foregroundColor}"
|
|
260
|
-
/>
|
|
261
|
-
<path
|
|
262
|
-
id="n_rt-${options.suffix}"
|
|
263
|
-
d="M${_98},${_30_5}h${_2}V0H${_69_5}v${_2}C${_85_3},${_2},${_98},${_14_8},${_98},${_30_5}z"
|
|
264
|
-
fill="${options.foregroundColor}"
|
|
265
|
-
/>
|
|
266
|
-
<path id="n_rb-${options.suffix}"
|
|
267
|
-
d="M${_69_5},${_98}v${_2}H${_100}V${_69_5}h-${_2}C${_98},${_85_3},${_85_2},${_98},${_69_5},${_98}z"
|
|
268
|
-
fill="${options.foregroundColor}"
|
|
269
|
-
/>
|
|
270
|
-
<path
|
|
162
|
+
function renderDefinitions(options: RequiredQrOptions): string {
|
|
163
|
+
return `<defs>
|
|
164
|
+
<g
|
|
271
165
|
id="point-${options.suffix}"
|
|
272
166
|
fill="${options.foregroundColor}"
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
C99.9982143,552.627632 112.972983,575.877216 118.547883,581.464617 C124.122784,587.027017 \
|
|
278
|
-
147.372368,600.001786 242.670667,600.001786 L457.329333,600.001786 C552.627632,600.001786 \
|
|
279
|
-
575.877216,587.027017 581.464617,581.464617 C587.039517,575.877216 600.001786,552.627632 \
|
|
280
|
-
600.001786,457.329333 Z M457.329333,0 C653.338333,0 700,46.6616668 700,242.658167 C700,438.667167 \
|
|
281
|
-
700,261.332833 700,457.329333 C700,653.338333 653.338333,700 457.329333,700 C261.332833,700 438.667167,700 \
|
|
282
|
-
242.670667,700 C46.6616668,700 0,653.338333 0,457.329333 C0,261.332833 0,352.118712 0,242.658167 \
|
|
283
|
-
C0,46.6616668 46.6616668,0 242.670667,0 C438.667167,0 261.332833,0 457.329333,0 Z M395.996667,200 \
|
|
284
|
-
C480.004166,200 500,220.008332 500,303.990835 C500,387.998334 500,312.001666 500,395.996667 \
|
|
285
|
-
C500,479.991668 480.004166,500 395.996667,500 C312.001666,500 387.998334,500 304.003333,500 C220.008332,500 \
|
|
286
|
-
200,479.991668 200,395.996667 C200,312.001666 200,350.906061 200,303.990835 C200,220.008332 220.008332,200 \
|
|
287
|
-
304.003333,200 C387.998334,200 312.001666,200 395.996667,200 Z"
|
|
288
|
-
/>
|
|
167
|
+
>
|
|
168
|
+
<rect width="300" height="300" x="200" y="200" rx="${FINDER_CORNER_RADIUS}" />
|
|
169
|
+
<rect width="600" height="600" x="50" y="50" rx="${FINDER_CORNER_RADIUS * 2}" stroke-width="100" fill="none" stroke="${options.foregroundColor}" />
|
|
170
|
+
</g>
|
|
289
171
|
<g id="vk_logo-${options.suffix}">
|
|
290
172
|
<path
|
|
291
173
|
fill="${options.logoColor}"
|
|
@@ -306,12 +188,5 @@ C500,479.991668 480.004166,500 395.996667,500 C312.001666,500 387.998334,500 304
|
|
|
306
188
|
<clipPath id="logo-mask-${options.suffix}">
|
|
307
189
|
<rect x="0" y="0" width="750" height="750" />
|
|
308
190
|
</clipPath>
|
|
309
|
-
</defs
|
|
310
|
-
|
|
311
|
-
${qrBackground}
|
|
312
|
-
|
|
313
|
-
<g transform="${qrTransform}">
|
|
314
|
-
${parts.join('\n')}
|
|
315
|
-
</g>
|
|
316
|
-
</svg>`;
|
|
317
|
-
};
|
|
191
|
+
</defs>`;
|
|
192
|
+
}
|