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
1212import { 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 */
18125export 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