Skip to content

Commit 493a1b5

Browse files
committed
fix: bundle size optimizations
1 parent b763cc9 commit 493a1b5

File tree

5 files changed

+149
-138
lines changed

5 files changed

+149
-138
lines changed

packages/tinyqrc/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "tinyqrc",
33
"description": "A tiny QR code generator, written in TypeScript.",
4-
"version": "0.0.4",
4+
"version": "0.0.5",
55
"author": "Christo Todorov <[email protected]>",
66
"license": "MIT",
77
"repository": "https://github.com/chroxify/tinyqrc",
@@ -37,6 +37,7 @@
3737
"clean": "rm -rf dist",
3838
"prepublishOnly": "bun run build"
3939
},
40+
"sideEffects": false,
4041
"type": "module",
4142
"devDependencies": {
4243
"@types/bun": "latest",

packages/tinyqrc/src/constants.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
import qrcodegen from "./codegen";
22

3-
export const ERROR_CORRECTION_LEVELS: {
4-
[index: string]: qrcodegen.QrCode.Ecc;
5-
} = {
6-
L: qrcodegen.QrCode.Ecc.LOW,
7-
M: qrcodegen.QrCode.Ecc.MEDIUM,
8-
Q: qrcodegen.QrCode.Ecc.QUARTILE,
9-
H: qrcodegen.QrCode.Ecc.HIGH,
10-
};
11-
123
export const DEFAULT_SIZE = 128;
134
export const DEFAULT_LEVEL = "L";
145
export const DEFAULT_BGCOLOR = "#FFFFFF";
156
export const DEFAULT_FGCOLOR = "#000000";
167
export const DEFAULT_MARGIN = 2;
8+
export const DEFAULT_IMG_SCALE = 0.1;
179

1810
export const QR_LEVELS = ["L", "M", "Q", "H"] as const;
1911

20-
// This is *very* rough estimate of max amount of QRCode allowed to be covered.
21-
// It is "wrong" in a lot of ways (area is a terrible way to estimate, it
22-
// really should be number of modules covered), but if for some reason we don't
23-
// get an explicit height or width, I'd rather default to something than throw.
24-
export const DEFAULT_IMG_SCALE = 0.1;
12+
export function getErrorCorrectionLevel(level: string): qrcodegen.QrCode.Ecc {
13+
switch (level) {
14+
case "L":
15+
return qrcodegen.QrCode.Ecc.LOW;
16+
case "M":
17+
return qrcodegen.QrCode.Ecc.MEDIUM;
18+
case "Q":
19+
return qrcodegen.QrCode.Ecc.QUARTILE;
20+
case "H":
21+
return qrcodegen.QrCode.Ecc.HIGH;
22+
default:
23+
return qrcodegen.QrCode.Ecc.LOW;
24+
}
25+
}

packages/tinyqrc/src/index.ts

Lines changed: 3 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,9 @@
1-
import qrcodegen from "./codegen";
2-
import {
1+
export { generateSVG } from "./svg";
2+
export type { QRCodeOptions, ImageSettings } from "./types";
3+
export {
34
DEFAULT_BGCOLOR,
45
DEFAULT_FGCOLOR,
56
DEFAULT_LEVEL,
67
DEFAULT_MARGIN,
78
DEFAULT_SIZE,
8-
ERROR_CORRECTION_LEVELS,
99
} from "./constants";
10-
import type { ImageSettings } from "./types";
11-
import { excavateModules, getImageSettings, generatePath } from "./utils";
12-
13-
export type QRCodeOptions = {
14-
/** The content to encode in the QR code */
15-
value: string;
16-
/** Width and height of the QR code in pixels. Defaults to 600 */
17-
size?: number;
18-
/** Error correction level. "L" (7%), "M" (15%), "Q" (25%), or "H" (30%). Defaults to "L" */
19-
level?: string;
20-
/** Background color of the QR code. Accepts any valid CSS color. Defaults to "#FFFFFF" */
21-
bgColor?: string;
22-
/** Foreground color of the QR code. Accepts any valid CSS color. Defaults to "#000000" */
23-
fgColor?: string;
24-
/** Quiet zone margin around the QR code in modules. Defaults to 4 */
25-
margin?: number;
26-
/** Settings for embedding an image/logo in the QR code center */
27-
imageSettings?: ImageSettings;
28-
/** Additional attributes to be added to the SVG element */
29-
[key: string]: unknown;
30-
};
31-
32-
/**
33-
* Generates an SVG QR code as a string. Use this when you need the raw SVG markup.
34-
*/
35-
export function generateSVG(options: QRCodeOptions): string {
36-
const {
37-
value,
38-
size = DEFAULT_SIZE,
39-
level = DEFAULT_LEVEL,
40-
bgColor = DEFAULT_BGCOLOR,
41-
fgColor = DEFAULT_FGCOLOR,
42-
margin = DEFAULT_MARGIN,
43-
imageSettings,
44-
...otherProps
45-
} = options;
46-
47-
const shouldUseHigherErrorLevel =
48-
imageSettings?.excavate && (level === "L" || level === "M");
49-
50-
// Use a higher error correction level 'Q' when excavation is enabled
51-
// to ensure the QR code remains scannable despite the removed modules.
52-
const effectiveLevel = shouldUseHigherErrorLevel ? "Q" : level;
53-
54-
let cells = qrcodegen.QrCode.encodeText(
55-
value,
56-
ERROR_CORRECTION_LEVELS[effectiveLevel]
57-
).getModules();
58-
59-
const numCells = cells.length + margin * 2;
60-
const calculatedImageSettings = getImageSettings(
61-
cells,
62-
size,
63-
margin,
64-
imageSettings
65-
);
66-
67-
let imageTag = "";
68-
if (imageSettings != null && calculatedImageSettings != null) {
69-
if (calculatedImageSettings.excavation != null) {
70-
cells = excavateModules(cells, calculatedImageSettings.excavation);
71-
}
72-
73-
// Create an SVG image tag
74-
imageTag = `<image
75-
href="${imageSettings.src}"
76-
height="${calculatedImageSettings.h}"
77-
width="${calculatedImageSettings.w}"
78-
x="${calculatedImageSettings.x + margin}"
79-
y="${calculatedImageSettings.y + margin}"
80-
preserveAspectRatio="none"
81-
/>`;
82-
}
83-
84-
// Drawing strategy: instead of a rect per module, we're going to create a
85-
// single path for the dark modules and layer that on top of a light rect,
86-
// for a total of 2 DOM nodes. We pay a bit more in string concat but that's
87-
// way faster than DOM ops.
88-
// For level 1, 441 nodes -> 2
89-
// For level 40, 31329 -> 2
90-
const fgPath = generatePath(cells, margin);
91-
92-
// Convert additional props to attribute string
93-
let attributesString = "";
94-
for (const [key, value] of Object.entries(otherProps)) {
95-
if (value != null) {
96-
attributesString += ` ${key}="${value}"`;
97-
}
98-
}
99-
100-
return `<svg
101-
xmlns="http://www.w3.org/2000/svg"
102-
height="${size}"
103-
width="${size}"
104-
viewBox="0 0 ${numCells} ${numCells}"
105-
role="img"
106-
aria-label="QR Code"
107-
data-generator="tinyqrc"
108-
${attributesString}
109-
>
110-
<title>QR Code</title>
111-
<desc>Scan this QR code with your mobile device</desc>
112-
<path
113-
fill="${bgColor}"
114-
d="M0,0 h${numCells}v${numCells}H0z"
115-
shapeRendering="crispEdges"
116-
/>
117-
<path fill="${fgColor}" d="${fgPath}" shapeRendering="crispEdges" />
118-
${imageTag}
119-
</svg>`;
120-
}

packages/tinyqrc/src/svg.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import qrcodegen from "./codegen";
2+
import {
3+
DEFAULT_BGCOLOR,
4+
DEFAULT_FGCOLOR,
5+
DEFAULT_LEVEL,
6+
DEFAULT_MARGIN,
7+
DEFAULT_SIZE,
8+
getErrorCorrectionLevel,
9+
} from "./constants";
10+
import type { QRCodeOptions } from "./types";
11+
import { excavateModules, getImageSettings, generatePath } from "./utils";
12+
13+
/**
14+
* Generates an SVG QR code as a string. Use this when you need the raw SVG markup.
15+
*/
16+
export function generateSVG(options: QRCodeOptions): string {
17+
const {
18+
value,
19+
size = DEFAULT_SIZE,
20+
level = DEFAULT_LEVEL,
21+
bgColor = DEFAULT_BGCOLOR,
22+
fgColor = DEFAULT_FGCOLOR,
23+
margin = DEFAULT_MARGIN,
24+
imageSettings,
25+
...otherProps
26+
} = options;
27+
28+
const shouldUseHigherErrorLevel =
29+
imageSettings?.excavate && (level === "L" || level === "M");
30+
31+
// Use a higher error correction level 'Q' when excavation is enabled
32+
// to ensure the QR code remains scannable despite the removed modules.
33+
const effectiveLevel = shouldUseHigherErrorLevel ? "Q" : level;
34+
35+
let cells = qrcodegen.QrCode.encodeText(
36+
value,
37+
getErrorCorrectionLevel(effectiveLevel)
38+
).getModules();
39+
40+
const numCells = cells.length + margin * 2;
41+
const calculatedImageSettings = getImageSettings(
42+
cells,
43+
size,
44+
margin,
45+
imageSettings
46+
);
47+
48+
let imageTag = "";
49+
if (imageSettings != null && calculatedImageSettings != null) {
50+
if (calculatedImageSettings.excavation != null) {
51+
cells = excavateModules(cells, calculatedImageSettings.excavation);
52+
}
53+
54+
// Create an SVG image tag
55+
imageTag = `<image
56+
href="${imageSettings.src}"
57+
height="${calculatedImageSettings.h}"
58+
width="${calculatedImageSettings.w}"
59+
x="${calculatedImageSettings.x + margin}"
60+
y="${calculatedImageSettings.y + margin}"
61+
preserveAspectRatio="none"
62+
/>`;
63+
}
64+
65+
// Drawing strategy: instead of a rect per module, we're going to create a
66+
// single path for the dark modules and layer that on top of a light rect,
67+
// for a total of 2 DOM nodes. We pay a bit more in string concat but that's
68+
// way faster than DOM ops.
69+
// For level 1, 441 nodes -> 2
70+
// For level 40, 31329 -> 2
71+
const fgPath = generatePath(cells, margin);
72+
73+
// Convert additional props to attribute string
74+
let attributesString = "";
75+
for (const [key, value] of Object.entries(otherProps)) {
76+
if (value != null) {
77+
attributesString += ` ${key}="${value}"`;
78+
}
79+
}
80+
81+
return `<svg
82+
xmlns="http://www.w3.org/2000/svg"
83+
height="${size}"
84+
width="${size}"
85+
viewBox="0 0 ${numCells} ${numCells}"
86+
role="img"
87+
aria-label="QR Code"
88+
data-generator="tinyqrc"
89+
${attributesString}
90+
>
91+
<title>QR Code</title>
92+
<desc>Scan this QR code with your mobile device</desc>
93+
<path
94+
fill="${bgColor}"
95+
d="M0,0 h${numCells}v${numCells}H0z"
96+
shapeRendering="crispEdges"
97+
/>
98+
<path fill="${fgColor}" d="${fgPath}" shapeRendering="crispEdges" />
99+
${imageTag}
100+
</svg>`;
101+
}

packages/tinyqrc/src/types.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,37 @@ export type Modules = ReturnType<qrcodegen.QrCode["getModules"]>;
55
export type Excavation = { x: number; y: number; w: number; h: number };
66

77
export type ImageSettings = {
8-
/** URL or data URI of the image to embed */
8+
/** URL of the image to embed */
99
src: string;
10-
/** Height of the embedded image in pixels */
11-
height: number;
12-
/** Width of the embedded image in pixels */
13-
width: number;
14-
/** Whether to remove QR code modules under the image for better visibility */
15-
excavate: boolean;
16-
/** Optional X position of the image. Centered if not specified */
10+
/** Height of the image in pixels */
11+
height?: number;
12+
/** Width of the image in pixels */
13+
width?: number;
14+
/** X coordinate of the image in modules */
1715
x?: number;
18-
/** Optional Y position of the image. Centered if not specified */
16+
/** Y coordinate of the image in modules */
1917
y?: number;
18+
/** Whether to excavate (remove) QR code modules under the image */
19+
excavate?: boolean;
20+
};
21+
22+
export type QRCodeOptions = {
23+
/** The content to encode in the QR code */
24+
value: string;
25+
/** Width and height of the QR code in pixels. Defaults to 600 */
26+
size?: number;
27+
/** Error correction level. "L" (7%), "M" (15%), "Q" (25%), or "H" (30%). Defaults to "L" */
28+
level?: string;
29+
/** Background color of the QR code. Accepts any valid CSS color. Defaults to "#FFFFFF" */
30+
bgColor?: string;
31+
/** Foreground color of the QR code. Accepts any valid CSS color. Defaults to "#000000" */
32+
fgColor?: string;
33+
/** Quiet zone margin around the QR code in modules. Defaults to 4 */
34+
margin?: number;
35+
/** Settings for embedding an image/logo in the QR code center */
36+
imageSettings?: ImageSettings;
37+
/** Additional attributes to be added to the SVG element */
38+
[key: string]: unknown;
2039
};
2140

2241
export type QRProps = {

0 commit comments

Comments
 (0)