qr-core 1.0.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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -0
  3. package/dist/bench/bench.d.ts +1 -0
  4. package/dist/bench/bench.js +38 -0
  5. package/dist/src/api/index.d.ts +9 -0
  6. package/dist/src/api/index.js +224 -0
  7. package/dist/src/core/bit-buffer.d.ts +11 -0
  8. package/dist/src/core/bit-buffer.js +53 -0
  9. package/dist/src/core/error.d.ts +8 -0
  10. package/dist/src/core/error.js +16 -0
  11. package/dist/src/core/gf.d.ts +4 -0
  12. package/dist/src/core/gf.js +52 -0
  13. package/dist/src/core/rs.d.ts +1 -0
  14. package/dist/src/core/rs.js +60 -0
  15. package/dist/src/encoding/bitstream.d.ts +7 -0
  16. package/dist/src/encoding/bitstream.js +68 -0
  17. package/dist/src/encoding/encoders.d.ts +13 -0
  18. package/dist/src/encoding/encoders.js +54 -0
  19. package/dist/src/encoding/interleave.d.ts +5 -0
  20. package/dist/src/encoding/interleave.js +49 -0
  21. package/dist/src/encoding/segmentation.d.ts +12 -0
  22. package/dist/src/encoding/segmentation.js +43 -0
  23. package/dist/src/index.d.ts +3 -0
  24. package/dist/src/index.js +3 -0
  25. package/dist/src/layout/patterns.d.ts +12 -0
  26. package/dist/src/layout/patterns.js +120 -0
  27. package/dist/src/mapping/zigzag.d.ts +5 -0
  28. package/dist/src/mapping/zigzag.js +28 -0
  29. package/dist/src/mask/patterns.d.ts +6 -0
  30. package/dist/src/mask/patterns.js +13 -0
  31. package/dist/src/mask/penalty.d.ts +5 -0
  32. package/dist/src/mask/penalty.js +173 -0
  33. package/dist/src/mask/selection.d.ts +12 -0
  34. package/dist/src/mask/selection.js +178 -0
  35. package/dist/src/matrix/bit-matrix.d.ts +46 -0
  36. package/dist/src/matrix/bit-matrix.js +139 -0
  37. package/dist/src/spec/bch.d.ts +11 -0
  38. package/dist/src/spec/bch.js +30 -0
  39. package/dist/src/spec/constants.d.ts +40 -0
  40. package/dist/src/spec/constants.js +37 -0
  41. package/dist/src/spec/tables.d.ts +20 -0
  42. package/dist/src/spec/tables.js +1099 -0
  43. package/dist/src/strategies/index.d.ts +3 -0
  44. package/dist/src/strategies/index.js +3 -0
  45. package/dist/src/strategies/mask.d.ts +16 -0
  46. package/dist/src/strategies/mask.js +9 -0
  47. package/dist/src/strategies/segmentation.d.ts +12 -0
  48. package/dist/src/strategies/segmentation.js +41 -0
  49. package/dist/src/strategies/version.d.ts +12 -0
  50. package/dist/src/strategies/version.js +22 -0
  51. package/dist/src/types/index.d.ts +51 -0
  52. package/dist/src/types/index.js +1 -0
  53. package/dist/tests/golden/golden.test.d.ts +1 -0
  54. package/dist/tests/golden/golden.test.js +26 -0
  55. package/dist/tests/unit/api.test.d.ts +1 -0
  56. package/dist/tests/unit/api.test.js +35 -0
  57. package/dist/tests/unit/bit-matrix.test.d.ts +1 -0
  58. package/dist/tests/unit/bit-matrix.test.js +46 -0
  59. package/dist/tests/unit/core.test.d.ts +1 -0
  60. package/dist/tests/unit/core.test.js +88 -0
  61. package/dist/tests/unit/encoding.test.d.ts +1 -0
  62. package/dist/tests/unit/encoding.test.js +63 -0
  63. package/dist/tests/unit/layout.test.d.ts +1 -0
  64. package/dist/tests/unit/layout.test.js +62 -0
  65. package/dist/tests/unit/masking.test.d.ts +1 -0
  66. package/dist/tests/unit/masking.test.js +31 -0
  67. package/package.json +45 -0
