@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.
@@ -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 { QrOptions } from './types';
3
-
4
- const MULTI = 1;
5
- const QR_BORDER = 7;
6
- const TILE_SIZE = 96 * MULTI;
7
- export const INC_TILE_SIZE = 96 * MULTI;
8
- const SMALL_QR_SIZE = 25;
9
-
10
- /**
11
- * Returns the color of the module (pixel) at the given coordinates, which is false
12
- * for white or true for black. The top left corner has the coordinates (x=0, y=0).
13
- * If the given coordinates are out of bounds, then false (white) is returned.
14
- */
15
- const getPixel = (x: number, y: number, size: number, modules: boolean[][], isLogoShowed: boolean) => {
16
- if (x < QR_BORDER && y < QR_BORDER) {
17
- return false;
18
- }
19
-
20
- if (x >= size - QR_BORDER && y < QR_BORDER) {
21
- return false;
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 _2 = 2 * MULTI;
77
- const _12_7 = 12.7 * MULTI;
78
- const _12_8 = 12.8 * MULTI;
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
- const neighbors = getNeighbors(x, y, qrCode.size, qrCode.modules, options.isShowLogo);
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
- let path = '';
111
- let selector = '';
47
+ ${renderBackground(background, options)}
112
48
 
113
- if (neighbors.current) {
114
- selector = !selector && !neighbors.l && !neighbors.r && !neighbors.t && !neighbors.b ? 'empty' : '';
115
- selector = (!selector && neighbors.l && neighbors.r) || (neighbors.t && neighbors.b) ? 'rect' : '';
116
- if (!selector) {
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
- if (!selector) {
155
- continue;
156
- }
157
- path = `<use xlink:href="#${selector}-${options.suffix}"/>`;
158
- parts.push(`<g transform="translate(${xCoord},${yCoord})">${path}</g>`);
159
- }
160
- topPadding += TILE_SIZE;
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 = ((qrCode.size - QR_BORDER * 3) / 2) * TILE_SIZE + TILE_SIZE * QR_BORDER - 10;
165
- if (qrCode.size <= SMALL_QR_SIZE) {
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
- const pointPosition = (qrCode.size - QR_BORDER) * options.incTileSize;
171
- parts.push(`<use fill-rule="evenodd" transform="translate(0,0)" xlink:href="#point-${options.suffix}"/>`);
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
- } else {
189
- parts.push(`
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 backgroundSize = 99 * qrCode.size;
201
- let qrBackground = '';
202
- let qrTransform = 'translate(0,0)';
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
- if (options.isShowBackground) {
205
- const qrScale = (options.qrSize - 20 * 2) / options.qrSize; // 0.756972112
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
- qrBackground = `
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="0"
212
- width="${backgroundSize}"
213
- height="${backgroundSize}"
153
+ x="${-padding}"
154
+ y="${-padding}"
155
+ width="${size}"
156
+ height="${size}"
214
157
  rx="${radius}"
215
158
  fill="${options.backgroundColor}"
216
159
  />`;
217
- qrTransform = `translate(${padding}, ${padding}) scale(${qrScale})`;
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
- return `
226
- <svg
227
- version="1.1"
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
- d="M600.001786,457.329333 L600.001786,242.658167 C600.001786,147.372368 587.039517,124.122784 \
274
- 581.464617,118.535383 C575.877216,112.960483 552.627632,99.9982143 457.329333,99.9982143 \
275
- L242.670667,99.9982143 C147.372368,99.9982143 124.122784,112.960483 118.547883,118.535383 \
276
- C112.972983,124.122784 99.9982143,147.372368 99.9982143,242.658167 L99.9982143,457.329333 \
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
+ }