ZHS — Husqvarna Viking (newer) / Zeng Hsing

Reverse-engineered entirely from real Artist Toolkit output. The single-color mapping is verified: converting 028-B.vip with this spec produces a file byte-identical to fixtures/028-B.zhs except for a single metadata word at 0x83.

No open-source ZHS writer exists anywhere in the world. pyembroidery / Ink-Stitch / libembroidery can read ZHS but cannot write it. This spec is the heart of the project — guard it with round-trip tests.

Fixed header (offsets 0x00–0x85, single-color)

All multi-byte integers are little-endian unless noted.

OffsetTypeMeaning
0x007 bytemagic ASCII "HSING12"
0x07uint32element count = nData + 1
0x0Buint32stitch count nStitches (0x02 records)
0x0Fuint32stitchStartOffset (155 / 0x9B)
0x13uint32headerStartOffset (134 / 0x86)
0x17uint32150 (0x96) — constant
0x1Cint16posX (max X)
0x1Eint16negX (min X)
0x22int16posY (max Y)
0x24int16negY (min Y)
0x68uint32stitch count (again)
0x6C/0x6Eint16×2firstX / firstY (delta of the first record)
0x74uint32140 (0x8C) — constant
0x7B/0x7Dint16×2lastX / lastY (absolute final position)
0x7Fuint16totalRecords − 2
0x83uint16UNKNOWN editor metadata (e.g. 486 vs 2) — see note

Note on 0x83

Across the two reference samples this field is 486 (001s-A) and 2 (028-B). It correlates with no metric of the design (stitch count, extent, byte sum, path, file size — all tested). The ZHS reader never reads it. Conclusion: it is an internal Artist Toolkit editor value; it can be set to 0. The physical stitch-out on the machine remains the final acceptance test.

Palette block (offset 0x86, single-color)

uint8   colorCount
colorCount × 3 byte   RGB, 24-bit BIG-ENDIAN
uint16  stringLength (LE)
bytes   thread-metadata string (UTF-8), length = stringLength
... zero padding up to stitchStartOffset (0x9B)

The string is separated by &$/&# (chart, description, catalog). For the monogram letters the block is constant:

01  34 8D 1A  08 00  "&$&#&#&%"

1 color, RGB #348D1A, an 8-byte string &$&#&#&%, then zeros up to 0x9B.

Stitch stream (offset 0x9B)

Flat list of 3-byte records: [ctrl, b1, b2].

Control byte

ctrlmeaning
0x01MOVE / jump-in
0x02STITCH
0x04COLOR CHANGE
0x10CHECKSUM (see below)
0x80END
0x41seen by the reader as "unmapped" — meaning unknown
0x88?TRIM — unknown, no sample. Open GAP.

Coordinate packing (b1, b2)

dx and dy are signed8 deltas with the bits interleaved between b1 and b2:

x.bit0 <- b1.bit0   x.bit1 <- b2.bit1   x.bit2 <- b1.bit2   x.bit3 <- b2.bit3
x.bit4 <- b1.bit4   x.bit5 <- b2.bit5   x.bit6 <- b1.bit6   x.bit7 <- b2.bit7
y.bit0 <- b2.bit0   y.bit1 <- b1.bit1   y.bit2 <- b2.bit2   y.bit3 <- b1.bit3
y.bit4 <- b2.bit4   y.bit5 <- b1.bit5   y.bit6 <- b2.bit6   y.bit7 <- b1.bit7

After extraction, the values are signed8 with a range-extension tweak on decode: if v>=63: v+=1 and if v<=-63: v-=1. On encode it inverts: if v>=64: store v-1 and if v<=-64: store v+1.

±63 hole: a delta of exactly +63 or −63 is not representable (a stored 63 decodes as 64). Decodable range −129..128 minus ±63. No fixture contains such deltas (max |delta| = 32). The TS writer computes deltas against the decoded position, so an unrepresentable ±63 fades into a 0.1 mm offset on that single stitch (with a structured warning) instead of shifting the whole design.

Y sign: the reader emits stitch(x, -y), so the stored y matches the VIP y. When converting VIP→ZHS the VIP deltas are copied directly — no sign flip needed.

Checksum record

After every 84 data records, insert a checksum record:

[0x10, sum & 0xFF, (sum >> 8) & 0xFF]   // 16-bit LE sum of all bytes
                                          // (ctrl+b1+b2) in the 84-record block

End of file

After the last data record: emit END [0x80, 0, 0], then a final checksum record over the block containing the END.

VIP → ZHS record mapping (single-color, verified)

  1. First VIP record → ZHS MOVE [0x01] with the same (dx,dy).
  2. Immediately after, a tie-in duplicate stitch [0x02] (0,0).
  3. Every subsequent VIP stitch → ZHS STITCH [0x02] with (dx,dy).
  4. Drop the VIP END record; emit ZHS END + final checksum.

See reference/zhs_vip_reference.py for the executable, tested implementation.

Multicolor layout (factory samples)

Decoded from a real 6-color / 10-block / 9302-stitch factory sample with a .pes twin. The “single-color” header of §1 is actually the 1-block special case of a per-block table of 20-byte rows. See docs/ZHS_FORMAT.md §5 for the per-block details (offsets +2/+9/+10/+14/+16/+18, terminator row, palette block with one "&$chart&#desc&#cat&%" entry per block — not per single color), the COLOR_CHANGE record (0x04) whose payload is the palette index of the next block (dy=0), and the block openings (MOVE run + tie-in stitch).

Still unknown

← VIP PES / PEC →