Skip to content

Commit 07284cf

Browse files
authored
Merge pull request #83 from u8array/feat/gfc-support
Feat/gfc support
2 parents 81d0c5e + 81c5e32 commit 07284cf

4 files changed

Lines changed: 325 additions & 12 deletions

File tree

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@dnd-kit/sortable": "^10.0.0",
1818
"@heroicons/react": "^2.2.0",
1919
"bwip-js": "^4.10.1",
20+
"fflate": "^0.8.3",
2021
"konva": "^10.3.0",
2122
"react": "^19.2.6",
2223
"react-dom": "^19.2.6",

‎pnpm-lock.yaml‎

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/lib/zplParser.test.ts‎

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,31 @@
11
import { describe, it, expect, beforeAll } from 'vitest';
2+
import { zlibSync } from 'fflate';
23
import { parseZPL } from './zplParser';
34
import { props } from '../test/helpers';
45

6+
/** CRC-16/XMODEM — same variant used by the parser to validate
7+
* :B64:/:Z64: wrappers (poly 0x1021, init 0x0000). Duplicated here so
8+
* tests can build valid CRC values without exporting the parser's
9+
* internal helper. */
10+
function testCrc16(s: string): string {
11+
let crc = 0;
12+
for (const ch of s) {
13+
crc ^= ch.charCodeAt(0) << 8;
14+
for (let j = 0; j < 8; j++) {
15+
crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff;
16+
}
17+
}
18+
return crc.toString(16).padStart(4, '0').toUpperCase();
19+
}
20+
21+
function makeZ64Field(bytes: Uint8Array): string {
22+
const deflated = zlibSync(bytes);
23+
let bin = '';
24+
for (const b of deflated) bin += String.fromCharCode(b);
25+
const b64 = btoa(bin);
26+
return `:Z64:${b64}:${testCrc16(b64)}`;
27+
}
28+
529
// ── label config ──────────────────────────────────────────────────────────────
630

731
describe('parseZPL — label config', () => {
@@ -543,6 +567,89 @@ describe('parseZPL — ^GFA graphic field', () => {
543567
expect(props(objects[0])._gfaCache).toContain('^GFA,');
544568
});
545569

570+
it('imports a :B64:-wrapped ^GFA payload as an image (CRC valid)', () => {
571+
// 8 bytes = [0,0,0,0xFF,0xFF,0,0,0] → base64 "AAAA//8AAAA="
572+
// CRC-16/CCITT-FALSE over "AAAA//8AAAA=" = 0xDFF8
573+
const { objects, importReport } = parseZPL(
574+
'^XA^FO0,0^GFA,8,8,1,:B64:AAAA//8AAAA=:DFF8^FS^XZ',
575+
8,
576+
);
577+
expect(objects).toHaveLength(1);
578+
expect(objects[0]?.type).toBe('image');
579+
expect(props(objects[0]).widthDots).toBe(8);
580+
expect(importReport.partial).not.toContain('^GF');
581+
});
582+
583+
it('still renders a :B64: payload with mismatched CRC but flags as partial', () => {
584+
const { objects, importReport } = parseZPL(
585+
'^XA^FO0,0^GFA,8,8,1,:B64:AAAA//8AAAA=:0000^FS^XZ',
586+
8,
587+
);
588+
expect(objects).toHaveLength(1);
589+
expect(importReport.partial).toContain('^GF');
590+
});
591+
592+
it('accepts :B64: wrapper on ^GFB and ^GFC (no raw-binary path needed)', () => {
593+
for (const fmt of ['B', 'C'] as const) {
594+
const { objects } = parseZPL(
595+
`^XA^FO0,0^GF${fmt},8,8,1,:B64:AAAA//8AAAA=:DFF8^FS^XZ`,
596+
8,
597+
);
598+
expect(objects).toHaveLength(1);
599+
expect(objects[0]?.type).toBe('image');
600+
}
601+
});
602+
603+
it('tolerates embedded whitespace inside a :B64: base64 payload', () => {
604+
// ZPL generators often line-break long base64 blocks every N chars.
605+
// Labelary accepts this; we should too.
606+
const zpl =
607+
'^XA^FO0,0^GFA,8,8,1,:B64:AAAA\n//8AAAA=:DFF8^FS^XZ';
608+
const { objects, importReport } = parseZPL(zpl, 8);
609+
expect(objects).toHaveLength(1);
610+
expect(importReport.partial).not.toContain('^GF');
611+
});
612+
613+
it('tolerates trailing whitespace on wrapped GF payloads', () => {
614+
// Real-world ZPL is often line-broken between commands; the tokenizer
615+
// preserves the trailing newline on the field body, so the regex needs
616+
// to accommodate that.
617+
const zplWithNewline =
618+
'^XA\n^FO0,0\n^GFA,8,8,1,:B64:AAAA//8AAAA=:DFF8\n^FS\n^XZ';
619+
const { objects, importReport } = parseZPL(zplWithNewline, 8);
620+
expect(objects).toHaveLength(1);
621+
expect(objects[0]?.type).toBe('image');
622+
expect(importReport.browserLimit).toHaveLength(0);
623+
});
624+
625+
it('imports a :Z64:-wrapped ^GFC payload by inflating zlib data', () => {
626+
// 8 bytes = [0,0,0,0xFF,0xFF,0,0,0] → zlib-compressed → base64 → CRC.
627+
const bytes = new Uint8Array([0, 0, 0, 0xff, 0xff, 0, 0, 0]);
628+
const field = makeZ64Field(bytes);
629+
const { objects, importReport } = parseZPL(
630+
`^XA^FO0,0^GFC,8,8,1,${field}^FS^XZ`,
631+
8,
632+
);
633+
expect(objects).toHaveLength(1);
634+
expect(objects[0]?.type).toBe('image');
635+
expect(props(objects[0]).widthDots).toBe(8);
636+
expect(importReport.partial).not.toContain('^GF');
637+
});
638+
639+
it('records :Z64: with corrupt deflate stream as browserLimit', () => {
640+
// Valid base64 but garbage bytes that fflate will reject as a deflate
641+
// stream. CRC must match so we know the failure is in inflate, not the
642+
// wrapper-shape detection.
643+
const b64 = btoa('not a valid zlib stream');
644+
const field = `:Z64:${b64}:${testCrc16(b64)}`;
645+
const { objects, importReport } = parseZPL(
646+
`^XA^FO0,0^GFC,8,8,1,${field}^FS^XZ`,
647+
8,
648+
);
649+
expect(objects).toHaveLength(0);
650+
expect(importReport.browserLimit.some((s) => s.startsWith('^GF'))).toBe(true);
651+
});
652+
546653
it('creates an image object from compressed ^GFA data', () => {
547654
// G=1 repeat → "GF" = repeat 'F' once, basically just 'F'
548655
// bytesPerRow=1, so we need 2 nibbles per row

0 commit comments

Comments
 (0)