Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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