Skip to content

Commit 0b4161a

Browse files
feat(#29): add GS1 Composite barcodes (CC-A, CC-B, CC-C)
- encodeGS1Composite() — supplemental 2D component for 1D barcodes - CC-A: MicroPDF417 (2 col), CC-B: MicroPDF417 (3 col), CC-C: PDF417 - AI data validation via GS1-128 parser - 7 tests added Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a7194f3 commit 0b4161a

3 files changed

Lines changed: 127 additions & 0 deletions

File tree

src/encoders/gs1-composite.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* GS1 Composite Component encoder
3+
* Adds supplemental 2D component to a primary 1D barcode
4+
*
5+
* CC-A: MicroPDF417-based, small capacity
6+
* CC-B: MicroPDF417-based, medium capacity
7+
* CC-C: PDF417-based, large capacity
8+
*
9+
* The composite component encodes additional GS1 AI data
10+
* that doesn't fit in the primary linear barcode.
11+
*/
12+
13+
import { InvalidInputError } from "../errors";
14+
import { encodeMicroPDF417 } from "./micropdf417";
15+
import { encodePDF417 } from "./pdf417/index";
16+
import { parseAIString } from "./gs1-128";
17+
18+
export type CompositeType = "CC-A" | "CC-B" | "CC-C";
19+
20+
export interface GS1CompositeResult {
21+
/** The 2D composite component matrix */
22+
composite: boolean[][];
23+
/** Composite type used */
24+
type: CompositeType;
25+
/** Number of rows in composite */
26+
rows: number;
27+
/** Width in modules */
28+
cols: number;
29+
}
30+
31+
/**
32+
* Encode GS1 Composite Component
33+
* Input: AI string in parenthesized format
34+
*
35+
* @param data - GS1 AI data for composite (e.g., "(17)260101(10)BATCH01")
36+
* @param type - Composite type: CC-A (small), CC-B (medium), CC-C (large)
37+
* @returns Composite component matrix
38+
*/
39+
export function encodeGS1Composite(data: string, type: CompositeType = "CC-A"): GS1CompositeResult {
40+
if (data.length === 0) {
41+
throw new InvalidInputError("GS1 Composite: data must not be empty");
42+
}
43+
44+
// Validate AI format if parenthesized
45+
if (data.includes("(")) {
46+
parseAIString(data); // throws on invalid
47+
}
48+
49+
// Encode based on composite type
50+
switch (type) {
51+
case "CC-A":
52+
case "CC-B": {
53+
// MicroPDF417-based
54+
const columns = type === "CC-A" ? 2 : 3;
55+
const result = encodeMicroPDF417(data, { columns });
56+
return {
57+
composite: result.matrix,
58+
type,
59+
rows: result.rows,
60+
cols: result.cols,
61+
};
62+
}
63+
case "CC-C": {
64+
// PDF417-based
65+
const result = encodePDF417(data, { ecLevel: 2, columns: 4 });
66+
return {
67+
composite: result.matrix,
68+
type,
69+
rows: result.rows,
70+
cols: result.cols,
71+
};
72+
}
73+
default:
74+
throw new InvalidInputError(`Invalid composite type: ${type}`);
75+
}
76+
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export { encodeGS1128 } from "./encoders/gs1-128";
7373
export { encodeIdentcode, encodeLeitcode } from "./encoders/deutsche-post";
7474
export { encodePOSTNET, encodePLANET } from "./encoders/postnet";
7575
export { encodePlessey } from "./encoders/plessey";
76+
export { encodeGS1Composite } from "./encoders/gs1-composite";
77+
export type { CompositeType, GS1CompositeResult } from "./encoders/gs1-composite";
7678
export {
7779
encodeGS1DataBarOmni,
7880
encodeGS1DataBarLimited,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, expect, it } from "vitest";
2+
import { encodeGS1Composite } from "../src/encoders/gs1-composite";
3+
4+
describe("GS1 Composite", () => {
5+
it("encodes CC-A (MicroPDF417, 2 columns)", () => {
6+
const result = encodeGS1Composite("(17)260101(10)BATCH01", "CC-A");
7+
expect(result.type).toBe("CC-A");
8+
expect(result.rows).toBeGreaterThan(0);
9+
expect(result.composite.length).toBe(result.rows);
10+
});
11+
12+
it("encodes CC-B (MicroPDF417, 3 columns)", () => {
13+
const result = encodeGS1Composite("(17)260101(10)BATCH01", "CC-B");
14+
expect(result.type).toBe("CC-B");
15+
expect(result.rows).toBeGreaterThan(0);
16+
});
17+
18+
it("encodes CC-C (PDF417)", () => {
19+
const result = encodeGS1Composite("(17)260101(10)BATCH01(21)SERIAL001", "CC-C");
20+
expect(result.type).toBe("CC-C");
21+
expect(result.rows).toBeGreaterThan(0);
22+
});
23+
24+
it("default type is CC-A", () => {
25+
const result = encodeGS1Composite("(10)LOT123");
26+
expect(result.type).toBe("CC-A");
27+
});
28+
29+
it("produces boolean matrix", () => {
30+
const result = encodeGS1Composite("(10)TEST");
31+
for (const row of result.composite) {
32+
for (const cell of row) {
33+
expect(typeof cell).toBe("boolean");
34+
}
35+
}
36+
});
37+
38+
it("throws on empty", () => {
39+
expect(() => encodeGS1Composite("")).toThrow();
40+
});
41+
42+
it("different data produces different output", () => {
43+
const a = encodeGS1Composite("(10)AAA");
44+
const b = encodeGS1Composite("(10)BBB");
45+
const aStr = a.composite.map((r) => r.map((c) => (c ? "1" : "0")).join("")).join("");
46+
const bStr = b.composite.map((r) => r.map((c) => (c ? "1" : "0")).join("")).join("");
47+
expect(aStr).not.toBe(bStr);
48+
});
49+
});

0 commit comments

Comments
 (0)