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.
| Offset | Type | Meaning |
|---|---|---|
| 0x00 | 7 byte | magic ASCII "HSING12" |
| 0x07 | uint32 | element count = nData + 1 |
| 0x0B | uint32 | stitch count nStitches (0x02 records) |
| 0x0F | uint32 | stitchStartOffset (155 / 0x9B) |
| 0x13 | uint32 | headerStartOffset (134 / 0x86) |
| 0x17 | uint32 | 150 (0x96) — constant |
| 0x1C | int16 | posX (max X) |
| 0x1E | int16 | negX (min X) |
| 0x22 | int16 | posY (max Y) |
| 0x24 | int16 | negY (min Y) |
| 0x68 | uint32 | stitch count (again) |
| 0x6C/0x6E | int16×2 | firstX / firstY (delta of the first record) |
| 0x74 | uint32 | 140 (0x8C) — constant |
| 0x7B/0x7D | int16×2 | lastX / lastY (absolute final position) |
| 0x7F | uint16 | totalRecords − 2 |
| 0x83 | uint16 | UNKNOWN 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
| ctrl | meaning |
|---|---|
0x01 | MOVE / jump-in |
0x02 | STITCH |
0x04 | COLOR CHANGE |
0x10 | CHECKSUM (see below) |
0x80 | END |
0x41 | seen 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)
- First VIP record → ZHS MOVE
[0x01]with the same (dx,dy). - Immediately after, a tie-in duplicate stitch
[0x02] (0,0). - Every subsequent VIP stitch → ZHS STITCH
[0x02]with (dx,dy). - 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
- TRIM — no sample contains trims.
0x88remains a guess; the writer drops the TRIMs with aTRIM_DROPPEDwarning (JEF-style); loose threads get snipped by hand. - Per-block monotonic counter +2, terminator +17 (the old
0x83), factory bytes 0x20/0x26 — editor metadata, written as 0/constants. 0x41— still unmapped (absent from every sample).