Skip to content

Commit 7013646

Browse files
fix(#59): add full Code 128 pattern table to Codablock F encoder
Replace truncated 33-entry pattern table with complete 106-entry table. Now supports all Code 128 characters including lowercase, special chars, and proper Code A/B/C charset switching per row. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5232d84 commit 7013646

2 files changed

Lines changed: 231 additions & 50 deletions

File tree

src/encoders/codablock-f.ts

Lines changed: 223 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,48 +8,122 @@
88

99
import { InvalidInputError, CapacityError } from "../errors";
1010

11-
// Code 128 patterns (same as code128.ts but self-contained for independence)
11+
// Code 128 constants
1212
const START_C = 105;
13-
const _STOP = 106;
13+
const CODE_A = 101;
1414
const CODE_B = 100;
15+
const CODE_C = 99;
1516

17+
// Full Code 128 encoding patterns (bar/space widths), indices 0-105
18+
// Each pattern is 6 elements: bar, space, bar, space, bar, space
1619
const PATTERNS: number[][] = [
17-
[2, 1, 2, 2, 2, 2],
18-
[2, 2, 2, 1, 2, 2],
19-
[2, 2, 2, 2, 2, 1],
20-
[1, 2, 1, 2, 2, 3],
21-
[1, 2, 1, 3, 2, 2],
22-
[1, 3, 1, 2, 2, 2],
23-
[1, 2, 2, 2, 1, 3],
24-
[1, 2, 2, 3, 1, 2],
25-
[1, 3, 2, 2, 1, 2],
26-
[2, 2, 1, 2, 1, 3],
27-
[2, 2, 1, 3, 1, 2],
28-
[2, 3, 1, 2, 1, 2],
29-
[1, 1, 2, 2, 3, 2],
30-
[1, 2, 2, 1, 3, 2],
31-
[1, 2, 2, 2, 3, 1],
32-
[1, 1, 3, 2, 2, 2],
33-
[1, 2, 3, 1, 2, 2],
34-
[1, 2, 3, 2, 2, 1],
35-
[2, 2, 3, 2, 1, 1],
36-
[2, 2, 1, 1, 3, 2],
37-
[2, 2, 1, 2, 3, 1],
38-
[2, 1, 3, 2, 1, 2],
39-
[2, 2, 3, 1, 1, 2],
40-
[3, 1, 2, 1, 3, 1],
41-
[3, 1, 1, 2, 2, 2],
42-
[3, 2, 1, 1, 2, 2],
43-
[3, 2, 1, 2, 2, 1],
44-
[3, 1, 2, 2, 1, 2],
45-
[3, 2, 2, 1, 1, 2],
46-
[3, 2, 2, 2, 1, 1],
47-
[2, 1, 2, 1, 2, 3],
48-
[2, 1, 2, 3, 2, 1],
49-
[2, 3, 2, 1, 2, 1],
20+
[2, 1, 2, 2, 2, 2], // 0
21+
[2, 2, 2, 1, 2, 2], // 1
22+
[2, 2, 2, 2, 2, 1], // 2
23+
[1, 2, 1, 2, 2, 3], // 3
24+
[1, 2, 1, 3, 2, 2], // 4
25+
[1, 3, 1, 2, 2, 2], // 5
26+
[1, 2, 2, 2, 1, 3], // 6
27+
[1, 2, 2, 3, 1, 2], // 7
28+
[1, 3, 2, 2, 1, 2], // 8
29+
[2, 2, 1, 2, 1, 3], // 9
30+
[2, 2, 1, 3, 1, 2], // 10
31+
[2, 3, 1, 2, 1, 2], // 11
32+
[1, 1, 2, 2, 3, 2], // 12
33+
[1, 2, 2, 1, 3, 2], // 13
34+
[1, 2, 2, 2, 3, 1], // 14
35+
[1, 1, 3, 2, 2, 2], // 15
36+
[1, 2, 3, 1, 2, 2], // 16
37+
[1, 2, 3, 2, 2, 1], // 17
38+
[2, 2, 3, 2, 1, 1], // 18
39+
[2, 2, 1, 1, 3, 2], // 19
40+
[2, 2, 1, 2, 3, 1], // 20
41+
[2, 1, 3, 2, 1, 2], // 21
42+
[2, 2, 3, 1, 1, 2], // 22
43+
[3, 1, 2, 1, 3, 1], // 23
44+
[3, 1, 1, 2, 2, 2], // 24
45+
[3, 2, 1, 1, 2, 2], // 25
46+
[3, 2, 1, 2, 2, 1], // 26
47+
[3, 1, 2, 2, 1, 2], // 27
48+
[3, 2, 2, 1, 1, 2], // 28
49+
[3, 2, 2, 2, 1, 1], // 29
50+
[2, 1, 2, 1, 2, 3], // 30
51+
[2, 1, 2, 3, 2, 1], // 31
52+
[2, 3, 2, 1, 2, 1], // 32
53+
[1, 1, 1, 3, 2, 3], // 33
54+
[1, 3, 1, 1, 2, 3], // 34
55+
[1, 3, 1, 3, 2, 1], // 35
56+
[1, 1, 2, 3, 1, 3], // 36
57+
[1, 3, 2, 1, 1, 3], // 37
58+
[1, 3, 2, 3, 1, 1], // 38
59+
[2, 1, 1, 3, 1, 3], // 39
60+
[2, 3, 1, 1, 1, 3], // 40
61+
[2, 3, 1, 3, 1, 1], // 41
62+
[1, 1, 2, 1, 3, 3], // 42
63+
[1, 1, 2, 3, 3, 1], // 43
64+
[1, 3, 2, 1, 3, 1], // 44
65+
[1, 1, 3, 1, 2, 3], // 45
66+
[1, 1, 3, 3, 2, 1], // 46
67+
[1, 3, 3, 1, 2, 1], // 47
68+
[3, 1, 3, 1, 2, 1], // 48
69+
[2, 1, 1, 3, 3, 1], // 49
70+
[2, 3, 1, 1, 3, 1], // 50
71+
[2, 1, 3, 1, 1, 3], // 51
72+
[2, 1, 3, 3, 1, 1], // 52
73+
[2, 1, 3, 1, 3, 1], // 53
74+
[3, 1, 1, 1, 2, 3], // 54
75+
[3, 1, 1, 3, 2, 1], // 55
76+
[3, 3, 1, 1, 2, 1], // 56
77+
[3, 1, 2, 1, 1, 3], // 57
78+
[3, 1, 2, 3, 1, 1], // 58
79+
[3, 3, 2, 1, 1, 1], // 59
80+
[2, 1, 2, 1, 3, 2], // 60
81+
[2, 1, 2, 2, 3, 1], // 61
82+
[2, 1, 2, 3, 1, 2], // 62
83+
[1, 4, 2, 1, 1, 2], // 63
84+
[1, 1, 4, 2, 1, 2], // 64
85+
[1, 2, 4, 1, 1, 2], // 65
86+
[1, 1, 1, 2, 4, 2], // 66
87+
[1, 2, 1, 1, 4, 2], // 67
88+
[1, 2, 1, 2, 4, 1], // 68
89+
[4, 2, 1, 1, 1, 2], // 69
90+
[4, 2, 1, 2, 1, 1], // 70
91+
[4, 1, 2, 1, 1, 2], // 71
92+
[2, 4, 1, 2, 1, 1], // 72
93+
[2, 2, 1, 4, 1, 1], // 73
94+
[4, 1, 1, 2, 1, 2], // 74
95+
[1, 1, 1, 2, 2, 4], // 75
96+
[1, 1, 1, 4, 2, 2], // 76
97+
[1, 2, 1, 1, 2, 4], // 77
98+
[1, 2, 1, 4, 2, 1], // 78
99+
[1, 4, 1, 1, 2, 2], // 79
100+
[1, 4, 1, 2, 2, 1], // 80
101+
[1, 1, 2, 2, 1, 4], // 81
102+
[1, 1, 2, 4, 1, 2], // 82
103+
[1, 2, 2, 1, 1, 4], // 83
104+
[1, 2, 2, 4, 1, 1], // 84
105+
[1, 4, 2, 1, 1, 2], // 85
106+
[1, 4, 2, 2, 1, 1], // 86
107+
[2, 4, 1, 1, 1, 2], // 87
108+
[2, 2, 1, 1, 1, 4], // 88
109+
[4, 1, 1, 2, 2, 1], // 89
110+
[4, 2, 2, 1, 1, 1], // 90
111+
[2, 1, 2, 1, 4, 1], // 91
112+
[2, 1, 4, 1, 2, 1], // 92
113+
[4, 1, 2, 1, 2, 1], // 93
114+
[1, 1, 1, 1, 4, 3], // 94
115+
[1, 1, 1, 3, 4, 1], // 95
116+
[1, 3, 1, 1, 4, 1], // 96 (CODE_A)
117+
[1, 1, 4, 1, 1, 3], // 97 (CODE_B)
118+
[1, 1, 4, 3, 1, 1], // 98 (CODE_C)
119+
[4, 1, 1, 1, 1, 3], // 99 (CODE_C)
120+
[4, 1, 1, 3, 1, 1], // 100 (CODE_B)
121+
[1, 1, 3, 1, 4, 1], // 101 (CODE_A)
122+
[1, 1, 4, 1, 3, 1], // 102 (FNC1)
123+
[2, 1, 1, 4, 1, 2], // 103 (START_A)
124+
[2, 1, 1, 2, 1, 4], // 104 (START_B)
125+
[2, 1, 1, 2, 3, 2], // 105 (START_C)
50126
];
51-
// Only first 33 patterns shown — full table exists in code128.ts
52-
// For Codablock F we only need values 0-106
53127

54128
const STOP_PATTERN = [2, 3, 3, 1, 1, 1, 2];
55129

@@ -59,6 +133,115 @@ export interface CodablockFResult {
59133
cols: number;
60134
}
61135

136+
/** Count consecutive digit characters from a given position */
137+
function countDigitsFrom(text: string, pos: number): number {
138+
let count = 0;
139+
while (pos + count < text.length) {
140+
const c = text.charCodeAt(pos + count);
141+
if (c < 48 || c > 57) break;
142+
count++;
143+
}
144+
return count;
145+
}
146+
147+
/**
148+
* Determine the optimal Code 128 charset for a character.
149+
* Returns "A" for control chars (0-31), "B" for printable ASCII (32-126), or null if unsupported.
150+
*/
151+
function charsetFor(charCode: number): "A" | "B" | null {
152+
if (charCode >= 0 && charCode < 32) return "A";
153+
if (charCode >= 32 && charCode <= 126) return "B";
154+
return null;
155+
}
156+
157+
/**
158+
* Encode text into Code 128 codeword values with automatic charset switching.
159+
* Supports Code A (control chars), Code B (printable ASCII), and Code C (digit pairs).
160+
*/
161+
function encodeValues(text: string): number[] {
162+
const values: number[] = [];
163+
let pos = 0;
164+
165+
// Determine initial charset
166+
const initialDigits = countDigitsFrom(text, 0);
167+
let currentCharset: "A" | "B" | "C";
168+
if (initialDigits >= 4 || (initialDigits >= 2 && initialDigits === text.length)) {
169+
currentCharset = "C";
170+
} else if (text.length > 0 && text.charCodeAt(0) < 32) {
171+
currentCharset = "A";
172+
} else {
173+
currentCharset = "B";
174+
}
175+
176+
while (pos < text.length) {
177+
if (currentCharset === "C") {
178+
const digits = countDigitsFrom(text, pos);
179+
if (digits >= 2) {
180+
// Encode digit pairs
181+
const pairCount = Math.floor(digits / 2);
182+
for (let i = 0; i < pairCount; i++) {
183+
const d1 = text.charCodeAt(pos) - 48;
184+
const d2 = text.charCodeAt(pos + 1) - 48;
185+
values.push(d1 * 10 + d2);
186+
pos += 2;
187+
}
188+
} else {
189+
// Switch out of Code C
190+
const charCode = pos < text.length ? text.charCodeAt(pos) : -1;
191+
const cs = charCode >= 0 ? charsetFor(charCode) : "B";
192+
if (cs === "A") {
193+
values.push(CODE_A);
194+
currentCharset = "A";
195+
} else {
196+
values.push(CODE_B);
197+
currentCharset = "B";
198+
}
199+
}
200+
} else {
201+
// Code A or Code B
202+
const numRun = countDigitsFrom(text, pos);
203+
if (numRun >= 4 || (numRun >= 2 && pos + numRun >= text.length)) {
204+
values.push(CODE_C);
205+
currentCharset = "C";
206+
continue;
207+
}
208+
209+
const charCode = text.charCodeAt(pos);
210+
const needed = charsetFor(charCode);
211+
if (needed === null) {
212+
throw new InvalidInputError(
213+
`Codablock F: unsupported character "${text[pos]}" (code ${charCode})`,
214+
);
215+
}
216+
217+
if (needed !== currentCharset) {
218+
if (needed === "A") {
219+
values.push(CODE_A);
220+
currentCharset = "A";
221+
} else {
222+
values.push(CODE_B);
223+
currentCharset = "B";
224+
}
225+
}
226+
227+
if (currentCharset === "A") {
228+
// Code A: control chars 0-31 → values 64-95, printable 32-95 → values 0-63
229+
if (charCode < 32) {
230+
values.push(charCode + 64);
231+
} else {
232+
values.push(charCode - 32);
233+
}
234+
} else {
235+
// Code B: printable 32-126 → values 0-94
236+
values.push(charCode - 32);
237+
}
238+
pos++;
239+
}
240+
}
241+
242+
return values;
243+
}
244+
62245
/**
63246
* Encode text as Codablock F (stacked Code 128)
64247
*
@@ -70,16 +253,8 @@ export function encodeCodablockF(text: string, options?: { columns?: number }):
70253
throw new InvalidInputError("Codablock F input must not be empty");
71254
}
72255

73-
// Encode all characters as Code 128B values
74-
const values: number[] = [];
75-
for (const ch of text) {
76-
const code = ch.charCodeAt(0);
77-
if (code >= 32 && code <= 126) {
78-
values.push(code - 32); // Code B encoding
79-
} else {
80-
throw new InvalidInputError(`Codablock F: unsupported character "${ch}" (code ${code})`);
81-
}
82-
}
256+
// Encode text into Code 128 codeword values
257+
const values = encodeValues(text);
83258

84259
// Determine columns per row
85260
const cols = options?.columns ?? Math.min(10, Math.max(4, Math.ceil(values.length / 5)));
@@ -129,7 +304,7 @@ export function encodeCodablockF(text: string, options?: { columns?: number }):
129304
const modules: boolean[] = [];
130305

131306
for (const code of codes) {
132-
const pattern = PATTERNS[code % PATTERNS.length]!;
307+
const pattern = PATTERNS[code]!;
133308
let isBar = true;
134309
for (const w of pattern) {
135310
for (let i = 0; i < w; i++) {

test/encoders-codablock-f.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,14 @@ describe("Codablock F", () => {
3535
expect(() => encodeCodablockF("")).toThrow();
3636
});
3737

38-
it("throws on non-printable characters", () => {
39-
expect(() => encodeCodablockF("\x01")).toThrow();
38+
it("throws on non-encodable characters", () => {
39+
expect(() => encodeCodablockF("\x80")).toThrow();
40+
});
41+
42+
it("encodes control characters via Code A", () => {
43+
const result = encodeCodablockF("\x01\x02\x03");
44+
expect(result.matrix.length).toBeGreaterThan(0);
45+
expect(result.rows).toBeGreaterThan(0);
4046
});
4147

4248
it("respects column count", () => {

0 commit comments

Comments
 (0)