Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@dnd-kit/sortable": "^10.0.0",
"@heroicons/react": "^2.2.0",
"bwip-js": "^4.10.1",
"fflate": "^0.8.3",
"konva": "^10.3.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
Expand Down
27 changes: 27 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

107 changes: 107 additions & 0 deletions src/lib/zplParser.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
import { describe, it, expect, beforeAll } from 'vitest';
import { zlibSync } from 'fflate';
import { parseZPL } from './zplParser';
import { props } from '../test/helpers';

/** CRC-16/XMODEM — same variant used by the parser to validate
* :B64:/:Z64: wrappers (poly 0x1021, init 0x0000). Duplicated here so
* tests can build valid CRC values without exporting the parser's
* internal helper. */
function testCrc16(s: string): string {
let crc = 0;
Comment thread
u8array marked this conversation as resolved.
for (const ch of s) {
crc ^= ch.charCodeAt(0) << 8;
for (let j = 0; j < 8; j++) {
crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff;
}
}
return crc.toString(16).padStart(4, '0').toUpperCase();
}

function makeZ64Field(bytes: Uint8Array): string {
const deflated = zlibSync(bytes);
let bin = '';
for (const b of deflated) bin += String.fromCharCode(b);
const b64 = btoa(bin);
return `:Z64:${b64}:${testCrc16(b64)}`;
}

// ── label config ──────────────────────────────────────────────────────────────

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

it('imports a :B64:-wrapped ^GFA payload as an image (CRC valid)', () => {
// 8 bytes = [0,0,0,0xFF,0xFF,0,0,0] → base64 "AAAA//8AAAA="
// CRC-16/CCITT-FALSE over "AAAA//8AAAA=" = 0xDFF8
const { objects, importReport } = parseZPL(
'^XA^FO0,0^GFA,8,8,1,:B64:AAAA//8AAAA=:DFF8^FS^XZ',
8,
);
expect(objects).toHaveLength(1);
expect(objects[0]?.type).toBe('image');
expect(props(objects[0]).widthDots).toBe(8);
expect(importReport.partial).not.toContain('^GF');
});

it('still renders a :B64: payload with mismatched CRC but flags as partial', () => {
const { objects, importReport } = parseZPL(
'^XA^FO0,0^GFA,8,8,1,:B64:AAAA//8AAAA=:0000^FS^XZ',
8,
);
expect(objects).toHaveLength(1);
expect(importReport.partial).toContain('^GF');
});

it('accepts :B64: wrapper on ^GFB and ^GFC (no raw-binary path needed)', () => {
for (const fmt of ['B', 'C'] as const) {
const { objects } = parseZPL(
`^XA^FO0,0^GF${fmt},8,8,1,:B64:AAAA//8AAAA=:DFF8^FS^XZ`,
8,
);
expect(objects).toHaveLength(1);
expect(objects[0]?.type).toBe('image');
}
});

it('tolerates embedded whitespace inside a :B64: base64 payload', () => {
// ZPL generators often line-break long base64 blocks every N chars.
// Labelary accepts this; we should too.
const zpl =
'^XA^FO0,0^GFA,8,8,1,:B64:AAAA\n//8AAAA=:DFF8^FS^XZ';
const { objects, importReport } = parseZPL(zpl, 8);
expect(objects).toHaveLength(1);
expect(importReport.partial).not.toContain('^GF');
});

it('tolerates trailing whitespace on wrapped GF payloads', () => {
// Real-world ZPL is often line-broken between commands; the tokenizer
// preserves the trailing newline on the field body, so the regex needs
// to accommodate that.
const zplWithNewline =
'^XA\n^FO0,0\n^GFA,8,8,1,:B64:AAAA//8AAAA=:DFF8\n^FS\n^XZ';
const { objects, importReport } = parseZPL(zplWithNewline, 8);
expect(objects).toHaveLength(1);
expect(objects[0]?.type).toBe('image');
expect(importReport.browserLimit).toHaveLength(0);
});

it('imports a :Z64:-wrapped ^GFC payload by inflating zlib data', () => {
// 8 bytes = [0,0,0,0xFF,0xFF,0,0,0] → zlib-compressed → base64 → CRC.
const bytes = new Uint8Array([0, 0, 0, 0xff, 0xff, 0, 0, 0]);
const field = makeZ64Field(bytes);
const { objects, importReport } = parseZPL(
`^XA^FO0,0^GFC,8,8,1,${field}^FS^XZ`,
8,
);
expect(objects).toHaveLength(1);
expect(objects[0]?.type).toBe('image');
expect(props(objects[0]).widthDots).toBe(8);
expect(importReport.partial).not.toContain('^GF');
});

it('records :Z64: with corrupt deflate stream as browserLimit', () => {
// Valid base64 but garbage bytes that fflate will reject as a deflate
// stream. CRC must match so we know the failure is in inflate, not the
// wrapper-shape detection.
const b64 = btoa('not a valid zlib stream');
const field = `:Z64:${b64}:${testCrc16(b64)}`;
const { objects, importReport } = parseZPL(
`^XA^FO0,0^GFC,8,8,1,${field}^FS^XZ`,
8,
);
expect(objects).toHaveLength(0);
expect(importReport.browserLimit.some((s) => s.startsWith('^GF'))).toBe(true);
});

it('creates an image object from compressed ^GFA data', () => {
// G=1 repeat → "GF" = repeat 'F' once, basically just 'F'
// bytesPerRow=1, so we need 2 nibbles per row
Expand Down
Loading