Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f161a25002 | |||
| 9a367b4d10 |
Generated
+1
-1
@@ -235,7 +235,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustitch"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"png",
|
||||
"thiserror",
|
||||
|
||||
@@ -23,4 +23,19 @@
|
||||
<comment>VP3 Pfaff embroidery file</comment>
|
||||
<glob pattern="*.vp3"/>
|
||||
</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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[Thumbnailer Entry]
|
||||
TryExec=stitch-peek
|
||||
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]
|
||||
name = "rustitch"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
edition = "2024"
|
||||
description = "PES embroidery file parser and thumbnail renderer"
|
||||
license = "MIT"
|
||||
|
||||
+18
-13
@@ -37,8 +37,10 @@ pub fn parse(data: &[u8]) -> Result<Vec<StitchCommand>, Error> {
|
||||
let b2 = stitch_data[i + 2];
|
||||
i += 3;
|
||||
|
||||
// End of file: byte 2 bits 0 and 1 both set, and specific pattern
|
||||
if b2 & 0x03 == 0x03 {
|
||||
// End of file: standard DST EOF pattern (0x00, 0x00, 0xF3)
|
||||
// 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);
|
||||
break;
|
||||
}
|
||||
@@ -46,14 +48,17 @@ pub fn parse(data: &[u8]) -> Result<Vec<StitchCommand>, Error> {
|
||||
let dx = decode_dx(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
|
||||
if b2 & 0x80 != 0 {
|
||||
if flags & 0x80 != 0 {
|
||||
commands.push(StitchCommand::ColorChange);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Jump: byte 2 bit 6
|
||||
if b2 & 0x40 != 0 {
|
||||
if flags & 0x40 != 0 {
|
||||
commands.push(StitchCommand::Jump { dx, dy });
|
||||
continue;
|
||||
}
|
||||
@@ -166,8 +171,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn decode_end_marker() {
|
||||
// b2 = 0x03 means end
|
||||
let data = [0x00, 0x00, 0x03];
|
||||
// DST EOF = 0x00, 0x00, 0xF3
|
||||
let data = [0x00, 0x00, 0xF3];
|
||||
let cmds = parse(&data).unwrap_err();
|
||||
assert!(matches!(cmds, Error::NoStitchData));
|
||||
}
|
||||
@@ -175,9 +180,9 @@ mod tests {
|
||||
#[test]
|
||||
fn decode_simple_stitch() {
|
||||
// A normal stitch followed by end
|
||||
// dx=+1: b0 bit 0. dy=+1: b0 bit 7. b2=0x00 (normal stitch)
|
||||
// Then end marker
|
||||
let data = [0x81, 0x00, 0x00, 0x00, 0x00, 0x03];
|
||||
// dx=+1: b0 bit 0. dy=+1: b0 bit 7. b2=0x03 (always-set bits)
|
||||
// Then end marker 0x00, 0x00, 0xF3
|
||||
let data = [0x81, 0x00, 0x03, 0x00, 0x00, 0xF3];
|
||||
let cmds = parse(&data).unwrap();
|
||||
assert!(matches!(cmds[0], StitchCommand::Stitch { dx: 1, dy: -1 }));
|
||||
assert!(matches!(cmds[1], StitchCommand::End));
|
||||
@@ -185,16 +190,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn decode_jump() {
|
||||
// b2 bit 6 = jump
|
||||
let data = [0x01, 0x00, 0x40, 0x00, 0x00, 0x03];
|
||||
// b2 bit 6 = jump, with always-set bits 0,1
|
||||
let data = [0x01, 0x00, 0x43, 0x00, 0x00, 0xF3];
|
||||
let cmds = parse(&data).unwrap();
|
||||
assert!(matches!(cmds[0], StitchCommand::Jump { dx: 1, dy: 0 }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_color_change() {
|
||||
// b2 bit 7 = color change
|
||||
let data = [0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x03];
|
||||
// b2 bit 7 = color change, with always-set bits 0,1
|
||||
let data = [0x00, 0x00, 0x83, 0x01, 0x00, 0x03, 0x00, 0x00, 0xF3];
|
||||
let cmds = parse(&data).unwrap();
|
||||
assert!(matches!(cmds[0], StitchCommand::ColorChange));
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ pub enum Format {
|
||||
Exp,
|
||||
Jef,
|
||||
Vp3,
|
||||
Pec,
|
||||
Xxx,
|
||||
Sew,
|
||||
}
|
||||
|
||||
/// 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%" {
|
||||
return Some(Format::Vp3);
|
||||
}
|
||||
if data.len() >= 8 && &data[0..8] == b"#PEC0001" {
|
||||
return Some(Format::Pec);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -29,6 +35,9 @@ pub fn detect_from_extension(path: &Path) -> Option<Format> {
|
||||
"exp" => Some(Format::Exp),
|
||||
"jef" => Some(Format::Jef),
|
||||
"vp3" => Some(Format::Vp3),
|
||||
"pec" => Some(Format::Pec),
|
||||
"xxx" => Some(Format::Xxx),
|
||||
"sew" => Some(Format::Sew),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ pub mod types;
|
||||
pub mod dst;
|
||||
pub mod exp;
|
||||
pub mod jef;
|
||||
pub mod pec;
|
||||
pub mod pes;
|
||||
pub mod sew;
|
||||
pub mod vp3;
|
||||
pub mod xxx;
|
||||
|
||||
mod render;
|
||||
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::Jef => jef::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
|
||||
];
|
||||
|
||||
/// 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`.
|
||||
pub fn default_colors(n: usize) -> Vec<(u8, u8, u8)> {
|
||||
(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 pec;
|
||||
pub mod pec;
|
||||
|
||||
pub use header::PesHeader;
|
||||
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)
|
||||
}
|
||||
+182
-171
@@ -4,14 +4,24 @@ use crate::types::{ResolvedDesign, StitchCommand};
|
||||
/// Parse a VP3 (Pfaff/Viking) file from raw bytes.
|
||||
///
|
||||
/// VP3 is a hierarchical format:
|
||||
/// - File header with "%vsm%" magic (or similar signature)
|
||||
/// - Design metadata section
|
||||
/// - One or more color sections, each containing:
|
||||
/// - Thread color (RGB)
|
||||
/// - Stitch data block
|
||||
/// - `%vsm%` magic + null byte
|
||||
/// - UTF-16 BE producer string
|
||||
/// - Design metadata (including center coordinates)
|
||||
/// - `xxPP` section marker
|
||||
/// - 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: variable-length (1 byte for small moves, 3 bytes for large).
|
||||
/// Stitch encoding: 2-byte signed pairs (i8 dx, i8 dy).
|
||||
/// 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>;
|
||||
|
||||
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
|
||||
// Skip the initial header to find the design data
|
||||
// The format starts with a variable-length producer string, then design sections
|
||||
skip_vp3_header(&mut reader)?;
|
||||
let xxpp_pos = find_marker(data, b"xxPP")
|
||||
.ok_or_else(|| Error::InvalidHeader("missing xxPP section marker".into()))?;
|
||||
|
||||
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 commands = Vec::new();
|
||||
let mut cursor = (0i32, 0i32);
|
||||
|
||||
// Read color sections
|
||||
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)?;
|
||||
for ci in 0..color_count {
|
||||
let color = read_color_block(&mut reader, &mut commands, &mut cursor, ci > 0)?;
|
||||
colors.push(color);
|
||||
}
|
||||
|
||||
@@ -61,163 +74,139 @@ pub fn parse_and_resolve(data: &[u8]) -> Result<ResolvedDesign, Error> {
|
||||
crate::resolve::resolve(&commands, colors)
|
||||
}
|
||||
|
||||
fn skip_vp3_header(reader: &mut Reader) -> Result<(), Error> {
|
||||
// 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
|
||||
|
||||
// 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(),
|
||||
});
|
||||
fn find_marker(data: &[u8], marker: &[u8]) -> Option<usize> {
|
||||
data.windows(marker.len()).position(|w| w == marker)
|
||||
}
|
||||
|
||||
// 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,
|
||||
commands: &mut Vec<StitchCommand>,
|
||||
cursor: &mut (i32, i32),
|
||||
add_color_change: bool,
|
||||
) -> Result<(u8, u8, u8), Error> {
|
||||
// Color change between sections (except first)
|
||||
if !commands.is_empty() {
|
||||
// 3-byte marker: \x00\x05\x00
|
||||
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);
|
||||
}
|
||||
|
||||
// Skip section start marker/offset bytes
|
||||
// Color sections start with coordinate offset data
|
||||
if reader.remaining() < 12 {
|
||||
return Err(Error::TooShort {
|
||||
expected: 12,
|
||||
actual: reader.remaining(),
|
||||
});
|
||||
}
|
||||
// Read thread info
|
||||
let (r, g, b) = read_thread_info(reader)?;
|
||||
|
||||
// Skip section offset/position data (2x i32 = 8 bytes)
|
||||
reader.skip(8)?;
|
||||
|
||||
// Skip thread info string
|
||||
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 {
|
||||
// Skip 15 bytes post-thread metadata + 3 bytes preamble (\x0A\xF6\x00)
|
||||
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));
|
||||
};
|
||||
// Decode stitches until block end
|
||||
decode_vp3_stitches(reader, commands, block_end, cursor);
|
||||
|
||||
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;
|
||||
}
|
||||
reader.pos = block_end;
|
||||
|
||||
Ok((r, g, b))
|
||||
}
|
||||
|
||||
fn decode_vp3_stitches(reader: &mut Reader, commands: &mut Vec<StitchCommand>, end: usize) {
|
||||
while reader.pos < end && reader.remaining() >= 2 {
|
||||
let b1 = reader.data[reader.pos];
|
||||
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()?;
|
||||
|
||||
// Check for 3-byte extended coordinates (high bit set on first byte)
|
||||
if b1 & 0x80 != 0 {
|
||||
if reader.remaining() < 4 {
|
||||
break;
|
||||
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
|
||||
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;
|
||||
|
||||
// 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);
|
||||
} else {
|
||||
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 => {
|
||||
// Trim
|
||||
commands.push(StitchCommand::Trim);
|
||||
}
|
||||
_ => {
|
||||
// Unknown or no-op (0x00, 0x02, etc.)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if len > reader.remaining() {
|
||||
return Err(Error::InvalidHeader(format!(
|
||||
@@ -272,19 +261,6 @@ impl<'a> Reader<'a> {
|
||||
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> {
|
||||
if self.pos + 4 > self.data.len() {
|
||||
return Err(Error::TooShort {
|
||||
@@ -302,6 +278,23 @@ impl<'a> Reader<'a> {
|
||||
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> {
|
||||
if self.pos + n > self.data.len() {
|
||||
return Err(Error::TooShort {
|
||||
@@ -321,26 +314,44 @@ mod tests {
|
||||
#[test]
|
||||
fn decode_small_stitch() {
|
||||
let mut commands = Vec::new();
|
||||
// Two small stitches: (10, -20) and (5, -3)
|
||||
let data = [0x0A, 0x14, 0x05, 0x03];
|
||||
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::Stitch { dx: 10, dy: -20 }
|
||||
StitchCommand::Stitch { dx: 10, dy: 20 }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_large_jump() {
|
||||
fn decode_escape_trim() {
|
||||
let mut commands = Vec::new();
|
||||
// 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 data = [0x80, 0x03, 0x05, 0x03];
|
||||
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!(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]
|
||||
name = "stitch-peek"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
edition = "2024"
|
||||
description = "Nautilus thumbnail generator for PES embroidery files"
|
||||
license = "MIT"
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::fs;
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
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 {
|
||||
/// Input embroidery file path
|
||||
|
||||
Reference in New Issue
Block a user