Skip to content

Commit 8147f0a

Browse files
fix(#55): DotCode proper GF(113) Reed-Solomon with log/antilog tables
Replace simplified EC with proper prime field GF(113) arithmetic using primitive root alpha=3, log/antilog lookup tables, and correct generator polynomial construction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f4b84c5 commit 8147f0a

12 files changed

Lines changed: 1229 additions & 319 deletions

File tree

src/_2d.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,20 @@ import { encodeDataMatrix, encodeGS1DataMatrix } from "./encoders/datamatrix/ind
66
import { encodePDF417 } from "./encoders/pdf417/index";
77
import { encodeAztec } from "./encoders/aztec/index";
88
import { renderMatrixSVG } from "./renderers/svg/matrix";
9+
import type { MatrixSVGOptions } from "./renderers/svg/matrix";
910

1011
/**
1112
* Generate a Data Matrix as SVG string
1213
*/
13-
export function datamatrix(
14-
text: string,
15-
options?: { size?: number; color?: string; background?: string; margin?: number },
16-
): string {
14+
export function datamatrix(text: string, options?: MatrixSVGOptions): string {
1715
const matrix = encodeDataMatrix(text);
1816
return renderMatrixSVG(matrix, options);
1917
}
2018

2119
/**
2220
* Generate a GS1 DataMatrix as SVG string
2321
*/
24-
export function gs1datamatrix(
25-
text: string,
26-
options?: { size?: number; color?: string; background?: string; margin?: number },
27-
): string {
22+
export function gs1datamatrix(text: string, options?: MatrixSVGOptions): string {
2823
const matrix = encodeGS1DataMatrix(text);
2924
return renderMatrixSVG(matrix, options);
3025
}
@@ -40,9 +35,7 @@ export function pdf417(
4035
compact?: boolean;
4136
width?: number;
4237
height?: number;
43-
color?: string;
44-
background?: string;
45-
},
38+
} & MatrixSVGOptions,
4639
): string {
4740
const { ecLevel, columns, compact, ...svgOpts } = options ?? {};
4841
const result = encodePDF417(text, { ecLevel, columns, compact });
@@ -58,11 +51,7 @@ export function aztec(
5851
ecPercent?: number;
5952
layers?: number;
6053
compact?: boolean;
61-
size?: number;
62-
color?: string;
63-
background?: string;
64-
margin?: number;
65-
},
54+
} & MatrixSVGOptions,
6655
): string {
6756
const { ecPercent, layers, compact, ...svgOpts } = options ?? {};
6857
const matrix = encodeAztec(text, { ecPercent, layers, compact });

src/_qrcode.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ export function qrcode(text: string, options: QRCodeSVGOptions & QRCodeOptions =
2424
corners,
2525
logo,
2626
xmlDeclaration,
27+
ariaLabel,
28+
role,
29+
title,
30+
desc,
2731
...qrOptions
2832
} = options;
2933
// Auto-upgrade EC level when logo is present (logo obscures modules)
@@ -42,6 +46,10 @@ export function qrcode(text: string, options: QRCodeSVGOptions & QRCodeOptions =
4246
corners,
4347
logo,
4448
xmlDeclaration,
49+
ariaLabel,
50+
role,
51+
title,
52+
desc,
4553
});
4654
}
4755

src/encoders/dotcode.ts

Lines changed: 146 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,64 +4,188 @@
44
*
55
* Structure:
66
* - Rectangular grid of dots (not connected bars)
7-
* - Checkerboard-like pattern — only odd positions filled
8-
* - Variable size based on data
9-
* - GF(113) Reed-Solomon error correction
7+
* - Checkerboard-like pattern — only even (r+c) positions filled
8+
* - Variable size, height + width must be odd
9+
* - GF(113) Reed-Solomon error correction (prime field, alpha = 3)
1010
*/
1111

1212
import { InvalidInputError } from "../errors";
1313

