ZHS — Husqvarna Viking (newer) / Zeng Hsing
Reverse-engineered completamente da output reali di
Artist Toolkit. Il mapping single-color è verificato:
generare 028-B.vip con questa spec produce un file
byte-identical a fixtures/028-B.zhs tranne una
singola word di metadata a 0x83.
Non esiste alcuno ZHS writer open-source al mondo. pyembroidery / Ink-Stitch / libembroidery sanno leggere ZHS ma non scriverlo. Questa spec è il cuore del progetto — guardala con test di round-trip.
Header fisso (offset 0x00–0x85, single-color)
Tutti gli interi multi-byte sono little-endian salvo nota.
| Offset | Tipo | Significato |
|---|---|---|
| 0x00 | 7 byte | magic ASCII "HSING12" |
| 0x07 | uint32 | element count = nData + 1 |
| 0x0B | uint32 | stitch count nStitches (record 0x02) |
| 0x0F | uint32 | stitchStartOffset (155 / 0x9B) |
| 0x13 | uint32 | headerStartOffset (134 / 0x86) |
| 0x17 | uint32 | 150 (0x96) — costante |
| 0x1C | int16 | posX (max X) |
| 0x1E | int16 | negX (min X) |
| 0x22 | int16 | posY (max Y) |
| 0x24 | int16 | negY (min Y) |
| 0x68 | uint32 | stitch count (di nuovo) |
| 0x6C/0x6E | int16×2 | firstX / firstY (delta del primo record) |
| 0x74 | uint32 | 140 (0x8C) — costante |
| 0x7B/0x7D | int16×2 | lastX / lastY (posizione finale assoluta) |
| 0x7F | uint16 | totalRecords − 2 |
| 0x83 | uint16 | UNKNOWN editor metadata (es: 486 vs 2) — vedi nota |
Nota su 0x83
Tra i due campioni di riferimento questo campo vale 486
(001s-A) e 2 (028-B). Non correla con nessuna
metrica del disegno (numero di punti, extent, somma byte, path, file size
— tutte testate). Il reader ZHS non lo legge mai.
Conclusione: è un valore interno dell'editor Artist Toolkit; lo si può
mettere a 0. La cucitura fisica in macchina resta il test
di accettazione finale.
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
... padding zero fino a stitchStartOffset (0x9B)
La stringa è separata da &$/&# (chart,
description, catalog). Per le lettere monogramma il blocco è costante:
01 34 8D 1A 08 00 "&$&%" 1 colore, RGB #348D1A, stringa di 8 byte &$&#&#&%, poi zero fino a 0x9B.
Stitch stream (offset 0x9B)
Lista piatta di record da 3 byte: [ctrl, b1, b2].
Control byte
| ctrl | meaning |
|---|---|
0x01 | MOVE / jump-in |
0x02 | STITCH |
0x04 | COLOR CHANGE |
0x10 | CHECKSUM (vedi sotto) |
0x80 | END |
0x41 | visto dal reader come "unmapped" — significato ignoto |
0x88? | TRIM — ignoto, nessun sample. GAP aperto. |
Packing delle coordinate (b1, b2)
dx e dy sono delta signed8 con i bit
interleaved tra b1 e 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
Dopo extraction, i valori sono signed8 con un range-extension
tweak su decode: if v>=63: v+=1 e
if v<=-63: v-=1. Su encode si inverte:
if v>=64: store v-1 e if v<=-64: store v+1.
±63 hole: un delta di esattamente +63 o −63 non è rappresentabile (63 memorizzato decodifica come 64). Range decodificabile −129..128 meno ±63. Nessun fixture contiene tali delta (max |delta| = 32). Il writer TS calcola i delta contro la posizione decoded, così un ±63 non rappresentabile sfuma a uno scarto di 0,1 mm su quel singolo punto (con warning strutturata) invece di shiftare tutto il disegno.
Segno Y: il reader emette stitch(x, -y), o
quindi lo stored y coincide con il VIP y. Convertendo VIP→ZHS
si copiano i delta VIP direttamente — nessun flip di segno necessario.
Record di checksum
Dopo ogni 84 record dati, inserire un record di checksum:
[0x10, sum & 0xFF, (sum >> 8) & 0xFF] // somma 16-bit LE di tutti i byte
// (ctrl+b1+b2) del blocco di 84 record Fine file
Dopo l'ultimo record dati: emettere END [0x80, 0, 0],
poi un record di checksum finale sul blocco che contiene
l'END.
VIP → ZHS record mapping (single-color, verificato)
- Primo record VIP → ZHS MOVE
[0x01]con lo stesso (dx,dy). - Subito dopo, una tie-in duplicate stitch
[0x02] (0,0). - Ogni stitch VIP successivo → ZHS STITCH
[0x02]con (dx,dy). - Drop del record END VIP; emettere ZHS END + checksum finale.
Vedi reference/zhs_vip_reference.py per l'implementazione
eseguibile e testata.
Layout multicolore (campioni di fabbrica)
Decodificato da un campione reale di fabbrica a 6 colori / 10 blocchi /
9302 punti con gemello .pes. L'header “single-color” di §1
è in realtà il caso speciale a 1 blocco di una tabella per-blocco
di righe da 20 byte. Vedi docs/ZHS_FORMAT.md §5 per
i dettagli per-blocco (offsets +2/+9/+10/+14/+16/+18, terminator row,
palette block con un'entry "&$chart&#desc&#cat&%"
per blocco — non per colore unico), il record COLOR_CHANGE (0x04) il cui
payload è palette index del blocco successivo (dy=0), e le
aperture dei blocchi (MOVE run + tie-in stitch).
Tuttora ignoto
- TRIM — nessun campione contiene trim.
0x88resta una guess; il writer droppa i TRIM conTRIM_DROPPEDwarning (stile JEF); i fili volanti si spuntano a mano. - Counter monotonico per-blocco +2, terminator +17 (il vecchio
0x83), byte di fabbrica 0x20/0x26 — editor metadata, scritti come 0/costanti. 0x41— ancora non mappato (assente da tutti i sample).