1 Commits

Author SHA1 Message Date
nvrl c9c7245dea Merge pull request 'added DST, EXP, JEF, VP3' (#7) from release/0.2.0 into main
Release / Build and Release (push) Successful in 38s
Reviewed-on: #7
2026-03-31 12:42:44 +02:00
26 changed files with 209 additions and 609 deletions
Generated
+2 -2
View File
@@ -235,7 +235,7 @@ dependencies = [
[[package]] [[package]]
name = "rustitch" name = "rustitch"
version = "0.2.2" version = "0.2.0"
dependencies = [ dependencies = [
"png", "png",
"thiserror", "thiserror",
@@ -250,7 +250,7 @@ checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]] [[package]]
name = "stitch-peek" name = "stitch-peek"
version = "0.1.4" version = "0.1.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
-15
View File
@@ -23,19 +23,4 @@
<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 -1
View File
@@ -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;application/x-pec;application/x-xxx;application/x-sew MimeType=application/x-pes;application/x-dst;application/x-exp;application/x-jef;application/x-vp3
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rustitch" name = "rustitch"
version = "0.2.2" version = "0.2.1"
edition = "2024" edition = "2024"
description = "PES embroidery file parser and thumbnail renderer" description = "PES embroidery file parser and thumbnail renderer"
license = "MIT" license = "MIT"
+13 -18
View File
@@ -37,10 +37,8 @@ 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: standard DST EOF pattern (0x00, 0x00, 0xF3) // End of file: byte 2 bits 0 and 1 both set, and specific pattern
// Bits 0 and 1 of byte 2 are always set in valid DST records, if b2 & 0x03 == 0x03 {
// 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;
} }
@@ -48,17 +46,14 @@ 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 flags & 0x80 != 0 { if b2 & 0x80 != 0 {
commands.push(StitchCommand::ColorChange); commands.push(StitchCommand::ColorChange);
continue; continue;
} }
// Jump: byte 2 bit 6 // Jump: byte 2 bit 6
if flags & 0x40 != 0 { if b2 & 0x40 != 0 {
commands.push(StitchCommand::Jump { dx, dy }); commands.push(StitchCommand::Jump { dx, dy });
continue; continue;
} }
@@ -171,8 +166,8 @@ mod tests {
#[test] #[test]
fn decode_end_marker() { fn decode_end_marker() {
// DST EOF = 0x00, 0x00, 0xF3 // b2 = 0x03 means end
let data = [0x00, 0x00, 0xF3]; let data = [0x00, 0x00, 0x03];
let cmds = parse(&data).unwrap_err(); let cmds = parse(&data).unwrap_err();
assert!(matches!(cmds, Error::NoStitchData)); assert!(matches!(cmds, Error::NoStitchData));
} }
@@ -180,9 +175,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=0x03 (always-set bits) // dx=+1: b0 bit 0. dy=+1: b0 bit 7. b2=0x00 (normal stitch)
// Then end marker 0x00, 0x00, 0xF3 // Then end marker
let data = [0x81, 0x00, 0x03, 0x00, 0x00, 0xF3]; let data = [0x81, 0x00, 0x00, 0x00, 0x00, 0x03];
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));
@@ -190,16 +185,16 @@ mod tests {
#[test] #[test]
fn decode_jump() { fn decode_jump() {
// b2 bit 6 = jump, with always-set bits 0,1 // b2 bit 6 = jump
let data = [0x01, 0x00, 0x43, 0x00, 0x00, 0xF3]; let data = [0x01, 0x00, 0x40, 0x00, 0x00, 0x03];
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, with always-set bits 0,1 // b2 bit 7 = color change
let data = [0x00, 0x00, 0x83, 0x01, 0x00, 0x03, 0x00, 0x00, 0xF3]; let data = [0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x03];
let cmds = parse(&data).unwrap(); let cmds = parse(&data).unwrap();
assert!(matches!(cmds[0], StitchCommand::ColorChange)); assert!(matches!(cmds[0], StitchCommand::ColorChange));
} }
-9
View File
@@ -7,9 +7,6 @@ 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).
@@ -20,9 +17,6 @@ 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
} }
@@ -35,9 +29,6 @@ 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,
} }
} }
+3 -3
View File
@@ -1,7 +1,7 @@
mod palette; mod palette;
use crate::error::Error; use crate::error::Error;
use crate::types::{Color, RawDesign, ResolvedDesign, StitchCommand}; use crate::types::{ResolvedDesign, StitchCommand};
use palette::JEF_PALETTE; use palette::JEF_PALETTE;
/// Parse a JEF (Janome) file from raw bytes into stitch commands and color info. /// Parse a JEF (Janome) file from raw bytes into stitch commands and color info.
@@ -15,7 +15,7 @@ use palette::JEF_PALETTE;
/// ///
/// Stitch data: 2 bytes per stitch (signed i8 dx, dy). /// Stitch data: 2 bytes per stitch (signed i8 dx, dy).
/// Control codes: 0x80 0x01 = color change, 0x80 0x02 = jump, 0x80 0x10 = end. /// Control codes: 0x80 0x01 = color change, 0x80 0x02 = jump, 0x80 0x10 = end.
type ParseResult = Result<RawDesign, Error>; type ParseResult = Result<(Vec<StitchCommand>, Vec<(u8, u8, u8)>), Error>;
pub fn parse(data: &[u8]) -> ParseResult { pub fn parse(data: &[u8]) -> ParseResult {
if data.len() < 116 { if data.len() < 116 {
@@ -38,7 +38,7 @@ pub fn parse(data: &[u8]) -> ParseResult {
// Read color table starting at offset 116 // Read color table starting at offset 116
let color_table_start = 116; let color_table_start = 116;
let mut colors: Vec<Color> = Vec::with_capacity(color_count); let mut colors = Vec::with_capacity(color_count);
for i in 0..color_count { for i in 0..color_count {
let entry_offset = color_table_start + i * 4; let entry_offset = color_table_start + i * 4;
if entry_offset + 4 > data.len() { if entry_offset + 4 > data.len() {
+1 -3
View File
@@ -1,8 +1,6 @@
use crate::types::Color;
/// Janome thread color palette (78 entries). /// Janome thread color palette (78 entries).
/// Index 0 is a fallback; indices 1-77 correspond to standard Janome thread colors. /// Index 0 is a fallback; indices 1-77 correspond to standard Janome thread colors.
pub const JEF_PALETTE: [Color; 78] = [ pub const JEF_PALETTE: [(u8, u8, u8); 78] = [
(0, 0, 0), // 0: Unknown / Black (0, 0, 0), // 0: Unknown / Black
(0, 0, 0), // 1: Black (0, 0, 0), // 1: Black
(255, 255, 255), // 2: White (255, 255, 255), // 2: White
-6
View File
@@ -6,11 +6,8 @@ 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;
@@ -43,8 +40,5 @@ 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),
} }
} }
+3 -10
View File
@@ -1,8 +1,6 @@
use crate::types::Color;
/// Brother PEC thread color palette (65 entries). /// Brother PEC thread color palette (65 entries).
/// Index 0 is a fallback; indices 1-64 correspond to standard Brother thread colors. /// Index 0 is a fallback; indices 1-64 correspond to standard Brother thread colors.
pub const PEC_PALETTE: [Color; 65] = [ pub const PEC_PALETTE: [(u8, u8, u8); 65] = [
(0, 0, 0), // 0: Unknown (0, 0, 0), // 0: Unknown
(14, 31, 124), // 1: Prussian Blue (14, 31, 124), // 1: Prussian Blue
(10, 85, 163), // 2: Blue (10, 85, 163), // 2: Blue
@@ -72,7 +70,7 @@ pub const PEC_PALETTE: [Color; 65] = [
/// Default high-contrast palette for formats without embedded color info (DST, EXP). /// Default high-contrast palette for formats without embedded color info (DST, EXP).
/// Colors cycle on each color change. /// Colors cycle on each color change.
pub const DEFAULT_PALETTE: [Color; 12] = [ pub const DEFAULT_PALETTE: [(u8, u8, u8); 12] = [
(0, 0, 0), // Black (0, 0, 0), // Black
(237, 23, 31), // Red (237, 23, 31), // Red
(10, 85, 163), // Blue (10, 85, 163), // Blue
@@ -87,13 +85,8 @@ pub const DEFAULT_PALETTE: [Color; 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) -> Color {
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<Color> { pub fn default_colors(n: usize) -> Vec<(u8, u8, u8)> {
(0..n) (0..n)
.map(|i| DEFAULT_PALETTE[i % DEFAULT_PALETTE.len()]) .map(|i| DEFAULT_PALETTE[i % DEFAULT_PALETTE.len()])
.collect() .collect()
-29
View File
@@ -1,29 +0,0 @@
use crate::error::Error;
use crate::pes::pec::{decode_stitches, parse_pec_header};
use crate::types::{Color, RawDesign, ResolvedDesign};
/// Parse a standalone PEC file (`#PEC0001` prefix + PEC data).
pub fn parse(data: &[u8]) -> Result<RawDesign, 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<Color> = 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)
}
+3 -3
View File
@@ -1,5 +1,5 @@
mod header; mod header;
pub mod pec; mod pec;
pub use header::PesHeader; pub use header::PesHeader;
pub use pec::PecHeader; pub use pec::PecHeader;
@@ -7,7 +7,7 @@ pub use pec::PecHeader;
// Re-export shared types for backward compatibility // Re-export shared types for backward compatibility
pub use crate::error::Error; pub use crate::error::Error;
pub use crate::palette::PEC_PALETTE; pub use crate::palette::PEC_PALETTE;
pub use crate::types::{BoundingBox, Color, ResolvedDesign, StitchCommand, StitchSegment}; pub use crate::types::{BoundingBox, ResolvedDesign, StitchCommand, StitchSegment};
pub struct PesDesign { pub struct PesDesign {
pub header: PesHeader, pub header: PesHeader,
@@ -37,7 +37,7 @@ pub fn parse(data: &[u8]) -> Result<PesDesign, Error> {
/// Convert parsed PES design into renderable segments with absolute coordinates. /// Convert parsed PES design into renderable segments with absolute coordinates.
pub fn resolve(design: &PesDesign) -> Result<ResolvedDesign, Error> { pub fn resolve(design: &PesDesign) -> Result<ResolvedDesign, Error> {
let colors: Vec<Color> = design let colors: Vec<(u8, u8, u8)> = design
.pec_header .pec_header
.color_indices .color_indices
.iter() .iter()
+5 -2
View File
@@ -1,8 +1,11 @@
use crate::error::Error; use crate::error::Error;
use crate::types::{BoundingBox, Color, ResolvedDesign, StitchCommand, StitchSegment}; use crate::types::{BoundingBox, ResolvedDesign, StitchCommand, StitchSegment};
/// Convert parsed stitch commands into renderable segments with absolute coordinates. /// Convert parsed stitch commands into renderable segments with absolute coordinates.
pub fn resolve(commands: &[StitchCommand], colors: Vec<Color>) -> Result<ResolvedDesign, Error> { pub fn resolve(
commands: &[StitchCommand],
colors: Vec<(u8, u8, u8)>,
) -> Result<ResolvedDesign, Error> {
let mut segments = Vec::new(); let mut segments = Vec::new();
let mut x: f32 = 0.0; let mut x: f32 = 0.0;
let mut y: f32 = 0.0; let mut y: f32 = 0.0;
-186
View File
@@ -1,186 +0,0 @@
use crate::error::Error;
use crate::types::{Color, RawDesign, ResolvedDesign, StitchCommand};
const STITCH_DATA_OFFSET: usize = 0x1D78;
/// Janome SEW thread color palette (first 80 entries).
const SEW_PALETTE: [Color; 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<RawDesign, 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<Color> = (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)
}
+1 -4
View File
@@ -7,9 +7,6 @@ pub enum StitchCommand {
End, End,
} }
pub type Color = (u8, u8, u8);
pub type RawDesign = (Vec<StitchCommand>, Vec<Color>);
pub struct StitchSegment { pub struct StitchSegment {
pub x0: f32, pub x0: f32,
pub y0: f32, pub y0: f32,
@@ -27,6 +24,6 @@ pub struct BoundingBox {
pub struct ResolvedDesign { pub struct ResolvedDesign {
pub segments: Vec<StitchSegment>, pub segments: Vec<StitchSegment>,
pub colors: Vec<Color>, pub colors: Vec<(u8, u8, u8)>,
pub bounds: BoundingBox, pub bounds: BoundingBox,
} }
+174 -185
View File
@@ -4,24 +4,14 @@ 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:
/// - `%vsm%` magic + null byte /// - File header with "%vsm%" magic (or similar signature)
/// - UTF-16 BE producer string /// - Design metadata section
/// - Design metadata (including center coordinates) /// - One or more color sections, each containing:
/// - `xxPP` section marker /// - Thread color (RGB)
/// - Producer string (again) /// - Stitch data block
/// - 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
/// ///
/// Stitch encoding: 2-byte signed pairs (i8 dx, i8 dy). /// Byte order: mixed, but length-prefixed strings and section sizes use big-endian.
/// Escape byte 0x80: next byte is sub-command: /// Stitch encoding: variable-length (1 byte for small moves, 3 bytes for large).
/// - 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 {
@@ -32,28 +22,25 @@ pub fn parse(data: &[u8]) -> ParseResult {
}); });
} }
if &data[0..5] != b"%vsm%" {
return Err(Error::InvalidHeader("missing %vsm% magic".into()));
}
let xxpp_pos = find_marker(data, b"xxPP")
.ok_or_else(|| Error::InvalidHeader("missing xxPP section marker".into()))?;
let mut reader = Reader::new(data); let mut reader = Reader::new(data);
reader.pos = xxpp_pos + 4;
// Skip 2 bytes + producer string after xxPP // VP3 files start with a magic/signature section
reader.skip(2)?; // Skip the initial header to find the design data
skip_string(&mut reader)?; // The format starts with a variable-length producer string, then design sections
skip_vp3_header(&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);
for ci in 0..color_count { // Read color sections
let color = read_color_block(&mut reader, &mut commands, &mut cursor, ci > 0)?; let color_section_count = reader.read_u16_be()?;
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);
} }
@@ -74,139 +61,163 @@ pub fn parse_and_resolve(data: &[u8]) -> Result<ResolvedDesign, Error> {
crate::resolve::resolve(&commands, colors) crate::resolve::resolve(&commands, colors)
} }
fn find_marker(data: &[u8], marker: &[u8]) -> Option<usize> { fn skip_vp3_header(reader: &mut Reader) -> Result<(), Error> {
data.windows(marker.len()).position(|w| w == marker) // Skip magic/producer string at start
} // VP3 starts with a string like "%vsm%" or similar, followed by metadata
// Find the start of actual design data by looking for patterns
fn read_color_block( // Read and skip the initial producer/signature string
reader: &mut Reader, skip_string(reader)?;
commands: &mut Vec<StitchCommand>,
cursor: &mut (i32, i32),
add_color_change: bool,
) -> Result<(u8, u8, u8), Error> {
// 3-byte marker: \x00\x05\x00
reader.skip(3)?;
// 4-byte block size (distance to next block from current position) // Skip design metadata: dimensions and other header fields
let block_size = reader.read_u32_be()? as usize; // After the producer string there are typically coordinate fields (i32 BE)
let block_end = reader.pos + block_size; // and additional metadata strings
if reader.remaining() < 38 {
// Start position: i32 BE x, i32 BE y (units ÷ 100, Y negated) return Err(Error::TooShort {
let start_x_raw = reader.read_i32_be()?; expected: 38,
let start_y_raw = reader.read_i32_be()?; actual: reader.remaining(),
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 { // 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(
reader: &mut Reader,
commands: &mut Vec<StitchCommand>,
) -> Result<(u8, u8, u8), Error> {
// Color change between sections (except first)
if !commands.is_empty() {
commands.push(StitchCommand::ColorChange); commands.push(StitchCommand::ColorChange);
} }
// Read thread info // Skip section start marker/offset bytes
let (r, g, b) = read_thread_info(reader)?; // Color sections start with coordinate offset data
if reader.remaining() < 12 {
// Skip 15 bytes post-thread metadata + 3 bytes preamble (\x0A\xF6\x00) return Err(Error::TooShort {
reader.skip(18)?; expected: 12,
actual: reader.remaining(),
// Decode stitches until block end });
decode_vp3_stitches(reader, commands, block_end, cursor);
reader.pos = block_end;
Ok((r, g, b))
}
fn read_thread_info(reader: &mut Reader) -> Result<(u8, u8, u8), Error> {
// Color table: count of sub-colors, transition byte
let colors_count = reader.read_u8()?;
let _transition = reader.read_u8()?;
let mut r = 0u8;
let mut g = 0u8;
let mut b = 0u8;
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 // Skip section offset/position data (2x i32 = 8 bytes)
reader.skip(2)?; reader.skip(8)?;
// 3 strings: catalog number, color name, brand name // Skip thread info string
skip_string(reader)?;
skip_string(reader)?;
skip_string(reader)?; skip_string(reader)?;
// Read thread color: RGB (3 bytes)
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( fn decode_vp3_stitches(reader: &mut Reader, commands: &mut Vec<StitchCommand>, end: usize) {
reader: &mut Reader, while reader.pos < end && reader.remaining() >= 2 {
commands: &mut Vec<StitchCommand>, let b1 = reader.data[reader.pos];
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 { // Check for 3-byte extended coordinates (high bit set on first byte)
// Normal stitch if b1 & 0x80 != 0 {
let dx = bx as i16; if reader.remaining() < 4 {
let dy = by as i16; break;
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;
}
}
} }
0x03 => { let dx = read_i16_be(reader.data, reader.pos);
// Trim reader.pos += 2;
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!(
@@ -261,6 +272,19 @@ 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 {
@@ -278,23 +302,6 @@ 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 {
@@ -314,44 +321,26 @@ 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);
let mut cursor = (0i32, 0i32); decode_vp3_stitches(&mut reader, &mut commands, data.len());
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_escape_trim() { fn decode_large_jump() {
let mut commands = Vec::new(); let mut commands = Vec::new();
let data = [0x80, 0x03, 0x05, 0x03]; // Large move: high bit set, 2-byte BE dx and dy
// 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);
let mut cursor = (0i32, 0i32); decode_vp3_stitches(&mut reader, &mut commands, data.len());
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!( assert!(matches!(commands[0], StitchCommand::Jump { .. }));
commands[0],
StitchCommand::Stitch { dx: 256, dy: -256 }
));
} }
} }
-130
View File
@@ -1,130 +0,0 @@
use crate::error::Error;
use crate::types::{Color, RawDesign, 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<RawDesign, 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: Vec<Color> = 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)
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "stitch-peek" name = "stitch-peek"
version = "0.1.4" version = "0.1.3"
edition = "2024" edition = "2024"
description = "Nautilus thumbnail generator for PES embroidery files" description = "Nautilus thumbnail generator for PES embroidery files"
license = "MIT" license = "MIT"
+1 -1
View File
@@ -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, PEC, XXX, SEW)" about = "Embroidery file thumbnailer (PES, DST, EXP, JEF, VP3)"
)] )]
struct Args { struct Args {
/// Input embroidery file path /// Input embroidery file path