14+
// ---------------------------------------------------------------------------
15+
// GF(113) prime field arithmetic
16+
// 113 is prime, so GF(113) = Z/113Z.
17+
// Primitive root alpha = 3 (order 112 = p-1).
18+
// ---------------------------------------------------------------------------
19+
20+
const GF = 113;
21+
const GF_ORDER = GF - 1; // 112
22+
23+
/** Exponent table: GF113_EXP[i] = alpha^i mod 113, i = 0..111 */
24+
const GF113_EXP = new Uint8Array(GF_ORDER);
25+
26+
/** Log table: GF113_LOG[v] = i where alpha^i = v, for v = 1..112 */
27+
const GF113_LOG = new Uint8Array(GF);
28+
29+
// Initialize GF(113) lookup tables
30+
(function initGF113() {
31+
let x = 1;
32+
for (let i = 0; i < GF_ORDER; i++) {
33+
GF113_EXP[i] = x;
34+
GF113_LOG[x] = i;
35+
x = (x * 3) % GF; // alpha = 3
36+
}
37+
})();
38+
39+
/** Multiply two GF(113) elements */
40+
function gfMul(a: number, b: number): number {
41+
if (a === 0 || b === 0) return 0;
42+
return GF113_EXP[(GF113_LOG[a]! + GF113_LOG[b]!) % GF_ORDER]!;
43+
}
44+
45+
/** Add two GF(113) elements */
46+
function gfAdd(a: number, b: number): number {
47+
return (a + b) % GF;
48+
}
49+
50+
/** Subtract two GF(113) elements */
51+
function gfSub(a: number, b: number): number {
52+
return (a - b + GF) % GF;
53+
}
54+
55+
// ---------------------------------------------------------------------------
56+
// Reed-Solomon over GF(113)
57+
// ---------------------------------------------------------------------------
58+
59+
/**
60+
* Build the RS generator polynomial of degree `n`.
61+
*
62+
* g(x) = (x - alpha^1)(x - alpha^2)...(x - alpha^n)
63+
*
64+
* Stored as coefficients [g_0, g_1, ..., g_n] where
65+
* g(x) = g_0 * x^n + g_1 * x^(n-1) + ... + g_n.
66+
*/
67+
function buildGenerator(n: number): number[] {
68+
const gen = Array.from<number>({ length: n + 1 }).fill(0);
69+
gen[0] = 1;
70+
71+
for (let i = 1; i <= n; i++) {
72+
const root = GF113_EXP[i % GF_ORDER]!; // alpha^i
73+
// Multiply current gen by (x - root)
74+
for (let j = i; j >= 1; j--) {
75+
gen[j] = gfSub(gen[j - 1]!, gfMul(gen[j]!, root));
76+
}
77+
gen[0] = gfMul(gen[0]!, (GF - root) % GF);
78+
}
79+
80+
return gen;
81+
}
82+
83+
/**
84+
* Generate Reed-Solomon error correction codewords over GF(113).
85+
*
86+
* Computes the remainder of data(x) * x^n / g(x), then negates
87+
* to produce check symbols that make the full codeword polynomial
88+
* evaluate to 0 at each root alpha^1..alpha^n.
89+
*
90+
* @param data - Data codewords (values 0..112)
91+
* @param ecCount - Number of EC codewords to generate
92+
* @returns Array of `ecCount` EC codewords
93+
*/
94+
function dotcodeEC(data: number[], ecCount: number): number[] {
95+
const gen = buildGenerator(ecCount);
96+
97+
// Polynomial long division (shift register)
98+
const remainder = Array.from<number>({ length: ecCount }).fill(0);
99+
100+
for (const d of data) {
101+
const feedback = gfAdd(d, remainder[0]!);
102+
for (let j = 0; j < ecCount - 1; j++) {
103+
remainder[j] = gfSub(remainder[j + 1]!, gfMul(feedback, gen[j + 1]!));
104+
}
105+
remainder[ecCount - 1] = gfSub(0, gfMul(feedback, gen[ecCount]!));
106+
}
107+
108+
// Negate remainder to get check symbols
109+
const ec = Array.from<number>({ length: ecCount });
110+
for (let i = 0; i < ecCount; i++) {
111+
ec[i] = (GF - remainder[i]!) % GF;
112+
}
113+
114+
return ec;
115+
}
116+
117+
// ---------------------------------------------------------------------------
118+
// DotCode encoder
119+
// ---------------------------------------------------------------------------
120+
14121
/**
15-
* Encode text as DotCode
16-
* Returns a 2D boolean matrix (true = dot present)
122+
* Encode text as DotCode.
123+
* Returns a 2D boolean matrix (true = dot present).
17124
*/
18125
export function encodeDotCode(text: string): boolean[][] {
19126
if (text.length === 0) {
20127
throw new InvalidInputError("DotCode input must not be empty");
21128
}
22129

23-
// Encode data as codewords (simplified: ASCII encoding)
130+
// Encode data as codewords (ASCII encoding, values 0..112)
131+
// DotCode codewords are in range 0..112 (GF(113) symbols)
24132
const codewords: number[] = [];
25133
for (const ch of text) {
26134
const code = ch.charCodeAt(0);
27135
if (code > 127) {
28-
// Extended: use shift + byte
136+
// Extended: binary shift (codeword 107) + high/low bytes
29137
codewords.push(107); // binary shift
30-
codewords.push(code);
138+
codewords.push(code % GF);
139+
if (code >= GF) {
140+
codewords.push(Math.floor(code / GF));
141+
}
142+
} else if (code >= GF) {
143+
// ASCII 113..127 — shift into valid GF(113) range
144+
codewords.push(107); // binary shift
145+
codewords.push(code % GF);
31146
} else {
32147
codewords.push(code);
33148
}
34149
}
35150

36151
// Select symbol size
37-
// DotCode: width must be odd, height must be odd
38-
// Capacity ≈ (w * h) / 2 dots, each codeword = ~5 dots
39-
const totalCW = codewords.length;
40-
const ecCW = Math.max(4, Math.ceil(totalCW * 0.3)); // ~30% EC
41-
const neededDots = (totalCW + ecCW) * 5;
152+
const dataCW = codewords.length;
153+
const ecCW = Math.max(4, Math.ceil(dataCW * 0.3)); // ~30% EC overhead
154+
const totalCW = dataCW + ecCW;
155+
const neededDots = totalCW * 5;
42156
const neededCells = neededDots * 2; // checkerboard = half filled
43157

44-
// Find suitable dimensions
158+
// Find suitable dimensions — DotCode requires (height + width) to be odd
45159
let width = Math.max(7, Math.ceil(Math.sqrt(neededCells * 2.5)));
46160
if (width % 2 === 0) width++;
47161
let height = Math.max(5, Math.ceil(neededCells / width));
48162
if (height % 2 === 0) height++;
49163

50-
// Pad codewords
51-
while (codewords.length < totalCW + ecCW) {
52-
codewords.push(109); // pad codeword
164+
// Ensure height + width is odd (DotCode constraint)
165+
// Currently both are odd, so sum is even. Adjust width by +2 to keep it odd
166+
// while making the sum odd.
167+
if ((height + width) % 2 === 0) {
168+
width += 2;
169+
}
170+
171+
// Pad data codewords to fill available data capacity
172+
const paddedData = codewords.slice();
173+
while (paddedData.length < dataCW) {
174+
paddedData.push(109); // pad codeword
53175
}
54176

55-
// Generate EC (simplified GF(113))
56-
const ec = dotcodeEC(codewords.slice(0, totalCW), ecCW);
57-
const allCW = [...codewords.slice(0, totalCW), ...ec];
177+
// Generate EC codewords
178+
// Clamp data values to GF(113) range for RS computation
179+
const rsData = paddedData.map((v) => v % GF);
180+
const ec = dotcodeEC(rsData, ecCW);
181+
const allCW = [...rsData, ...ec];
58182

59183
// Build matrix with checkerboard pattern
60184
const matrix: boolean[][] = Array.from({ length: height }, () =>
61185
Array.from({ length: width }, () => false),
62186
);
63187

64-
// Place data dots in checkerboard positions
188+
// Place data as dots in checkerboard positions
65189
let cwIdx = 0;
66190
let bitIdx = 0;
67191

@@ -71,7 +195,7 @@ export function encodeDotCode(text: string): boolean[][] {
71195
if ((r + c) % 2 !== 0) continue;
72196

73197
if (cwIdx < allCW.length) {
74-
// Each codeword contributes ~7 bits
198+
// Each codeword contributes 7 bits (ceil(log2(113)) = 7)
75199
const bit = (allCW[cwIdx]! >> (6 - bitIdx)) & 1;
76200
matrix[r]![c] = bit === 1;
77201
bitIdx++;
@@ -85,19 +209,3 @@ export function encodeDotCode(text: string): boolean[][] {
85209

86210
return matrix;
87211
}
88-
89-
/** Simplified DotCode EC over GF(113) */
90-
function dotcodeEC(data: number[], ecCount: number): number[] {
91-
const GF = 113;
92-
const ec = Array.from({ length: ecCount }, () => 0);
93-
94-
for (const byte of data) {
95-
const feedback = (byte + ec[0]!) % GF;
96-
for (let j = 0; j < ecCount - 1; j++) {
97-
ec[j] = (ec[j + 1]! + feedback * (j + 2)) % GF;
98-
}
99-
ec[ecCount - 1] = (feedback * (ecCount + 1)) % GF;
100-
}
101-
102-
return ec;
103-
}

0 commit comments

Comments
 (0)