@@ -0,0 +1,3 @@
1
+ export * from "./mask.js";
2
+ export * from "./segmentation.js";
3
+ export * from "./version.js";
@@ -0,0 +1,3 @@
1
+ export * from "./mask.js";
2
+ export * from "./segmentation.js";
3
+ export * from "./version.js";
@@ -0,0 +1,16 @@
1
+ import type { Matrix } from "../matrix/bit-matrix.js";
2
+ import type { EccLevel, MaskId } from "../types/index.js";
3
+ export interface MaskResult {
4
+ mask: MaskId;
5
+ penalties: number[];
6
+ }
7
+ /**
8
+ * Strategy for selecting the best mask pattern.
9
+ */
10
+ export interface MaskStrategy {
11
+ select(matrix: Matrix, ecc: EccLevel): MaskResult;
12
+ }
13
+ /**
14
+ * Evaluates all 8 masks and picks the one with the lowest penalty.
15
+ */
16
+ export declare const BestMaskStrategy: MaskStrategy;
@@ -0,0 +1,9 @@
1
+ import { selectAndApplyBestMask } from "../mask/selection.js";
2
+ /**
3
+ * Evaluates all 8 masks and picks the one with the lowest penalty.
4
+ */
5
+ export const BestMaskStrategy = {
6
+ select(matrix, ecc) {
7
+ return selectAndApplyBestMask(matrix, ecc);
8
+ },
9
+ };
@@ -0,0 +1,12 @@
1
+ import type { Segment } from "../encoding/segmentation.js";
2
+ /**
3
+ * Strategy for splitting input data into segments with optimal encoding modes.
4
+ */
5
+ export interface SegmentationStrategy {
6
+ apply(input: string): Segment[];
7
+ }
8
+ /**
9
+ * Default greedy strategy that switches mode when a character requires it.
10
+ * Not optimal but efficient and simple.
11
+ */
12
+ export declare const GreedySegmentationStrategy: SegmentationStrategy;
@@ -0,0 +1,41 @@
1
+ import { segmentize } from "../encoding/segmentation.js";
2
+ /**
3
+ * Default greedy strategy that switches mode when a character requires it.
4
+ * Not optimal but efficient and simple.
5
+ */
6
+ export const GreedySegmentationStrategy = {
7
+ apply(input) {
8
+ const greedy = segmentize(input);
9
+ // Optimization: If mixed segments, check if pure Byte mode is smaller.
10
+ // "Hello World" case: Greedy splits into Alpha/Byte/Alpha/Byte = high overhead.
11
+ // Pure Byte is often better for short ASCII strings.
12
+ if (greedy.length > 1) {
13
+ const byteSeg = { mode: "byte", data: input, count: input.length };
14
+ const greedyBits = estimateCost(greedy);
15
+ const byteBits = estimateCost([byteSeg]);
16
+ if (byteBits < greedyBits) {
17
+ return [byteSeg];
18
+ }
19
+ }
20
+ return greedy;
21
+ },
22
+ };
23
+ function estimateCost(segments) {
24
+ let bits = 0;
25
+ for (const s of segments) {
26
+ // Overhead: Mode(4) + Count(var). Use V1 constants (conservative overhead cost).
27
+ // Numeric: 10, Alpha: 9, Byte: 8
28
+ if (s.mode === "numeric")
29
+ bits += 4 + 10 + Math.ceil(s.count / 3) * 10;
30
+ // Alpha: 11 bits per 2 chars. 6 for last.
31
+ else if (s.mode === "alphanumeric") {
32
+ const pairs = Math.floor(s.count / 2);
33
+ const odd = s.count % 2;
34
+ bits += 4 + 9 + pairs * 11 + odd * 6;
35
+ }
36
+ // Byte
37
+ else
38
+ bits += 4 + 8 + s.count * 8;
39
+ }
40
+ return bits;
41
+ }
@@ -0,0 +1,12 @@
1
+ import type { Segment } from "../encoding/segmentation.js";
2
+ import type { EccLevel } from "../types/index.js";
3
+ /**
4
+ * Strategy for selecting the smallest QR version that fits the data.
5
+ */
6
+ export interface VersionStrategy {
7
+ select(segments: Segment[], ecc: EccLevel): number;
8
+ }
9
+ /**
10
+ * Iterates from MIN_VERSION to MAX_VERSION to find the first one that fits.
11
+ */
12
+ export declare const SmallestVersionStrategy: VersionStrategy;
@@ -0,0 +1,22 @@
1
+ import { QrException } from "../core/error.js";
2
+ import { buildBitstream } from "../encoding/bitstream.js";
3
+ import { MAX_VERSION, MIN_VERSION } from "../spec/constants.js";
4
+ /**
5
+ * Iterates from MIN_VERSION to MAX_VERSION to find the first one that fits.
6
+ */
7
+ export const SmallestVersionStrategy = {
8
+ select(segments, ecc) {
9
+ for (let v = MIN_VERSION; v <= MAX_VERSION; v++) {
10
+ try {
11
+ // Try to build bitstream specifically for this version to check capacity
12
+ // This is less efficient than just checking capacity tables but safer
13
+ // as it accounts for mode overheads correctly
14
+ // TODO: Optimize by checking capacity table directly first
15
+ buildBitstream(segments, v, ecc);
16
+ return v;
17
+ }
18
+ catch (_e) { }
19
+ }
20
+ throw new QrException("DATA_TOO_LARGE", "Data exceeds maximum QR capacity");
21
+ },
22
+ };
@@ -0,0 +1,51 @@
1
+ export type EccLevel = "L" | "M" | "Q" | "H";
2
+ export type QrMode = "auto" | "byte" | "alphanumeric" | "numeric";
3
+ export type MaskId = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
4
+ export interface EncodeOptions {
5
+ ecc?: EccLevel;
6
+ version?: number | "auto";
7
+ mask?: MaskId | "auto";
8
+ mode?: QrMode;
9
+ charset?: "utf-8" | "iso-8859-1";
10
+ quietZone?: number;
11
+ strict?: boolean;
12
+ }
13
+ export interface BitMatrix {
14
+ readonly size: number;
15
+ get(x: number, y: number): 0 | 1;
16
+ toPacked?(): Uint8Array;
17
+ toRows?(): ReadonlyArray<Uint8Array>;
18
+ }
19
+ export interface SegmentInfo {
20
+ mode: Exclude<QrMode, "auto">;
21
+ charCount: number;
22
+ }
23
+ export interface QrMeta {
24
+ quietZone: number;
25
+ modeUsed: Exclude<QrMode, "auto"> | "mixed";
26
+ segments: ReadonlyArray<SegmentInfo>;
27
+ maskPenalties?: ReadonlyArray<number>;
28
+ chosenPenalty?: number;
29
+ details?: Readonly<Record<string, unknown>>;
30
+ }
31
+ export type QrErrorCode = "INVALID_OPTIONS" | "UNSUPPORTED_MODE" | "DATA_TOO_LARGE" | "INVALID_VERSION" | "INVALID_MASK" | "ENCODING_FAILED" | "INTERNAL_INVARIANT_BROKEN";
32
+ export interface QrError {
33
+ code: QrErrorCode;
34
+ message: string;
35
+ details?: unknown;
36
+ }
37
+ export type Result<T, E> = {
38
+ ok: true;
39
+ value: T;
40
+ } | {
41
+ ok: false;
42
+ error: E;
43
+ };
44
+ export interface QrCode {
45
+ version: number;
46
+ ecc: EccLevel;
47
+ mask: MaskId;
48
+ size: number;
49
+ matrix: BitMatrix;
50
+ meta?: QrMeta;
51
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import { encode } from "../../src/api/index";
5
+ const vectorsDir = path.join(__dirname, "../vectors");
6
+ const vectorFiles = fs
7
+ .readdirSync(vectorsDir)
8
+ .filter((f) => f.endsWith(".json"));
9
+ let vectors = [];
10
+ for (const file of vectorFiles) {
11
+ const content = JSON.parse(fs.readFileSync(path.join(vectorsDir, file), "utf-8"));
12
+ vectors = vectors.concat(content);
13
+ }
14
+ describe("Golden Vectors", () => {
15
+ for (const vec of vectors) {
16
+ it(`should match vector: ${vec.name}`, () => {
17
+ // Normalize inputs
18
+ // biome-ignore lint/suspicious/noExplicitAny: generic casting for options
19
+ const opts = { ...vec.options };
20
+ const result = encode(vec.input.value, opts);
21
+ expect(result.version).toBe(vec.expected.version);
22
+ expect(result.size).toBe(vec.expected.size);
23
+ // In a real golden test, we would compare the entire matrix hash/bits
24
+ });
25
+ }
26
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { encode, encodeSafe } from "../../src/api/index";
3
+ describe("Public API", () => {
4
+ it("should encode simple string", () => {
5
+ const result = encode("Hello World");
6
+ expect(result.version).toBeGreaterThan(0);
7
+ expect(result.size).toBeGreaterThan(0);
8
+ expect(result.matrix).toBeDefined();
9
+ });
10
+ it("should allow forcing version", () => {
11
+ const result = encode("A", { version: 5 });
12
+ expect(result.version).toBe(5);
13
+ expect(result.size).toBe(21 + 4 * (5 - 1));
14
+ });
15
+ it("should throw if data too large for fixed version", () => {
16
+ // Version 1 can only hold ~17 bytes (Ecc L)
17
+ // "A" * 100 needs version ~4
18
+ const largeData = "A".repeat(100);
19
+ // Expect message to contain "Data requires version"
20
+ expect(() => encode(largeData, { version: 1 })).toThrow(/Data requires version/);
21
+ });
22
+ it("should safe encode without throwing", () => {
23
+ const largeData = "A".repeat(100);
24
+ const result = encodeSafe(largeData, { version: 1 });
25
+ expect(result.ok).toBe(false);
26
+ if (!result.ok) {
27
+ expect(result.error.code).toBe("DATA_TOO_LARGE");
28
+ }
29
+ });
30
+ it("should support Uint8Array input", () => {
31
+ const input = new Uint8Array([65, 66, 67]);
32
+ const result = encode(input, { mode: "byte" });
33
+ expect(result.version).toBe(1);
34
+ });
35
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { Matrix } from "../../src/matrix/bit-matrix";
3
+ describe("BitMatrix", () => {
4
+ it("should initialize with correct size and default values", () => {
5
+ const size = 21;
6
+ const matrix = new Matrix(size);
7
+ expect(matrix.size).toBe(size);
8
+ expect(matrix.get(0, 0)).toBe(0);
9
+ expect(matrix.get(size - 1, size - 1)).toBe(0);
10
+ });
11
+ it("should set and get values", () => {
12
+ const matrix = new Matrix(21);
13
+ matrix.set(5, 5, true);
14
+ expect(matrix.get(5, 5)).toBe(1);
15
+ matrix.set(5, 5, false);
16
+ expect(matrix.get(5, 5)).toBe(0);
17
+ });
18
+ it("should handle reserved modules", () => {
19
+ const matrix = new Matrix(21);
20
+ matrix.setReserved(10, 10, true);
21
+ expect(matrix.isReserved(10, 10)).toBe(true);
22
+ expect(matrix.get(10, 10)).toBe(1);
23
+ // Normal set should not overwrite reserved
24
+ matrix.set(10, 10, false);
25
+ expect(matrix.get(10, 10)).toBe(1);
26
+ });
27
+ it("should throw on out of bounds access", () => {
28
+ const matrix = new Matrix(21);
29
+ expect(() => matrix.get(-1, 0)).toThrow();
30
+ expect(() => matrix.get(0, 21)).toThrow();
31
+ expect(() => matrix.set(21, 0, true)).toThrow();
32
+ expect(() => matrix.setReserved(0, -1, true)).toThrow();
33
+ });
34
+ it("should serialize to packed bytes correct MSB-first order", () => {
35
+ // Size 9 -> 2 bytes per row
36
+ // Row 0: 10000000 1.......
37
+ const matrix = new Matrix(9);
38
+ matrix.set(0, 0, true); // First bit
39
+ matrix.set(8, 0, true); // 9th bit (first bit of second byte)
40
+ const packed = matrix.toPacked();
41
+ expect(packed.length).toBe(9 * 2);
42
+ // Row 0
43
+ expect(packed[0]).toBe(0x80); // 10000000
44
+ expect(packed[1]).toBe(0x80); // 10000000 (padded)
45
+ });
46
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { BitBuffer } from "../../src/core/bit-buffer";
3
+ import { EXP, inv, LOG, mul } from "../../src/core/gf";
4
+ import { computeEcc } from "../../src/core/rs";
5
+ describe("GF(256)", () => {
6
+ it("should have correct table sizes", () => {
7
+ expect(EXP.length).toBe(512);
8
+ expect(LOG.length).toBe(256);
9
+ });
10
+ it("EXP[0] should be 1", () => {
11
+ expect(EXP[0]).toBe(1);
12
+ expect(EXP[255]).toBe(1);
13
+ });
14
+ it("LOG[1] should be 0", () => {
15
+ expect(LOG[1]).toBe(0);
16
+ });
17
+ it("multiplication should be correct", () => {
18
+ expect(mul(0, 5)).toBe(0);
19
+ expect(mul(5, 0)).toBe(0);
20
+ expect(mul(1, 10)).toBe(10);
21
+ // 2 * 2 = 4
22
+ expect(mul(2, 2)).toBe(4);
23
+ // Inverse check
24
+ const a = 123;
25
+ const invA = inv(a);
26
+ expect(mul(a, invA)).toBe(1);
27
+ });
28
+ });
29
+ describe("BitBuffer", () => {
30
+ it("should append bits correctly", () => {
31
+ const bb = new BitBuffer();
32
+ bb.put(0b1, 1);
33
+ bb.put(0b0, 1);
34
+ bb.put(0b1, 1);
35
+ // current: 101 (5)
36
+ // length: 3
37
+ expect(bb.length).toBe(3);
38
+ const bytes = bb.toBytes();
39
+ // 10100000 = 0xA0
40
+ expect(bytes[0]).toBe(0xa0);
41
+ });
42
+ it("should handle multi-bit put", () => {
43
+ const bb = new BitBuffer();
44
+ // append 0xABC (101010111100) 12 bits
45
+ bb.put(0xabc, 12);
46
+ expect(bb.length).toBe(12);
47
+ const bytes = bb.toBytes();
48
+ // 10101011 1100xxxx
49
+ // 0xAB 0xC0
50
+ expect(bytes[0]).toBe(0xab);
51
+ expect((bytes[1] ?? 0) & 0xf0).toBe(0xc0);
52
+ });
53
+ });
54
+ describe("Reed-Solomon", () => {
55
+ it("should return all zeros for zero data", () => {
56
+ const data = new Uint8Array([0, 0, 0]);
57
+ const ecc = computeEcc(data, 5);
58
+ expect(ecc).toEqual(new Uint8Array(5));
59
+ });
60
+ // Simple known vector check could be added here if we had one.
61
+ // Using a known property check:
62
+ // If we encode M(x), then M(x)x^n + R(x) is a codeword.
63
+ // This means it evaluates to 0 at the roots of G(x)?
64
+ // Roots of G(x) are 2^0, 2^1 ... 2^{n-1}.
65
+ // Let's check with eccLen=2. Roots 2^0=1, 2^1=2.
66
+ // Codeword C(x) should satisfy C(1)=0 and C(2)=0.
67
+ it("should produce valid codewords (eval to 0 at roots)", () => {
68
+ const message = new Uint8Array([10, 20, 30]);
69
+ const eccLen = 2; // Roots: 1, 2
70
+ const ecc = computeEcc(message, eccLen);
71
+ // Construct full codeword: message * x^n + ecc
72
+ // Coefficients: [10, 20, 30, ecc[0], ecc[1]]
73
+ const codeword = new Uint8Array([...message, ...ecc]);
74
+ // Evaluate at root 1 (2^0) = 1
75
+ // Eval(x) = c0*x^4 + c1*x^3 + ...
76
+ function evalPoly(poly, x) {
77
+ let res = 0;
78
+ for (const coeff of poly) {
79
+ res = mul(res, x) ^ coeff;
80
+ }
81
+ return res;
82
+ }
83
+ const root0 = EXP[0] ?? 0; // 1
84
+ const root1 = EXP[1] ?? 0; // 2
85
+ expect(evalPoly(codeword, root0)).toBe(0);
86
+ expect(evalPoly(codeword, root1)).toBe(0);
87
+ });
88
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { BitBuffer } from "../../src/core/bit-buffer";
3
+ import { encodeAlphanumeric, encodeByte, encodeNumeric, } from "../../src/encoding/encoders";
4
+ import { interleave } from "../../src/encoding/interleave";
5
+ import { segmentize } from "../../src/encoding/segmentation";
6
+ describe("Encoders", () => {
7
+ it("should encode numeric data", () => {
8
+ const buffer = new BitBuffer();
9
+ // "01234567" -> "012" (10 bits) + "345" (10 bits) + "67" (7 bits)
10
+ encodeNumeric("01234567", buffer);
11
+ // 012 = 0000001100
12
+ // 345 = 0101011001
13
+ // 67 = 1000011
14
+ expect(buffer.length).toBe(27);
15
+ });
16
+ it("should encode alphanumeric data", () => {
17
+ const buffer = new BitBuffer();
18
+ // "AC-42" -> "AC" (11) + "-4" (11) + "2" (6)
19
+ encodeAlphanumeric("AC-42", buffer);
20
+ expect(buffer.length).toBe(11 + 11 + 6);
21
+ });
22
+ it("should encode byte data", () => {
23
+ const buffer = new BitBuffer();
24
+ encodeByte("Hello", buffer);
25
+ expect(buffer.length).toBe(5 * 8);
26
+ });
27
+ });
28
+ describe("Segmentation", () => {
29
+ it("should segment simple numeric", () => {
30
+ const segments = segmentize("0123456789");
31
+ expect(segments).toHaveLength(1);
32
+ expect(segments[0]?.mode).toBe("numeric");
33
+ });
34
+ it("should segment simple alphanumeric", () => {
35
+ const segments = segmentize("HELLO WORLD");
36
+ expect(segments).toHaveLength(1);
37
+ expect(segments[0]?.mode).toBe("alphanumeric");
38
+ });
39
+ it("should segment mixed content", () => {
40
+ // "123ABC123" -> numeric, alphanumeric, numeric
41
+ // Our greedy segmenter might merge if it's suboptimal, but let's see current behavior.
42
+ // "123" (num) "ABC" (alnum) "123" (num)
43
+ const segments = segmentize("123ABC123");
44
+ // Actually, greedy might switch eagerly.
45
+ // "123" -> numeric
46
+ // "A" -> switch to Alnum
47
+ // "123" -> switch to Numeric
48
+ expect(segments.map((s) => s.mode)).toEqual([
49
+ "numeric",
50
+ "alphanumeric",
51
+ "numeric",
52
+ ]);
53
+ });
54
+ });
55
+ describe("Interleave", () => {
56
+ it("should interleave data and ecc correctly", () => {
57
+ // Mock data that fits version 1-L (19 data bytes)
58
+ const data = new Uint8Array(19).fill(0x11);
59
+ // Version 1-L: 19 data bytes, 7 ECC bytes. Total 26.
60
+ const result = interleave(data, 1, "L");
61
+ expect(result.length).toBe(26);
62
+ });
63
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { setupFunctionPatterns } from "../../src/layout/patterns";
3
+ import { Matrix } from "../../src/matrix/bit-matrix";
4
+ describe("Layout Patterns", () => {
5
+ it("should set up finder patterns", () => {
6
+ const matrix = new Matrix(21);
7
+ setupFunctionPatterns(matrix, 1);
8
+ // Top Left Finder
9
+ expect(matrix.isReserved(3, 3)).toBe(true); // Center of 3x3
10
+ // Finder is 7x7. (0,0) to (6,6). Center (3,3).
11
+ // (3,3) is dark.
12
+ expect(matrix.get(3, 3)).toBe(1);
13
+ // (0,0) is dark (outer ring)
14
+ expect(matrix.get(0, 0)).toBe(1);
15
+ // (1,1) is light (gap between outer and inner) - wait no
16
+ // 7x7 finder:
17
+ // xxxxxxx
18
+ // x.....x
19
+ // x.xxx.x
20
+ // x.xxx.x
21
+ // x.xxx.x
22
+ // x.....x
23
+ // xxxxxxx
24
+ // (0,0) dark
25
+ // (1,1) light?
26
+ // Logic: max(abs(x-3), abs(y-3))
27
+ // (1,1): abs(-2)=2. max(2,2)=2.
28
+ // isDark = (max === 3) || (max <= 1).
29
+ // 2 is not 3, nor <= 1. So light. Correct.
30
+ expect(matrix.get(1, 1)).toBe(0);
31
+ });
32
+ it("should set up timing patterns", () => {
33
+ const matrix = new Matrix(21);
34
+ setupFunctionPatterns(matrix, 1);
35
+ // Row 6, Cols 8..Size-8 (8..13 for V1)
36
+ // (8, 6) -> i=8. even -> dark (true)
37
+ // (9, 6) -> light
38
+ expect(matrix.isReserved(8, 6)).toBe(true);
39
+ expect(matrix.get(8, 6)).toBe(1);
40
+ expect(matrix.get(9, 6)).toBe(0);
41
+ });
42
+ it("should set up alignment patterns for V2", () => {
43
+ const matrix = new Matrix(25); // V2 size
44
+ setupFunctionPatterns(matrix, 2);
45
+ // V2 alignment at 6, 18
46
+ // (6,6) is finder.
47
+ // (18,18) should be alignment center.
48
+ // Alignment is 5x5 center (18,18).
49
+ expect(matrix.isReserved(18, 18)).toBe(true);
50
+ // Center is dark
51
+ expect(matrix.get(18, 18)).toBe(1);
52
+ });
53
+ it("should set up version info for V7", () => {
54
+ const matrix = new Matrix(45); // V7 size
55
+ setupFunctionPatterns(matrix, 7);
56
+ // Version info is 3x6 block above bottom-left finder and left of top-right finder.
57
+ // Bottom-left finder is at (0, Size-7).
58
+ // Version info above it: Row Size-11..Size-9. Col 0..5
59
+ // Let's check reservation.
60
+ expect(matrix.isReserved(0, 45 - 11)).toBe(true);
61
+ });
62
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { applyMask, selectAndApplyBestMask } from "../../src/mask/selection";
3
+ import { Matrix } from "../../src/matrix/bit-matrix";
4
+ describe("Masking", () => {
5
+ it("should allow applying a specific mask", () => {
6
+ const matrix = new Matrix(21);
7
+ // Mask 0: (x+y)%2 == 0
8
+ applyMask(matrix, 0);
9
+ // (0,0) -> even -> flip inv (0->1)
10
+ expect(matrix.get(0, 0)).toBe(1);
11
+ // (0,1) -> odd -> 0
12
+ expect(matrix.get(0, 1)).toBe(0);
13
+ });
14
+ it("should select the best mask", () => {
15
+ const matrix = new Matrix(21);
16
+ // Empty matrix -> penalty calculation will favor a mask that breaks up large white areas
17
+ // or one that doesn't create false patterns.
18
+ const result = selectAndApplyBestMask(matrix, "M");
19
+ expect(result.mask).toBeGreaterThanOrEqual(0);
20
+ expect(result.mask).toBeLessThanOrEqual(7);
21
+ expect(result.penalties).toHaveLength(8);
22
+ // Ensure format info was written
23
+ // (8,0) to (8,8) area
24
+ let formatInfoWritten = false;
25
+ for (let i = 0; i < 9; i++) {
26
+ if (matrix.isReserved(i, 8))
27
+ formatInfoWritten = true;
28
+ }
29
+ expect(formatInfoWritten).toBe(true);
30
+ });
31
+ });
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "qr-core",
3
+ "version": "1.0.0",
4
+ "description": "Deterministic, zero-dependency QR Code Model 2 encoder (matrix-only)",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "prepublishOnly": "npm run build",
17
+ "test": "vitest",
18
+ "lint": "biome check src tests bench",
19
+ "format": "biome format --write src tests bench",
20
+ "bench": "npm run build && node dist/bench/bench.js"
21
+ },
22
+ "keywords": [
23
+ "qr",
24
+ "qrcode",
25
+ "qr-code",
26
+ "encoder",
27
+ "matrix",
28
+ "typescript",
29
+ "esm"
30
+ ],
31
+ "author": "QR Core Contributors",
32
+ "license": "MIT",
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "sideEffects": false,
39
+ "devDependencies": {
40
+ "@biomejs/biome": "^2.3.13",
41
+ "@types/node": "^25.1.0",
42
+ "typescript": "^5.9.3",
43
+ "vitest": "^3.2.4"
44
+ }
45
+ }