Compare commits
4 Commits
473da90b01
..
v0.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 800da1872b | |||
| f161a25002 | |||
| 9a367b4d10 | |||
| c9c7245dea |
Generated
+1
-1
@@ -235,7 +235,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustitch"
|
name = "rustitch"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"png",
|
"png",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
|||||||
@@ -23,4 +23,19 @@
|
|||||||
<comment>VP3 Pfaff embroidery file</comment>
|
<comment>VP3 Pfaff embroidery file</comment>
|
||||||
<glob pattern="*.vp3"/>
|
<glob pattern="*.vp3"/>
|
||||||
</mime-type>
|
</mime-type>
|
||||||
|
<mime-type type="application/x-pec">
|
||||||
|
<comment>PEC Brother embroidery file</comment>
|
||||||
|
<glob pattern="*.pec"/>
|
||||||
|
<magic priority="50">
|
||||||
|
<match type="string" offset="0" value="#PEC"/>
|
||||||
|
</magic>
|
||||||
|
</mime-type>
|
||||||
|
<mime-type type="application/x-xxx">
|
||||||
|
<comment>XXX Singer embroidery file</comment>
|
||||||
|
<glob pattern="*.xxx"/>
|
||||||
|
</mime-type>
|
||||||
|
<mime-type type="application/x-sew">
|
||||||
|
<comment>SEW Janome embroidery file</comment>
|
||||||
|
<glob pattern="*.sew"/>
|
||||||
|
</mime-type>
|
||||||
</mime-info>
|
</mime-info>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[Thumbnailer Entry]
|
[Thumbnailer Entry]
|
||||||
TryExec=stitch-peek
|
TryExec=stitch-peek
|
||||||
Exec=stitch-peek -i %i -o %o -s %s
|
Exec=stitch-peek -i %i -o %o -s %s
|
||||||
MimeType=application/x-pes;application/x-dst;application/x-exp;application/x-jef;application/x-vp3
|
MimeType=application/x-pes;application/x-dst;application/x-exp;application/x-jef;application/x-vp3;application/x-pec;application/x-xxx;application/x-sew
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustitch"
|
name = "rustitch"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "PES embroidery file parser and thumbnail renderer"
|
description = "PES embroidery file parser and thumbnail renderer"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
+18
-13
@@ -37,8 +37,10 @@ pub fn parse(data: &[u8]) -> Result<Vec<StitchCommand>, Error> {
|
|||||||
let b2 = stitch_data[i + 2];
|
let b2 = stitch_data[i + 2];
|
||||||
i += 3;
|
i += 3;
|
||||||
|
|
||||||
// End of file: byte 2 bits 0 and 1 both set, and specific pattern
|
// End of file: standard DST EOF pattern (0x00, 0x00, 0xF3)
|
||||||
if b2 & 0x03 == 0x03 {
|
// Bits 0 and 1 of byte 2 are always set in valid DST records,
|
||||||
|
// so we must check the full EOF pattern, not just those bits.
|
||||||
|
if b0 == 0x00 && b1 == 0x00 && b2 == 0xF3 {
|
||||||
commands.push(StitchCommand::End);
|
commands.push(StitchCommand::End);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -46,14 +48,17 @@ pub fn parse(data: &[u8]) -> Result<Vec<StitchCommand>, Error> {
|
|||||||
let dx = decode_dx(b0, b1, b2);
|
let dx = decode_dx(b0, b1, b2);
|
||||||
let dy = decode_dy(b0, b1, b2);
|
let dy = decode_dy(b0, b1, b2);
|
||||||
|
|
||||||
|
// Mask off the always-set bits 0,1 to get control flags
|
||||||
|
let flags = b2 & 0xFC;
|
||||||
|
|
||||||
// Color change: byte 2 bit 7
|
// Color change: byte 2 bit 7
|
||||||
if b2 & 0x80 != 0 {
|
if flags & 0x80 != 0 {
|
||||||
commands.push(StitchCommand::ColorChange);
|
commands.push(StitchCommand::ColorChange);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jump: byte 2 bit 6
|
// Jump: byte 2 bit 6
|
||||||
if b2 & 0x40 != 0 {
|
if flags & 0x40 != 0 {
|
||||||
commands.push(StitchCommand::Jump { dx, dy });
|
commands.push(StitchCommand::Jump { dx, dy });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -166,8 +171,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn decode_end_marker() {
|
fn decode_end_marker() {
|
||||||
// b2 = 0x03 means end
|
// DST EOF = 0x00, 0x00, 0xF3
|
||||||
let data = [0x00, 0x00, 0x03];
|
let data = [0x00, 0x00, 0xF3];
|
||||||
let cmds = parse(&data).unwrap_err();
|
let cmds = parse(&data).unwrap_err();
|
||||||
assert!(matches!(cmds, Error::NoStitchData));
|
assert!(matches!(cmds, Error::NoStitchData));
|
||||||
}
|
}
|
||||||
@@ -175,9 +180,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn decode_simple_stitch() {
|
fn decode_simple_stitch() {
|
||||||
// A normal stitch followed by end
|
// A normal stitch followed by end
|
||||||
// dx=+1: b0 bit 0. dy=+1: b0 bit 7. b2=0x00 (normal stitch)
|
// dx=+1: b0 bit 0. dy=+1: b0 bit 7. b2=0x03 (always-set bits)
|
||||||
// Then end marker
|
// Then end marker 0x00, 0x00, 0xF3
|
||||||
let data = [0x81, 0x00, 0x00, 0x00, 0x00, 0x03];
|
let data = [0x81, 0x00, 0x03, 0x00, 0x00, 0xF3];
|
||||||
let cmds = parse(&data).unwrap();
|
let cmds = parse(&data).unwrap();
|
||||||
assert!(matches!(cmds[0], StitchCommand::Stitch { dx: 1, dy: -1 }));
|
assert!(matches!(cmds[0], StitchCommand::Stitch { dx: 1, dy: -1 }));
|
||||||
assert!(matches!(cmds[1], StitchCommand::End));
|
assert!(matches!(cmds[1], StitchCommand::End));
|
||||||
@@ -185,16 +190,16 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn decode_jump() {
|
fn decode_jump() {
|
||||||
// b2 bit 6 = jump
|
// b2 bit 6 = jump, with always-set bits 0,1
|
||||||
let data = [0x01, 0x00, 0x40, 0x00, 0x00, 0x03];
|
let data = [0x01, 0x00, 0x43, 0x00, 0x00, 0xF3];
|
||||||
let cmds = parse(&data).unwrap();
|
let cmds = parse(&data).unwrap();
|
||||||
assert!(matches!(cmds[0], StitchCommand::Jump { dx: 1, dy: 0 }));
|
assert!(matches!(cmds[0], StitchCommand::Jump { dx: 1, dy: 0 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn decode_color_change() {
|
fn decode_color_change() {
|
||||||
// b2 bit 7 = color change
|
// b2 bit 7 = color change, with always-set bits 0,1
|
||||||
let data = [0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x03];
|
let data = [0x00, 0x00, 0x83, 0x01, 0x00, 0x03, 0x00, 0x00, 0xF3];
|
||||||
let cmds = parse(&data).unwrap();
|
let cmds = parse(&data).unwrap();
|
||||||
assert!(matches!(cmds[0], StitchCommand::ColorChange));
|
assert!(matches!(cmds[0], StitchCommand::ColorChange));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ pub enum Format {
|
|||||||
Exp,
|
Exp,
|
||||||
Jef,
|
Jef,
|
||||||
Vp3,
|
Vp3,
|
||||||
|
Pec,
|
||||||
|
Xxx,
|
||||||
|
Sew,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detect format from file content (magic bytes).
|
/// Detect format from file content (magic bytes).
|
||||||
@@ -17,6 +20,9 @@ pub fn detect_from_bytes(data: &[u8]) -> Option<Format> {
|
|||||||
if data.len() >= 5 && &data[0..5] == b"%vsm%" {
|
if data.len() >= 5 && &data[0..5] == b"%vsm%" {
|
||||||
return Some(Format::Vp3);
|
return Some(Format::Vp3);
|
||||||
}
|
}
|
||||||
|
if data.len() >= 8 && &data[0..8] == b"#PEC0001" {
|
||||||
|
return Some(Format::Pec);
|
||||||
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +35,9 @@ pub fn detect_from_extension(path: &Path) -> Option<Format> {
|
|||||||
"exp" => Some(Format::Exp),
|
"exp" => Some(Format::Exp),
|
||||||
"jef" => Some(Format::Jef),
|
"jef" => Some(Format::Jef),
|
||||||
"vp3" => Some(Format::Vp3),
|
"vp3" => Some(Format::Vp3),
|
||||||
|
"pec" => Some(Format::Pec),
|
||||||
|
"xxx" => Some(Format::Xxx),
|
||||||
|
"sew" => Some(Format::Sew),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ pub mod types;
|
|||||||
pub mod dst;
|
pub mod dst;
|
||||||
pub mod exp;
|
pub mod exp;
|
||||||
pub mod jef;
|
pub mod jef;
|
||||||
|
pub mod pec;
|
||||||
pub mod pes;
|
pub mod pes;
|
||||||
|
pub mod sew;
|
||||||
pub mod vp3;
|
pub mod vp3;
|
||||||
|
pub mod xxx;
|
||||||
|
|
||||||
mod render;
|
mod render;
|
||||||
mod resolve;
|
mod resolve;
|
||||||
@@ -40,5 +43,8 @@ fn parse_and_resolve(data: &[u8], fmt: Format) -> Result<ResolvedDesign, Error>
|
|||||||
Format::Exp => exp::parse_and_resolve(data),
|
Format::Exp => exp::parse_and_resolve(data),
|
||||||
Format::Jef => jef::parse_and_resolve(data),
|
Format::Jef => jef::parse_and_resolve(data),
|
||||||
Format::Vp3 => vp3::parse_and_resolve(data),
|
Format::Vp3 => vp3::parse_and_resolve(data),
|
||||||
|
Format::Pec => pec::parse_and_resolve(data),
|
||||||
|
Format::Xxx => xxx::parse_and_resolve(data),
|
||||||
|
Format::Sew => sew::parse_and_resolve(data),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,11 @@ pub const DEFAULT_PALETTE: [(u8, u8, u8); 12] = [
|
|||||||
(106, 28, 138), // Violet
|
(106, 28, 138), // Violet
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Look up a PEC palette color by index, clamping to valid range.
|
||||||
|
pub fn pec_color(idx: u8) -> (u8, u8, u8) {
|
||||||
|
PEC_PALETTE[(idx as usize).min(PEC_PALETTE.len() - 1)]
|
||||||
|
}
|
||||||
|
|
||||||
/// Build a color list for `n` thread slots by cycling through `DEFAULT_PALETTE`.
|
/// Build a color list for `n` thread slots by cycling through `DEFAULT_PALETTE`.
|
||||||
pub fn default_colors(n: usize) -> Vec<(u8, u8, u8)> {
|
pub fn default_colors(n: usize) -> Vec<(u8, u8, u8)> {
|
||||||
(0..n)
|
(0..n)
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
use crate::error::Error;
|
||||||
|
use crate::pes::pec::{decode_stitches, parse_pec_header};
|
||||||
|
use crate::types::{ResolvedDesign, StitchCommand};
|
||||||
|
|
||||||
|
/// Parse a standalone PEC file (`#PEC0001` prefix + PEC data).
|
||||||
|
pub fn parse(data: &[u8]) -> Result<(Vec<StitchCommand>, Vec<(u8, u8, u8)>), Error> {
|
||||||
|
if data.len() < 8 || &data[0..8] != b"#PEC0001" {
|
||||||
|
return Err(Error::InvalidHeader("missing #PEC0001 magic".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let pec_data = &data[8..];
|
||||||
|
let (header, stitch_offset) = parse_pec_header(pec_data)?;
|
||||||
|
let commands = decode_stitches(&pec_data[stitch_offset..])?;
|
||||||
|
|
||||||
|
// Map PEC palette indices to RGB colors
|
||||||
|
let colors: Vec<(u8, u8, u8)> = header
|
||||||
|
.color_indices
|
||||||
|
.iter()
|
||||||
|
.map(|&idx| crate::palette::pec_color(idx))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok((commands, colors))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a standalone PEC file and resolve to a renderable design.
|
||||||
|
pub fn parse_and_resolve(data: &[u8]) -> Result<ResolvedDesign, Error> {
|
||||||
|
let (commands, colors) = parse(data)?;
|
||||||
|
crate::resolve::resolve(&commands, colors)
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
mod header;
|
mod header;
|
||||||
mod pec;
|
pub mod pec;
|
||||||
|
|
||||||
pub use header::PesHeader;
|
pub use header::PesHeader;
|
||||||
pub use pec::PecHeader;
|
pub use pec::PecHeader;
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
use crate::error::Error;
|
||||||
|
use crate::types::{ResolvedDesign, StitchCommand};
|
||||||
|
|
||||||
|
const STITCH_DATA_OFFSET: usize = 0x1D78;
|
||||||
|
|
||||||
|
/// Janome SEW thread color palette (first 80 entries).
|
||||||
|
const SEW_PALETTE: [(u8, u8, u8); 80] = [
|
||||||
|
(0, 0, 0), // 0: Unknown
|
||||||
|
(0, 0, 0), // 1: Black
|
||||||
|
(255, 255, 255), // 2: White
|
||||||
|
(255, 255, 23), // 3: Sunflower
|
||||||
|
(250, 160, 96), // 4: Hazel
|
||||||
|
(92, 118, 73), // 5: Green Dust
|
||||||
|
(64, 192, 48), // 6: Green
|
||||||
|
(101, 194, 200), // 7: Sky
|
||||||
|
(172, 128, 190), // 8: Purple
|
||||||
|
(245, 188, 203), // 9: Pink
|
||||||
|
(255, 0, 0), // 10: Red
|
||||||
|
(192, 128, 0), // 11: Brown
|
||||||
|
(0, 0, 240), // 12: Blue
|
||||||
|
(228, 195, 93), // 13: Gold
|
||||||
|
(165, 42, 42), // 14: Dark Brown
|
||||||
|
(213, 176, 212), // 15: Pale Violet
|
||||||
|
(252, 242, 148), // 16: Pale Yellow
|
||||||
|
(240, 208, 192), // 17: Pale Pink
|
||||||
|
(255, 192, 0), // 18: Peach
|
||||||
|
(201, 164, 128), // 19: Beige
|
||||||
|
(155, 61, 75), // 20: Wine Red
|
||||||
|
(160, 184, 204), // 21: Pale Sky
|
||||||
|
(127, 194, 28), // 22: Yellow Green
|
||||||
|
(185, 185, 185), // 23: Silver Grey
|
||||||
|
(160, 160, 160), // 24: Grey
|
||||||
|
(152, 214, 189), // 25: Pale Aqua
|
||||||
|
(184, 240, 240), // 26: Baby Blue
|
||||||
|
(54, 139, 160), // 27: Powder Blue
|
||||||
|
(79, 131, 171), // 28: Bright Blue
|
||||||
|
(56, 106, 145), // 29: Slate Blue
|
||||||
|
(0, 32, 107), // 30: Navy Blue
|
||||||
|
(229, 197, 202), // 31: Salmon Pink
|
||||||
|
(249, 103, 107), // 32: Coral
|
||||||
|
(227, 49, 31), // 33: Burnt Orange
|
||||||
|
(226, 161, 136), // 34: Cinnamon
|
||||||
|
(181, 148, 116), // 35: Umber
|
||||||
|
(228, 207, 153), // 36: Blonde
|
||||||
|
(225, 203, 0), // 37: Sunflower
|
||||||
|
(225, 173, 212), // 38: Orchid Pink
|
||||||
|
(195, 0, 126), // 39: Peony Purple
|
||||||
|
(128, 0, 75), // 40: Burgundy
|
||||||
|
(160, 96, 176), // 41: Royal Purple
|
||||||
|
(192, 64, 32), // 42: Cardinal Red
|
||||||
|
(202, 224, 192), // 43: Opal Green
|
||||||
|
(137, 152, 86), // 44: Moss Green
|
||||||
|
(0, 170, 0), // 45: Meadow Green
|
||||||
|
(33, 138, 33), // 46: Dark Green
|
||||||
|
(93, 174, 148), // 47: Aquamarine
|
||||||
|
(76, 191, 143), // 48: Emerald Green
|
||||||
|
(0, 119, 114), // 49: Peacock Green
|
||||||
|
(112, 112, 112), // 50: Dark Grey
|
||||||
|
(242, 255, 255), // 51: Ivory White
|
||||||
|
(177, 88, 24), // 52: Hazel
|
||||||
|
(203, 138, 7), // 53: Toast
|
||||||
|
(247, 146, 123), // 54: Salmon
|
||||||
|
(152, 105, 45), // 55: Cocoa Brown
|
||||||
|
(162, 113, 72), // 56: Sienna
|
||||||
|
(123, 85, 74), // 57: Sepia
|
||||||
|
(79, 57, 70), // 58: Dark Sepia
|
||||||
|
(82, 58, 151), // 59: Violet Blue
|
||||||
|
(0, 0, 160), // 60: Blue Ink
|
||||||
|
(0, 150, 222), // 61: Solar Blue
|
||||||
|
(178, 221, 83), // 62: Green Dust
|
||||||
|
(250, 143, 187), // 63: Crimson
|
||||||
|
(222, 100, 158), // 64: Floral Pink
|
||||||
|
(181, 80, 102), // 65: Wine
|
||||||
|
(94, 87, 71), // 66: Olive Drab
|
||||||
|
(76, 136, 31), // 67: Meadow
|
||||||
|
(228, 220, 121), // 68: Canary Yellow
|
||||||
|
(203, 138, 26), // 69: Toast
|
||||||
|
(198, 170, 66), // 70: Beige
|
||||||
|
(236, 176, 44), // 71: Honey Dew
|
||||||
|
(248, 128, 64), // 72: Tangerine
|
||||||
|
(255, 229, 5), // 73: Ocean Blue
|
||||||
|
(250, 122, 122), // 74: Sepia
|
||||||
|
(209, 164, 255), // 75: Sepia (alt)
|
||||||
|
(140, 90, 48), // 76: Unknown
|
||||||
|
(48, 80, 140), // 77: Unknown
|
||||||
|
(100, 160, 100), // 78: Unknown
|
||||||
|
(200, 100, 50), // 79: Unknown
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Parse a SEW (Janome) embroidery file.
|
||||||
|
///
|
||||||
|
/// Format:
|
||||||
|
/// - u16 LE color count at offset 0x00
|
||||||
|
/// - color_count × u16 LE thread palette indices at offset 0x02
|
||||||
|
/// - Graphical preview bitmap
|
||||||
|
/// - Stitch data at fixed offset 0x1D78
|
||||||
|
/// - Escape byte 0x80, control in next byte:
|
||||||
|
/// - control & 1: color change (skip 2 bytes)
|
||||||
|
/// - 0x02/0x04: jump/move (read 2 signed bytes)
|
||||||
|
/// - 0x10: normal stitch (read 2 signed bytes)
|
||||||
|
/// - other: end
|
||||||
|
/// - Y is negated
|
||||||
|
pub fn parse(data: &[u8]) -> Result<(Vec<StitchCommand>, Vec<(u8, u8, u8)>), Error> {
|
||||||
|
if data.len() < STITCH_DATA_OFFSET + 4 {
|
||||||
|
return Err(Error::TooShort {
|
||||||
|
expected: STITCH_DATA_OFFSET + 4,
|
||||||
|
actual: data.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let color_count = u16::from_le_bytes([data[0], data[1]]) as usize;
|
||||||
|
if color_count == 0 {
|
||||||
|
return Err(Error::InvalidHeader("zero color count".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read thread palette indices
|
||||||
|
let colors: Vec<(u8, u8, u8)> = (0..color_count)
|
||||||
|
.map(|i| {
|
||||||
|
let off = 2 + i * 2;
|
||||||
|
if off + 1 < data.len() {
|
||||||
|
let idx = u16::from_le_bytes([data[off], data[off + 1]]) as usize;
|
||||||
|
SEW_PALETTE[idx % SEW_PALETTE.len()]
|
||||||
|
} else {
|
||||||
|
(0, 0, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut commands = Vec::new();
|
||||||
|
let mut i = STITCH_DATA_OFFSET;
|
||||||
|
|
||||||
|
while i + 1 < data.len() {
|
||||||
|
let b0 = data[i];
|
||||||
|
let b1 = data[i + 1];
|
||||||
|
i += 2;
|
||||||
|
|
||||||
|
if b0 != 0x80 {
|
||||||
|
let dx = b0 as i8 as i16;
|
||||||
|
let dy = -(b1 as i8 as i16);
|
||||||
|
commands.push(StitchCommand::Stitch { dx, dy });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape: b0 == 0x80, b1 is the control byte
|
||||||
|
if i + 1 >= data.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let c0 = data[i];
|
||||||
|
let c1 = data[i + 1];
|
||||||
|
i += 2;
|
||||||
|
|
||||||
|
if b1 & 1 != 0 {
|
||||||
|
// Color change
|
||||||
|
commands.push(StitchCommand::ColorChange);
|
||||||
|
} else if b1 == 0x04 || b1 == 0x02 {
|
||||||
|
// Move/jump
|
||||||
|
let dx = c0 as i8 as i16;
|
||||||
|
let dy = -(c1 as i8 as i16);
|
||||||
|
commands.push(StitchCommand::Jump { dx, dy });
|
||||||
|
} else if b1 == 0x10 {
|
||||||
|
// Stitch with preceding escape
|
||||||
|
let dx = c0 as i8 as i16;
|
||||||
|
let dy = -(c1 as i8 as i16);
|
||||||
|
commands.push(StitchCommand::Stitch { dx, dy });
|
||||||
|
} else {
|
||||||
|
// Unknown control or end
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if commands.is_empty() {
|
||||||
|
return Err(Error::NoStitchData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matches!(commands.last(), Some(StitchCommand::End)) {
|
||||||
|
commands.push(StitchCommand::End);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((commands, colors))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a SEW file and resolve to a renderable design.
|
||||||
|
pub fn parse_and_resolve(data: &[u8]) -> Result<ResolvedDesign, Error> {
|
||||||
|
let (commands, colors) = parse(data)?;
|
||||||
|
crate::resolve::resolve(&commands, colors)
|
||||||
|
}
|
||||||
+187
-176
@@ -4,14 +4,24 @@ use crate::types::{ResolvedDesign, StitchCommand};
|
|||||||
/// Parse a VP3 (Pfaff/Viking) file from raw bytes.
|
/// Parse a VP3 (Pfaff/Viking) file from raw bytes.
|
||||||
///
|
///
|
||||||
/// VP3 is a hierarchical format:
|
/// VP3 is a hierarchical format:
|
||||||
/// - File header with "%vsm%" magic (or similar signature)
|
/// - `%vsm%` magic + null byte
|
||||||
/// - Design metadata section
|
/// - UTF-16 BE producer string
|
||||||
/// - One or more color sections, each containing:
|
/// - Design metadata (including center coordinates)
|
||||||
/// - Thread color (RGB)
|
/// - `xxPP` section marker
|
||||||
/// - Stitch data block
|
/// - Producer string (again)
|
||||||
|
/// - Color count
|
||||||
|
/// - Color blocks, each containing:
|
||||||
|
/// - 3-byte marker `\x00\x05\x00`
|
||||||
|
/// - 4-byte block size (u32 BE)
|
||||||
|
/// - Start position (2 × i32 BE, units ÷ 100, Y negated)
|
||||||
|
/// - Thread info (RGB, catalog, name, brand)
|
||||||
|
/// - 15 bytes metadata + 3 bytes preamble (`\x0A\xF6\x00`)
|
||||||
|
/// - Stitch data
|
||||||
///
|
///
|
||||||
/// Byte order: mixed, but length-prefixed strings and section sizes use big-endian.
|
/// Stitch encoding: 2-byte signed pairs (i8 dx, i8 dy).
|
||||||
/// Stitch encoding: variable-length (1 byte for small moves, 3 bytes for large).
|
/// Escape byte 0x80: next byte is sub-command:
|
||||||
|
/// - 0x01: extended move (2 × i16 BE dx, dy), followed by 2 bytes to skip
|
||||||
|
/// - 0x03: trim
|
||||||
type ParseResult = Result<(Vec<StitchCommand>, Vec<(u8, u8, u8)>), Error>;
|
type ParseResult = Result<(Vec<StitchCommand>, Vec<(u8, u8, u8)>), Error>;
|
||||||
|
|
||||||
pub fn parse(data: &[u8]) -> ParseResult {
|
pub fn parse(data: &[u8]) -> ParseResult {
|
||||||
@@ -22,25 +32,28 @@ pub fn parse(data: &[u8]) -> ParseResult {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut reader = Reader::new(data);
|
if &data[0..5] != b"%vsm%" {
|
||||||
|
return Err(Error::InvalidHeader("missing %vsm% magic".into()));
|
||||||
|
}
|
||||||
|
|
||||||
// VP3 files start with a magic/signature section
|
let xxpp_pos = find_marker(data, b"xxPP")
|
||||||
// Skip the initial header to find the design data
|
.ok_or_else(|| Error::InvalidHeader("missing xxPP section marker".into()))?;
|
||||||
// The format starts with a variable-length producer string, then design sections
|
|
||||||
skip_vp3_header(&mut reader)?;
|
let mut reader = Reader::new(data);
|
||||||
|
reader.pos = xxpp_pos + 4;
|
||||||
|
|
||||||
|
// Skip 2 bytes + producer string after xxPP
|
||||||
|
reader.skip(2)?;
|
||||||
|
skip_string(&mut reader)?;
|
||||||
|
|
||||||
|
let color_count = reader.read_u16_be()? as usize;
|
||||||
|
|
||||||
let mut colors = Vec::new();
|
let mut colors = Vec::new();
|
||||||
let mut commands = Vec::new();
|
let mut commands = Vec::new();
|
||||||
|
let mut cursor = (0i32, 0i32);
|
||||||
|
|
||||||
// Read color sections
|
for ci in 0..color_count {
|
||||||
let color_section_count = reader.read_u16_be()?;
|
let color = read_color_block(&mut reader, &mut commands, &mut cursor, ci > 0)?;
|
||||||
|
|
||||||
for _ in 0..color_section_count {
|
|
||||||
if reader.remaining() < 4 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let color = read_color_section(&mut reader, &mut commands)?;
|
|
||||||
colors.push(color);
|
colors.push(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,163 +74,139 @@ pub fn parse_and_resolve(data: &[u8]) -> Result<ResolvedDesign, Error> {
|
|||||||
crate::resolve::resolve(&commands, colors)
|
crate::resolve::resolve(&commands, colors)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn skip_vp3_header(reader: &mut Reader) -> Result<(), Error> {
|
fn find_marker(data: &[u8], marker: &[u8]) -> Option<usize> {
|
||||||
// Skip magic/producer string at start
|
data.windows(marker.len()).position(|w| w == marker)
|
||||||
// VP3 starts with a string like "%vsm%" or similar, followed by metadata
|
|
||||||
// Find the start of actual design data by looking for patterns
|
|
||||||
|
|
||||||
// Read and skip the initial producer/signature string
|
|
||||||
skip_string(reader)?;
|
|
||||||
|
|
||||||
// Skip design metadata: dimensions and other header fields
|
|
||||||
// After the producer string there are typically coordinate fields (i32 BE)
|
|
||||||
// and additional metadata strings
|
|
||||||
if reader.remaining() < 38 {
|
|
||||||
return Err(Error::TooShort {
|
|
||||||
expected: 38,
|
|
||||||
actual: reader.remaining(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip: design size fields (4x i32 = 16 bytes) + unknown bytes (4) + unknown (4)
|
|
||||||
reader.skip(24)?;
|
|
||||||
|
|
||||||
// Skip design notes/comments strings
|
|
||||||
skip_string(reader)?; // x-offset or notes
|
|
||||||
skip_string(reader)?; // y-offset or notes
|
|
||||||
|
|
||||||
// Skip remaining header fields before color sections
|
|
||||||
// There are typically 6 more bytes of header data
|
|
||||||
if reader.remaining() >= 6 {
|
|
||||||
reader.skip(6)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip another potential string
|
|
||||||
if reader.remaining() >= 2 {
|
|
||||||
let peek = reader.peek_u16_be();
|
|
||||||
if let Ok(len) = peek
|
|
||||||
&& len < 1000
|
|
||||||
&& (len as usize) + 2 <= reader.remaining()
|
|
||||||
{
|
|
||||||
skip_string(reader)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_color_section(
|
fn read_color_block(
|
||||||
reader: &mut Reader,
|
reader: &mut Reader,
|
||||||
commands: &mut Vec<StitchCommand>,
|
commands: &mut Vec<StitchCommand>,
|
||||||
|
cursor: &mut (i32, i32),
|
||||||
|
add_color_change: bool,
|
||||||
) -> Result<(u8, u8, u8), Error> {
|
) -> Result<(u8, u8, u8), Error> {
|
||||||
// Color change between sections (except first)
|
// 3-byte marker: \x00\x05\x00
|
||||||
if !commands.is_empty() {
|
reader.skip(3)?;
|
||||||
|
|
||||||
|
// 4-byte block size (distance to next block from current position)
|
||||||
|
let block_size = reader.read_u32_be()? as usize;
|
||||||
|
let block_end = reader.pos + block_size;
|
||||||
|
|
||||||
|
// Start position: i32 BE x, i32 BE y (units ÷ 100, Y negated)
|
||||||
|
let start_x_raw = reader.read_i32_be()?;
|
||||||
|
let start_y_raw = reader.read_i32_be()?;
|
||||||
|
let start_x = start_x_raw / 100;
|
||||||
|
let start_y = -(start_y_raw / 100);
|
||||||
|
|
||||||
|
// Jump to section start position if cursor is not already there
|
||||||
|
let jump_dx = start_x - cursor.0;
|
||||||
|
let jump_dy = start_y - cursor.1;
|
||||||
|
if jump_dx != 0 || jump_dy != 0 {
|
||||||
|
commands.push(StitchCommand::Trim);
|
||||||
|
commands.push(StitchCommand::Jump {
|
||||||
|
dx: jump_dx.clamp(-32768, 32767) as i16,
|
||||||
|
dy: jump_dy.clamp(-32768, 32767) as i16,
|
||||||
|
});
|
||||||
|
cursor.0 = start_x;
|
||||||
|
cursor.1 = start_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
if add_color_change {
|
||||||
commands.push(StitchCommand::ColorChange);
|
commands.push(StitchCommand::ColorChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip section start marker/offset bytes
|
// Read thread info
|
||||||
// Color sections start with coordinate offset data
|
let (r, g, b) = read_thread_info(reader)?;
|
||||||
if reader.remaining() < 12 {
|
|
||||||
return Err(Error::TooShort {
|
|
||||||
expected: 12,
|
|
||||||
actual: reader.remaining(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip section offset/position data (2x i32 = 8 bytes)
|
// Skip 15 bytes post-thread metadata + 3 bytes preamble (\x0A\xF6\x00)
|
||||||
reader.skip(8)?;
|
reader.skip(18)?;
|
||||||
|
|
||||||
// Skip thread info string
|
// Decode stitches until block end
|
||||||
skip_string(reader)?;
|
decode_vp3_stitches(reader, commands, block_end, cursor);
|
||||||
|
|
||||||
// Read thread color: RGB (3 bytes)
|
reader.pos = block_end;
|
||||||
if reader.remaining() < 3 {
|
|
||||||
return Err(Error::TooShort {
|
|
||||||
expected: 3,
|
|
||||||
actual: reader.remaining(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let r = reader.read_u8()?;
|
|
||||||
let g = reader.read_u8()?;
|
|
||||||
let b = reader.read_u8()?;
|
|
||||||
|
|
||||||
// Skip remaining thread metadata (thread type, weight, catalog info)
|
|
||||||
// Skip to stitch data: look for the stitch count field
|
|
||||||
skip_string(reader)?; // thread catalog number
|
|
||||||
skip_string(reader)?; // thread description
|
|
||||||
|
|
||||||
// Skip thread brand and additional metadata
|
|
||||||
// There's typically some padding/unknown bytes here
|
|
||||||
if reader.remaining() >= 18 {
|
|
||||||
reader.skip(18)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read stitch data
|
|
||||||
let stitch_byte_count = if reader.remaining() >= 4 {
|
|
||||||
reader.read_u32_be()? as usize
|
|
||||||
} else {
|
|
||||||
return Ok((r, g, b));
|
|
||||||
};
|
|
||||||
|
|
||||||
if stitch_byte_count == 0 || stitch_byte_count > reader.remaining() {
|
|
||||||
// Skip what we can
|
|
||||||
return Ok((r, g, b));
|
|
||||||
}
|
|
||||||
|
|
||||||
let stitch_end = reader.pos + stitch_byte_count;
|
|
||||||
decode_vp3_stitches(reader, commands, stitch_end);
|
|
||||||
|
|
||||||
// Ensure we're at the right position after stitch data
|
|
||||||
if reader.pos < stitch_end {
|
|
||||||
reader.pos = stitch_end;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((r, g, b))
|
Ok((r, g, b))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_vp3_stitches(reader: &mut Reader, commands: &mut Vec<StitchCommand>, end: usize) {
|
fn read_thread_info(reader: &mut Reader) -> Result<(u8, u8, u8), Error> {
|
||||||
while reader.pos < end && reader.remaining() >= 2 {
|
// Color table: count of sub-colors, transition byte
|
||||||
let b1 = reader.data[reader.pos];
|
let colors_count = reader.read_u8()?;
|
||||||
|
let _transition = reader.read_u8()?;
|
||||||
|
|
||||||
// Check for 3-byte extended coordinates (high bit set on first byte)
|
let mut r = 0u8;
|
||||||
if b1 & 0x80 != 0 {
|
let mut g = 0u8;
|
||||||
if reader.remaining() < 4 {
|
let mut b = 0u8;
|
||||||
break;
|
|
||||||
|
for _ in 0..colors_count {
|
||||||
|
r = reader.read_u8()?;
|
||||||
|
g = reader.read_u8()?;
|
||||||
|
b = reader.read_u8()?;
|
||||||
|
let _parts = reader.read_u8()?;
|
||||||
|
let _color_length = reader.read_u16_be()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread type + weight
|
||||||
|
reader.skip(2)?;
|
||||||
|
|
||||||
|
// 3 strings: catalog number, color name, brand name
|
||||||
|
skip_string(reader)?;
|
||||||
|
skip_string(reader)?;
|
||||||
|
skip_string(reader)?;
|
||||||
|
|
||||||
|
Ok((r, g, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_vp3_stitches(
|
||||||
|
reader: &mut Reader,
|
||||||
|
commands: &mut Vec<StitchCommand>,
|
||||||
|
end: usize,
|
||||||
|
cursor: &mut (i32, i32),
|
||||||
|
) {
|
||||||
|
while reader.pos + 1 < end && reader.pos + 1 < reader.data.len() {
|
||||||
|
let bx = reader.data[reader.pos] as i8;
|
||||||
|
let by = reader.data[reader.pos + 1] as i8;
|
||||||
|
reader.pos += 2;
|
||||||
|
|
||||||
|
if (bx as u8) != 0x80 {
|
||||||
|
// Normal stitch
|
||||||
|
let dx = bx as i16;
|
||||||
|
let dy = by as i16;
|
||||||
|
cursor.0 += dx as i32;
|
||||||
|
cursor.1 += dy as i32;
|
||||||
|
commands.push(StitchCommand::Stitch { dx, dy });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape byte 0x80 — check sub-command
|
||||||
|
match by as u8 {
|
||||||
|
0x01 => {
|
||||||
|
// Extended move: 2 × i16 BE
|
||||||
|
if reader.pos + 4 <= end {
|
||||||
|
let dx = read_i16_be(reader.data, reader.pos);
|
||||||
|
reader.pos += 2;
|
||||||
|
let dy = read_i16_be(reader.data, reader.pos);
|
||||||
|
reader.pos += 2;
|
||||||
|
cursor.0 += dx as i32;
|
||||||
|
cursor.1 += dy as i32;
|
||||||
|
commands.push(StitchCommand::Stitch { dx, dy });
|
||||||
|
// Skip trailing 0x80 0x02
|
||||||
|
if reader.pos + 2 <= end {
|
||||||
|
reader.pos += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let dx = read_i16_be(reader.data, reader.pos);
|
0x03 => {
|
||||||
reader.pos += 2;
|
// Trim
|
||||||
let dy = read_i16_be(reader.data, reader.pos);
|
|
||||||
reader.pos += 2;
|
|
||||||
|
|
||||||
// Large moves are jumps
|
|
||||||
commands.push(StitchCommand::Jump { dx, dy: -dy });
|
|
||||||
} else {
|
|
||||||
// 1-byte per coordinate
|
|
||||||
let dx = reader.data[reader.pos] as i8 as i16;
|
|
||||||
reader.pos += 1;
|
|
||||||
if reader.pos >= end {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let dy = -(reader.data[reader.pos] as i8 as i16);
|
|
||||||
reader.pos += 1;
|
|
||||||
|
|
||||||
if dx == 0 && dy == 0 {
|
|
||||||
// Zero-length stitch can be a trim marker
|
|
||||||
commands.push(StitchCommand::Trim);
|
commands.push(StitchCommand::Trim);
|
||||||
} else {
|
}
|
||||||
commands.push(StitchCommand::Stitch { dx, dy });
|
_ => {
|
||||||
|
// Unknown or no-op (0x00, 0x02, etc.)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn skip_string(reader: &mut Reader) -> Result<(), Error> {
|
fn skip_string(reader: &mut Reader) -> Result<(), Error> {
|
||||||
if reader.remaining() < 2 {
|
|
||||||
return Err(Error::TooShort {
|
|
||||||
expected: reader.pos + 2,
|
|
||||||
actual: reader.data.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let len = reader.read_u16_be()? as usize;
|
let len = reader.read_u16_be()? as usize;
|
||||||
if len > reader.remaining() {
|
if len > reader.remaining() {
|
||||||
return Err(Error::InvalidHeader(format!(
|
return Err(Error::InvalidHeader(format!(
|
||||||
@@ -272,19 +261,6 @@ impl<'a> Reader<'a> {
|
|||||||
Ok(v)
|
Ok(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn peek_u16_be(&self) -> Result<u16, Error> {
|
|
||||||
if self.pos + 2 > self.data.len() {
|
|
||||||
return Err(Error::TooShort {
|
|
||||||
expected: self.pos + 2,
|
|
||||||
actual: self.data.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(u16::from_be_bytes([
|
|
||||||
self.data[self.pos],
|
|
||||||
self.data[self.pos + 1],
|
|
||||||
]))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_u32_be(&mut self) -> Result<u32, Error> {
|
fn read_u32_be(&mut self) -> Result<u32, Error> {
|
||||||
if self.pos + 4 > self.data.len() {
|
if self.pos + 4 > self.data.len() {
|
||||||
return Err(Error::TooShort {
|
return Err(Error::TooShort {
|
||||||
@@ -302,6 +278,23 @@ impl<'a> Reader<'a> {
|
|||||||
Ok(v)
|
Ok(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_i32_be(&mut self) -> Result<i32, Error> {
|
||||||
|
if self.pos + 4 > self.data.len() {
|
||||||
|
return Err(Error::TooShort {
|
||||||
|
expected: self.pos + 4,
|
||||||
|
actual: self.data.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let v = i32::from_be_bytes([
|
||||||
|
self.data[self.pos],
|
||||||
|
self.data[self.pos + 1],
|
||||||
|
self.data[self.pos + 2],
|
||||||
|
self.data[self.pos + 3],
|
||||||
|
]);
|
||||||
|
self.pos += 4;
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
|
||||||
fn skip(&mut self, n: usize) -> Result<(), Error> {
|
fn skip(&mut self, n: usize) -> Result<(), Error> {
|
||||||
if self.pos + n > self.data.len() {
|
if self.pos + n > self.data.len() {
|
||||||
return Err(Error::TooShort {
|
return Err(Error::TooShort {
|
||||||
@@ -321,26 +314,44 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn decode_small_stitch() {
|
fn decode_small_stitch() {
|
||||||
let mut commands = Vec::new();
|
let mut commands = Vec::new();
|
||||||
// Two small stitches: (10, -20) and (5, -3)
|
|
||||||
let data = [0x0A, 0x14, 0x05, 0x03];
|
let data = [0x0A, 0x14, 0x05, 0x03];
|
||||||
let mut reader = Reader::new(&data);
|
let mut reader = Reader::new(&data);
|
||||||
decode_vp3_stitches(&mut reader, &mut commands, data.len());
|
let mut cursor = (0i32, 0i32);
|
||||||
|
decode_vp3_stitches(&mut reader, &mut commands, data.len(), &mut cursor);
|
||||||
assert_eq!(commands.len(), 2);
|
assert_eq!(commands.len(), 2);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
commands[0],
|
commands[0],
|
||||||
StitchCommand::Stitch { dx: 10, dy: -20 }
|
StitchCommand::Stitch { dx: 10, dy: 20 }
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn decode_large_jump() {
|
fn decode_escape_trim() {
|
||||||
let mut commands = Vec::new();
|
let mut commands = Vec::new();
|
||||||
// Large move: high bit set, 2-byte BE dx and dy
|
let data = [0x80, 0x03, 0x05, 0x03];
|
||||||
// dx = 0x8100 = -32512 as i16, dy = 0x0100 = 256
|
|
||||||
let data = [0x81, 0x00, 0x01, 0x00];
|
|
||||||
let mut reader = Reader::new(&data);
|
let mut reader = Reader::new(&data);
|
||||||
decode_vp3_stitches(&mut reader, &mut commands, data.len());
|
let mut cursor = (0i32, 0i32);
|
||||||
|
decode_vp3_stitches(&mut reader, &mut commands, data.len(), &mut cursor);
|
||||||
|
assert_eq!(commands.len(), 2);
|
||||||
|
assert!(matches!(commands[0], StitchCommand::Trim));
|
||||||
|
assert!(matches!(
|
||||||
|
commands[1],
|
||||||
|
StitchCommand::Stitch { dx: 5, dy: 3 }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_extended_move() {
|
||||||
|
// 0x80 0x01 + i16 BE dx(0x0100=256) + i16 BE dy(0xFF00=-256) + 0x80 0x02
|
||||||
|
let data = [0x80, 0x01, 0x01, 0x00, 0xFF, 0x00, 0x80, 0x02];
|
||||||
|
let mut commands = Vec::new();
|
||||||
|
let mut reader = Reader::new(&data);
|
||||||
|
let mut cursor = (0i32, 0i32);
|
||||||
|
decode_vp3_stitches(&mut reader, &mut commands, data.len(), &mut cursor);
|
||||||
assert_eq!(commands.len(), 1);
|
assert_eq!(commands.len(), 1);
|
||||||
assert!(matches!(commands[0], StitchCommand::Jump { .. }));
|
assert!(matches!(
|
||||||
|
commands[0],
|
||||||
|
StitchCommand::Stitch { dx: 256, dy: -256 }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
use crate::error::Error;
|
||||||
|
use crate::types::{ResolvedDesign, StitchCommand};
|
||||||
|
|
||||||
|
const HEADER_SIZE: usize = 256;
|
||||||
|
|
||||||
|
/// Parse an XXX (Singer) embroidery file.
|
||||||
|
///
|
||||||
|
/// Format:
|
||||||
|
/// - 256-byte header ("XXX" at offset 0xB6)
|
||||||
|
/// - Color count at offset 0x27 (LE u16)
|
||||||
|
/// - Stitch data at offset 0x100: 2-byte signed pairs (i8 dx, i8 dy), Y negated
|
||||||
|
/// - Escape byte 0x7F, followed by sub-command + 2 data bytes:
|
||||||
|
/// - 0x01: jump/move
|
||||||
|
/// - 0x03: trim (with optional move)
|
||||||
|
/// - 0x08 or 0x0A..0x17: color change
|
||||||
|
/// - 0x7F: end of data
|
||||||
|
/// - Color table after stitch data: skip 2 bytes, then color_count × i32 BE (0x00RRGGBB)
|
||||||
|
pub fn parse(data: &[u8]) -> Result<(Vec<StitchCommand>, Vec<(u8, u8, u8)>), Error> {
|
||||||
|
if data.len() < HEADER_SIZE + 2 {
|
||||||
|
return Err(Error::TooShort {
|
||||||
|
expected: HEADER_SIZE + 2,
|
||||||
|
actual: data.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let color_count = u16::from_le_bytes([data[0x27], data[0x28]]) as usize;
|
||||||
|
if color_count == 0 {
|
||||||
|
return Err(Error::InvalidHeader("zero color count".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut commands = Vec::new();
|
||||||
|
let mut i = HEADER_SIZE;
|
||||||
|
let mut color_table_start = data.len();
|
||||||
|
|
||||||
|
while i < data.len() {
|
||||||
|
let b1 = data[i];
|
||||||
|
i += 1;
|
||||||
|
|
||||||
|
// Big jump codes (0x7D, 0x7E)
|
||||||
|
if b1 == 0x7D || b1 == 0x7E {
|
||||||
|
if i + 4 > data.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let x = i16::from_le_bytes([data[i], data[i + 1]]);
|
||||||
|
let y = -i16::from_le_bytes([data[i + 2], data[i + 3]]);
|
||||||
|
i += 4;
|
||||||
|
commands.push(StitchCommand::Jump { dx: x, dy: y });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if i >= data.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let b2 = data[i];
|
||||||
|
i += 1;
|
||||||
|
|
||||||
|
if b1 != 0x7F {
|
||||||
|
let dx = b1 as i8 as i16;
|
||||||
|
let dy = -(b2 as i8 as i16);
|
||||||
|
commands.push(StitchCommand::Stitch { dx, dy });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape: b1 == 0x7F
|
||||||
|
if i + 2 > data.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let b3 = data[i];
|
||||||
|
let b4 = data[i + 1];
|
||||||
|
i += 2;
|
||||||
|
|
||||||
|
if b2 == 0x01 {
|
||||||
|
// Move/jump
|
||||||
|
let dx = b3 as i8 as i16;
|
||||||
|
let dy = -(b4 as i8 as i16);
|
||||||
|
commands.push(StitchCommand::Jump { dx, dy });
|
||||||
|
} else if b2 == 0x03 {
|
||||||
|
// Trim with optional move
|
||||||
|
commands.push(StitchCommand::Trim);
|
||||||
|
let dx = b3 as i8 as i16;
|
||||||
|
let dy = -(b4 as i8 as i16);
|
||||||
|
if dx != 0 || dy != 0 {
|
||||||
|
commands.push(StitchCommand::Jump { dx, dy });
|
||||||
|
}
|
||||||
|
} else if b2 == 0x08 || (0x0A..=0x17).contains(&b2) {
|
||||||
|
// Color change
|
||||||
|
commands.push(StitchCommand::ColorChange);
|
||||||
|
} else if b2 == 0x7F || b2 == 0x18 {
|
||||||
|
// End — color table follows after 2 bytes
|
||||||
|
color_table_start = i + 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if commands.is_empty() {
|
||||||
|
return Err(Error::NoStitchData);
|
||||||
|
}
|
||||||
|
|
||||||
|
commands.push(StitchCommand::End);
|
||||||
|
|
||||||
|
// Read color table: color_count × i32 BE (0x00RRGGBB)
|
||||||
|
let colors = if color_table_start + color_count * 4 <= data.len() {
|
||||||
|
(0..color_count)
|
||||||
|
.map(|c| {
|
||||||
|
let base = color_table_start + c * 4;
|
||||||
|
let rgb = u32::from_be_bytes([
|
||||||
|
data[base],
|
||||||
|
data[base + 1],
|
||||||
|
data[base + 2],
|
||||||
|
data[base + 3],
|
||||||
|
]);
|
||||||
|
(
|
||||||
|
((rgb >> 16) & 0xFF) as u8,
|
||||||
|
((rgb >> 8) & 0xFF) as u8,
|
||||||
|
(rgb & 0xFF) as u8,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
crate::palette::default_colors(color_count)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((commands, colors))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse an XXX file and resolve to a renderable design.
|
||||||
|
pub fn parse_and_resolve(data: &[u8]) -> Result<ResolvedDesign, Error> {
|
||||||
|
let (commands, colors) = parse(data)?;
|
||||||
|
crate::resolve::resolve(&commands, colors)
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "stitch-peek"
|
name = "stitch-peek"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Nautilus thumbnail generator for PES embroidery files"
|
description = "Nautilus thumbnail generator for PES embroidery files"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::fs;
|
|||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
name = "stitch-peek",
|
name = "stitch-peek",
|
||||||
about = "Embroidery file thumbnailer (PES, DST, EXP, JEF, VP3)"
|
about = "Embroidery file thumbnailer (PES, DST, EXP, JEF, VP3, PEC, XXX, SEW)"
|
||||||
)]
|
)]
|
||||||
struct Args {
|
struct Args {
|
||||||
/// Input embroidery file path
|
/// Input embroidery file path
|
||||||
|
|||||||
Reference in New Issue
Block a user