diff --git a/rustitch/src/dst/mod.rs b/rustitch/src/dst/mod.rs new file mode 100644 index 0000000..a73ec6c --- /dev/null +++ b/rustitch/src/dst/mod.rs @@ -0,0 +1,180 @@ +use crate::error::Error; +use crate::palette::default_colors; +use crate::types::{ResolvedDesign, StitchCommand}; + +/// Parse a DST (Tajima) file from raw bytes into stitch commands. +/// +/// DST format: 3 bytes per stitch record with bit-packed dx, dy, and control flags. +/// The standard Tajima bit layout maps specific bits across all 3 bytes to coordinate values. +pub fn parse(data: &[u8]) -> Result, Error> { + if data.len() < 3 { + return Err(Error::TooShort { + expected: 3, + actual: data.len(), + }); + } + + // DST files may have a 512-byte header; stitch data can start at byte 512 + // if the file is large enough, or at byte 0 for raw stitch streams. + // The header contains "LA:" at offset 0 if present. + let offset = if data.len() > 512 && &data[0..3] == b"LA:" { + 512 + } else { + 0 + }; + + let stitch_data = &data[offset..]; + if stitch_data.len() < 3 { + return Err(Error::NoStitchData); + } + + let mut commands = Vec::new(); + let mut i = 0; + + while i + 2 < stitch_data.len() { + let b0 = stitch_data[i]; + let b1 = stitch_data[i + 1]; + 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 { + commands.push(StitchCommand::End); + break; + } + + let dx = decode_dx(b0, b1, b2); + let dy = decode_dy(b0, b1, b2); + + // Color change: byte 2 bit 7 + if b2 & 0x80 != 0 { + commands.push(StitchCommand::ColorChange); + continue; + } + + // Jump: byte 2 bit 6 + if b2 & 0x40 != 0 { + commands.push(StitchCommand::Jump { dx, dy }); + continue; + } + + commands.push(StitchCommand::Stitch { dx, dy }); + } + + if commands.is_empty() || (commands.len() == 1 && matches!(commands[0], StitchCommand::End)) { + return Err(Error::NoStitchData); + } + + // Ensure we have an End marker + if !matches!(commands.last(), Some(StitchCommand::End)) { + commands.push(StitchCommand::End); + } + + Ok(commands) +} + +/// Decode X displacement from the 3-byte Tajima record. +/// Standard bit layout for dx across bytes b0, b1, b2. +fn decode_dx(b0: u8, b1: u8, b2: u8) -> i16 { + let mut x: i16 = 0; + if b0 & 0x01 != 0 { x += 1; } + if b0 & 0x02 != 0 { x -= 1; } + if b0 & 0x04 != 0 { x += 9; } + if b0 & 0x08 != 0 { x -= 9; } + if b1 & 0x01 != 0 { x += 3; } + if b1 & 0x02 != 0 { x -= 3; } + if b1 & 0x04 != 0 { x += 27; } + if b1 & 0x08 != 0 { x -= 27; } + if b2 & 0x04 != 0 { x += 81; } + if b2 & 0x08 != 0 { x -= 81; } + x +} + +/// Decode Y displacement from the 3-byte Tajima record. +/// Standard bit layout for dy across bytes b0, b1, b2. +fn decode_dy(b0: u8, b1: u8, b2: u8) -> i16 { + let mut y: i16 = 0; + if b0 & 0x80 != 0 { y += 1; } + if b0 & 0x40 != 0 { y -= 1; } + if b0 & 0x20 != 0 { y += 9; } + if b0 & 0x10 != 0 { y -= 9; } + if b1 & 0x80 != 0 { y += 3; } + if b1 & 0x40 != 0 { y -= 3; } + if b1 & 0x20 != 0 { y += 27; } + if b1 & 0x10 != 0 { y -= 27; } + if b2 & 0x20 != 0 { y += 81; } + if b2 & 0x10 != 0 { y -= 81; } + // DST Y axis is inverted (positive = up in machine coords, down in screen coords) + -y +} + +/// Parse a DST file and resolve to a renderable design. +pub fn parse_and_resolve(data: &[u8]) -> Result { + let commands = parse(data)?; + let color_count = commands + .iter() + .filter(|c| matches!(c, StitchCommand::ColorChange)) + .count() + + 1; + let colors = default_colors(color_count); + crate::resolve::resolve(&commands, colors) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decode_end_marker() { + // b2 = 0x03 means end + let data = [0x00, 0x00, 0x03]; + let cmds = parse(&data).unwrap_err(); + assert!(matches!(cmds, Error::NoStitchData)); + } + + #[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]; + let cmds = parse(&data).unwrap(); + assert!(matches!(cmds[0], StitchCommand::Stitch { dx: 1, dy: -1 })); + assert!(matches!(cmds[1], StitchCommand::End)); + } + + #[test] + fn decode_jump() { + // b2 bit 6 = jump + let data = [0x01, 0x00, 0x40, 0x00, 0x00, 0x03]; + 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]; + let cmds = parse(&data).unwrap(); + assert!(matches!(cmds[0], StitchCommand::ColorChange)); + } + + #[test] + fn decode_dx_values() { + assert_eq!(decode_dx(0x01, 0x00, 0x00), 1); + assert_eq!(decode_dx(0x02, 0x00, 0x00), -1); + assert_eq!(decode_dx(0x04, 0x00, 0x00), 9); + assert_eq!(decode_dx(0x00, 0x04, 0x00), 27); + assert_eq!(decode_dx(0x00, 0x00, 0x04), 81); + assert_eq!(decode_dx(0x05, 0x05, 0x04), 1 + 9 + 27 + 81); // 118 + } + + #[test] + fn decode_dy_values() { + assert_eq!(decode_dy(0x80, 0x00, 0x00), -1); + assert_eq!(decode_dy(0x40, 0x00, 0x00), 1); + assert_eq!(decode_dy(0x20, 0x00, 0x00), -9); + assert_eq!(decode_dy(0x00, 0x20, 0x00), -27); + assert_eq!(decode_dy(0x00, 0x00, 0x20), -81); + } +} diff --git a/rustitch/src/error.rs b/rustitch/src/error.rs new file mode 100644 index 0000000..f325741 --- /dev/null +++ b/rustitch/src/error.rs @@ -0,0 +1,23 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("invalid PES magic: expected #PES, got {0:?}")] + InvalidPesMagic([u8; 4]), + #[error("file too short: need {expected} bytes, got {actual}")] + TooShort { expected: usize, actual: usize }, + #[error("invalid PEC offset: {0} exceeds file length {1}")] + InvalidPecOffset(u32, usize), + #[error("invalid header: {0}")] + InvalidHeader(String), + #[error("no stitch data found")] + NoStitchData, + #[error("empty design: no stitch segments produced")] + EmptyDesign, + #[error("unsupported format")] + UnsupportedFormat, + #[error("render error: {0}")] + Render(String), + #[error("PNG encoding error: {0}")] + PngEncode(#[from] png::EncodingError), +} diff --git a/rustitch/src/exp.rs b/rustitch/src/exp.rs new file mode 100644 index 0000000..6ff0b58 --- /dev/null +++ b/rustitch/src/exp.rs @@ -0,0 +1,135 @@ +use crate::error::Error; +use crate::palette::default_colors; +use crate::types::{ResolvedDesign, StitchCommand}; + +/// Parse an EXP (Melco) file from raw bytes into stitch commands. +/// +/// EXP format: 2 bytes per stitch (signed i8 dx, dy). +/// Escape byte 0x80 followed by a control byte: +/// 0x01 = color change +/// 0x02 = color change (variant) +/// 0x04 = jump (next 2 bytes are jump dx, dy) +/// 0x80 = trim +pub fn parse(data: &[u8]) -> Result, Error> { + if data.len() < 2 { + return Err(Error::TooShort { + expected: 2, + actual: data.len(), + }); + } + + let mut commands = Vec::new(); + let mut i = 0; + + while i + 1 < data.len() { + let b1 = data[i]; + let b2 = data[i + 1]; + + if b1 == 0x80 { + match b2 { + 0x01 | 0x02 => { + commands.push(StitchCommand::ColorChange); + i += 2; + } + 0x80 => { + commands.push(StitchCommand::Trim); + i += 2; + } + 0x04 => { + // Jump: next 2 bytes are the movement + i += 2; + if i + 1 >= data.len() { + break; + } + let dx = data[i] as i8 as i16; + let dy = data[i + 1] as i8 as i16; + commands.push(StitchCommand::Jump { dx, dy }); + i += 2; + } + _ => { + // Unknown escape, skip + i += 2; + } + } + } else { + let dx = b1 as i8 as i16; + let dy = b2 as i8 as i16; + commands.push(StitchCommand::Stitch { dx, dy }); + i += 2; + } + } + + commands.push(StitchCommand::End); + + if commands.len() <= 1 { + return Err(Error::NoStitchData); + } + + Ok(commands) +} + +/// Parse an EXP file and resolve to a renderable design. +pub fn parse_and_resolve(data: &[u8]) -> Result { + let commands = parse(data)?; + let color_count = commands + .iter() + .filter(|c| matches!(c, StitchCommand::ColorChange)) + .count() + + 1; + let colors = default_colors(color_count); + crate::resolve::resolve(&commands, colors) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_simple_stitches() { + let data = [0x0A, 0x14, 0x05, 0x03]; + let cmds = parse(&data).unwrap(); + assert!(matches!( + cmds[0], + StitchCommand::Stitch { dx: 10, dy: 20 } + )); + assert!(matches!(cmds[1], StitchCommand::Stitch { dx: 5, dy: 3 })); + assert!(matches!(cmds[2], StitchCommand::End)); + } + + #[test] + fn parse_negative_coords() { + // -10 as i8 = 0xF6, -20 as i8 = 0xEC + let data = [0xF6, 0xEC]; + let cmds = parse(&data).unwrap(); + assert!(matches!( + cmds[0], + StitchCommand::Stitch { dx: -10, dy: -20 } + )); + } + + #[test] + fn parse_color_change() { + let data = [0x0A, 0x14, 0x80, 0x01, 0x05, 0x03]; + let cmds = parse(&data).unwrap(); + assert!(matches!(cmds[0], StitchCommand::Stitch { .. })); + assert!(matches!(cmds[1], StitchCommand::ColorChange)); + assert!(matches!(cmds[2], StitchCommand::Stitch { dx: 5, dy: 3 })); + } + + #[test] + fn parse_jump() { + let data = [0x80, 0x04, 0x0A, 0x14]; + let cmds = parse(&data).unwrap(); + assert!(matches!( + cmds[0], + StitchCommand::Jump { dx: 10, dy: 20 } + )); + } + + #[test] + fn parse_trim() { + let data = [0x0A, 0x14, 0x80, 0x80, 0x05, 0x03]; + let cmds = parse(&data).unwrap(); + assert!(matches!(cmds[1], StitchCommand::Trim)); + } +} diff --git a/rustitch/src/format.rs b/rustitch/src/format.rs new file mode 100644 index 0000000..a07e429 --- /dev/null +++ b/rustitch/src/format.rs @@ -0,0 +1,34 @@ +use std::path::Path; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Format { + Pes, + Dst, + Exp, + Jef, + Vp3, +} + +/// Detect format from file content (magic bytes). +pub fn detect_from_bytes(data: &[u8]) -> Option { + if data.len() >= 4 && &data[0..4] == b"#PES" { + return Some(Format::Pes); + } + if data.len() >= 5 && &data[0..5] == b"%vsm%" { + return Some(Format::Vp3); + } + None +} + +/// Detect format from file extension. +pub fn detect_from_extension(path: &Path) -> Option { + let ext = path.extension()?.to_str()?; + match ext.to_ascii_lowercase().as_str() { + "pes" => Some(Format::Pes), + "dst" => Some(Format::Dst), + "exp" => Some(Format::Exp), + "jef" => Some(Format::Jef), + "vp3" => Some(Format::Vp3), + _ => None, + } +} diff --git a/rustitch/src/jef/mod.rs b/rustitch/src/jef/mod.rs new file mode 100644 index 0000000..bcedee5 --- /dev/null +++ b/rustitch/src/jef/mod.rs @@ -0,0 +1,173 @@ +mod palette; + +use crate::error::Error; +use crate::types::{ResolvedDesign, StitchCommand}; +use palette::JEF_PALETTE; + +/// Parse a JEF (Janome) file from raw bytes into stitch commands and color info. +/// +/// JEF header layout (little-endian): +/// 0..4: stitch data offset (u32) +/// 4..8: flags/format indicator +/// 24..28: color count (u32) +/// 28..32: stitch count (u32) +/// 116+: color table (each entry: i32 palette index) +/// +/// Stitch data: 2 bytes per stitch (signed i8 dx, dy). +/// Control codes: 0x80 0x01 = color change, 0x80 0x02 = jump, 0x80 0x10 = end. +pub fn parse(data: &[u8]) -> Result<(Vec, Vec<(u8, u8, u8)>), Error> { + if data.len() < 116 { + return Err(Error::TooShort { + expected: 116, + actual: data.len(), + }); + } + + let stitch_offset = read_u32_le(data, 0) as usize; + let color_count = read_u32_le(data, 24) as usize; + + if stitch_offset > data.len() { + return Err(Error::InvalidHeader(format!( + "stitch data offset {} exceeds file length {}", + stitch_offset, + data.len() + ))); + } + + // Read color table starting at offset 116 + let color_table_start = 116; + let mut colors = Vec::with_capacity(color_count); + for i in 0..color_count { + let entry_offset = color_table_start + i * 4; + if entry_offset + 4 > data.len() { + break; + } + let idx = read_i32_le(data, entry_offset); + let palette_idx = if idx >= 0 && (idx as usize) < JEF_PALETTE.len() { + idx as usize + } else { + 0 + }; + colors.push(JEF_PALETTE[palette_idx]); + } + + if colors.is_empty() { + colors.push((0, 0, 0)); + } + + // Parse stitch data + let stitch_data = &data[stitch_offset..]; + let commands = decode_stitches(stitch_data)?; + + Ok((commands, colors)) +} + +fn decode_stitches(data: &[u8]) -> Result, Error> { + let mut commands = Vec::new(); + let mut i = 0; + + while i + 1 < data.len() { + let b1 = data[i]; + let b2 = data[i + 1]; + + if b1 == 0x80 { + match b2 { + 0x01 => { + commands.push(StitchCommand::ColorChange); + i += 2; + } + 0x02 => { + // Jump: next 2 bytes are movement + i += 2; + if i + 1 >= data.len() { + break; + } + let dx = data[i] as i8 as i16; + let dy = -(data[i + 1] as i8 as i16); + commands.push(StitchCommand::Jump { dx, dy }); + i += 2; + } + 0x10 => { + commands.push(StitchCommand::End); + break; + } + _ => { + i += 2; + } + } + } else { + let dx = b1 as i8 as i16; + let dy = -(b2 as i8 as i16); + commands.push(StitchCommand::Stitch { dx, dy }); + i += 2; + } + } + + if commands.is_empty() { + return Err(Error::NoStitchData); + } + + if !matches!(commands.last(), Some(StitchCommand::End)) { + commands.push(StitchCommand::End); + } + + Ok(commands) +} + +/// Parse a JEF file and resolve to a renderable design. +pub fn parse_and_resolve(data: &[u8]) -> Result { + let (commands, colors) = parse(data)?; + crate::resolve::resolve(&commands, colors) +} + +fn read_u32_le(data: &[u8], offset: usize) -> u32 { + u32::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ]) +} + +fn read_i32_le(data: &[u8], offset: usize) -> i32 { + i32::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decode_simple_stitches() { + let data = [0x0A, 0x14, 0x05, 0x03, 0x80, 0x10]; + let cmds = decode_stitches(&data).unwrap(); + assert!(matches!( + cmds[0], + StitchCommand::Stitch { dx: 10, dy: -20 } + )); + assert!(matches!(cmds[1], StitchCommand::Stitch { dx: 5, dy: -3 })); + assert!(matches!(cmds[2], StitchCommand::End)); + } + + #[test] + fn decode_color_change() { + let data = [0x0A, 0x14, 0x80, 0x01, 0x05, 0x03, 0x80, 0x10]; + let cmds = decode_stitches(&data).unwrap(); + assert!(matches!(cmds[1], StitchCommand::ColorChange)); + } + + #[test] + fn decode_jump() { + let data = [0x80, 0x02, 0x0A, 0x14, 0x80, 0x10]; + let cmds = decode_stitches(&data).unwrap(); + assert!(matches!( + cmds[0], + StitchCommand::Jump { dx: 10, dy: -20 } + )); + } +} diff --git a/rustitch/src/jef/palette.rs b/rustitch/src/jef/palette.rs new file mode 100644 index 0000000..0b91fbc --- /dev/null +++ b/rustitch/src/jef/palette.rs @@ -0,0 +1,82 @@ +/// Janome thread color palette (78 entries). +/// Index 0 is a fallback; indices 1-77 correspond to standard Janome thread colors. +pub const JEF_PALETTE: [(u8, u8, u8); 78] = [ + (0, 0, 0), // 0: Unknown / Black + (0, 0, 0), // 1: Black + (255, 255, 255), // 2: White + (255, 255, 23), // 3: Yellow + (250, 160, 96), // 4: Orange + (92, 118, 73), // 5: Olive Green + (64, 192, 48), // 6: Green + (101, 194, 200), // 7: Sky Blue + (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 Blue + (127, 194, 28), // 22: Yellow Green + (185, 185, 185), // 23: Silver Gray + (160, 160, 160), // 24: Gray + (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: Blond + (225, 203, 0), // 37: Sunflower + (225, 173, 212), // 38: Orchid Pink + (195, 0, 126), // 39: Peony + (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 Gray + (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: Mustard + (203, 138, 26), // 69: Yellow Ochre + (198, 170, 66), // 70: Old Gold + (236, 176, 44), // 71: Honeydew + (248, 128, 64), // 72: Tangerine + (255, 229, 5), // 73: Canary Yellow + (250, 122, 122), // 74: Vermilion + (107, 224, 0), // 75: Bright Green + (56, 108, 174), // 76: Ocean Blue + (227, 196, 180), // 77: Beige Gray +]; diff --git a/rustitch/src/lib.rs b/rustitch/src/lib.rs index aa9ad3b..a51b4c2 100644 --- a/rustitch/src/lib.rs +++ b/rustitch/src/lib.rs @@ -1,12 +1,37 @@ -pub mod pes; -mod render; +pub mod error; +pub mod format; +pub mod palette; +pub mod types; +pub mod pes; + +mod render; +mod resolve; + +pub use error::Error; +pub use format::Format; pub use render::render_thumbnail; +pub use types::{BoundingBox, ResolvedDesign, StitchCommand, StitchSegment}; /// Parse a PES file and render a thumbnail PNG of the given size. -pub fn thumbnail(pes_data: &[u8], size: u32) -> Result, pes::Error> { +pub fn thumbnail(pes_data: &[u8], size: u32) -> Result, Error> { let design = pes::parse(pes_data)?; let resolved = pes::resolve(&design)?; - let png_bytes = render::render_thumbnail(&resolved, size)?; - Ok(png_bytes) + render::render_thumbnail(&resolved, size) +} + +/// Parse any supported format and render a thumbnail PNG. +pub fn thumbnail_format(data: &[u8], size: u32, fmt: Format) -> Result, Error> { + let resolved = parse_and_resolve(data, fmt)?; + render::render_thumbnail(&resolved, size) +} + +fn parse_and_resolve(data: &[u8], fmt: Format) -> Result { + match fmt { + Format::Pes => { + let design = pes::parse(data)?; + pes::resolve(&design) + } + _ => Err(Error::UnsupportedFormat), + } } diff --git a/rustitch/src/palette.rs b/rustitch/src/palette.rs new file mode 100644 index 0000000..0c3fd2f --- /dev/null +++ b/rustitch/src/palette.rs @@ -0,0 +1,93 @@ +/// Brother PEC thread color palette (65 entries). +/// Index 0 is a fallback; indices 1-64 correspond to standard Brother thread colors. +pub const PEC_PALETTE: [(u8, u8, u8); 65] = [ + (0, 0, 0), // 0: Unknown + (14, 31, 124), // 1: Prussian Blue + (10, 85, 163), // 2: Blue + (0, 135, 119), // 3: Teal Green + (75, 107, 175), // 4: Cornflower Blue + (237, 23, 31), // 5: Red + (209, 92, 0), // 6: Reddish Brown + (145, 54, 151), // 7: Magenta + (228, 154, 203), // 8: Light Lilac + (145, 95, 172), // 9: Lilac + (158, 214, 125), // 10: Mint Green + (232, 169, 0), // 11: Deep Gold + (254, 186, 53), // 12: Orange + (255, 255, 0), // 13: Yellow + (112, 188, 31), // 14: Lime Green + (186, 152, 0), // 15: Brass + (168, 168, 168), // 16: Silver + (125, 111, 0), // 17: Russet Brown + (255, 255, 179), // 18: Cream Brown + (79, 85, 86), // 19: Pewter + (0, 0, 0), // 20: Black + (11, 61, 145), // 21: Ultramarine + (119, 1, 118), // 22: Royal Purple + (41, 49, 51), // 23: Dark Gray + (42, 19, 1), // 24: Dark Brown + (246, 74, 138), // 25: Deep Rose + (178, 118, 36), // 26: Light Brown + (252, 187, 197), // 27: Salmon Pink + (254, 55, 15), // 28: Vermilion + (240, 240, 240), // 29: White + (106, 28, 138), // 30: Violet + (168, 221, 196), // 31: Seacrest + (37, 132, 187), // 32: Sky Blue + (254, 179, 67), // 33: Pumpkin + (255, 243, 107), // 34: Cream Yellow + (208, 166, 96), // 35: Khaki + (209, 84, 0), // 36: Clay Brown + (102, 186, 73), // 37: Leaf Green + (19, 74, 70), // 38: Peacock Blue + (135, 135, 135), // 39: Gray + (216, 204, 198), // 40: Warm Gray + (67, 86, 7), // 41: Dark Olive + (253, 217, 222), // 42: Flesh Pink + (249, 147, 188), // 43: Pink + (0, 56, 34), // 44: Deep Green + (178, 175, 212), // 45: Lavender + (104, 106, 176), // 46: Wisteria Violet + (239, 227, 185), // 47: Beige + (247, 56, 102), // 48: Carmine + (181, 75, 100), // 49: Amber Red + (19, 43, 26), // 50: Olive Green + (199, 1, 86), // 51: Dark Fuchsia + (254, 158, 50), // 52: Tangerine + (168, 222, 235), // 53: Light Blue + (0, 103, 62), // 54: Emerald Green + (78, 41, 144), // 55: Purple + (47, 126, 32), // 56: Moss Green + (255, 204, 204), // 57: Flesh Pink + (255, 217, 17), // 58: Harvest Gold + (9, 91, 166), // 59: Electric Blue + (240, 249, 112), // 60: Lemon Yellow + (227, 243, 91), // 61: Fresh Green + (255, 153, 0), // 62: Orange + (255, 240, 141), // 63: Cream Yellow + (255, 200, 200), // 64: Applique +]; + +/// Default high-contrast palette for formats without embedded color info (DST, EXP). +/// Colors cycle on each color change. +pub const DEFAULT_PALETTE: [(u8, u8, u8); 12] = [ + (0, 0, 0), // Black + (237, 23, 31), // Red + (10, 85, 163), // Blue + (0, 135, 119), // Teal Green + (254, 186, 53), // Orange + (145, 54, 151), // Magenta + (112, 188, 31), // Lime Green + (42, 19, 1), // Dark Brown + (37, 132, 187), // Sky Blue + (246, 74, 138), // Deep Rose + (186, 152, 0), // Brass + (106, 28, 138), // Violet +]; + +/// 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) + .map(|i| DEFAULT_PALETTE[i % DEFAULT_PALETTE.len()]) + .collect() +} diff --git a/rustitch/src/pes/header.rs b/rustitch/src/pes/header.rs index 60a215e..b9b84dc 100644 --- a/rustitch/src/pes/header.rs +++ b/rustitch/src/pes/header.rs @@ -1,4 +1,4 @@ -use super::Error; +use crate::error::Error; #[derive(Debug)] pub struct PesHeader { @@ -18,7 +18,7 @@ pub fn parse_header(data: &[u8]) -> Result { if magic != b"#PES" { let mut m = [0u8; 4]; m.copy_from_slice(magic); - return Err(Error::InvalidMagic(m)); + return Err(Error::InvalidPesMagic(m)); } let mut version = [0u8; 4]; @@ -41,7 +41,6 @@ mod tests { let mut data = vec![0u8; 20]; data[0..4].copy_from_slice(b"#PES"); data[4..8].copy_from_slice(b"0001"); - // PEC offset = 16 (little-endian) data[8..12].copy_from_slice(&16u32.to_le_bytes()); let header = parse_header(&data).unwrap(); @@ -53,7 +52,7 @@ mod tests { fn reject_invalid_magic() { let data = b"NOTPES0001\x10\x00\x00\x00"; let err = parse_header(data).unwrap_err(); - assert!(matches!(err, Error::InvalidMagic(_))); + assert!(matches!(err, Error::InvalidPesMagic(_))); } #[test] diff --git a/rustitch/src/pes/mod.rs b/rustitch/src/pes/mod.rs index a305109..44e6c37 100644 --- a/rustitch/src/pes/mod.rs +++ b/rustitch/src/pes/mod.rs @@ -1,30 +1,13 @@ mod header; -mod palette; mod pec; pub use header::PesHeader; -pub use palette::PEC_PALETTE; -pub use pec::{PecHeader, StitchCommand}; +pub use pec::PecHeader; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum Error { - #[error("invalid PES magic: expected #PES, got {0:?}")] - InvalidMagic([u8; 4]), - #[error("file too short: need {expected} bytes, got {actual}")] - TooShort { expected: usize, actual: usize }, - #[error("invalid PEC offset: {0} exceeds file length {1}")] - InvalidPecOffset(u32, usize), - #[error("no stitch data found")] - NoStitchData, - #[error("empty design: no stitch segments produced")] - EmptyDesign, - #[error("render error: {0}")] - Render(String), - #[error("PNG encoding error: {0}")] - PngEncode(#[from] png::EncodingError), -} +// Re-export shared types for backward compatibility +pub use crate::error::Error; +pub use crate::palette::PEC_PALETTE; +pub use crate::types::{BoundingBox, ResolvedDesign, StitchCommand, StitchSegment}; pub struct PesDesign { pub header: PesHeader, @@ -32,27 +15,6 @@ pub struct PesDesign { pub commands: Vec, } -pub struct StitchSegment { - pub x0: f32, - pub y0: f32, - pub x1: f32, - pub y1: f32, - pub color_index: usize, -} - -pub struct BoundingBox { - pub min_x: f32, - pub max_x: f32, - pub min_y: f32, - pub max_y: f32, -} - -pub struct ResolvedDesign { - pub segments: Vec, - pub colors: Vec<(u8, u8, u8)>, - pub bounds: BoundingBox, -} - /// Parse a PES file from raw bytes. pub fn parse(data: &[u8]) -> Result { let header = header::parse_header(data)?; @@ -73,66 +35,8 @@ pub fn parse(data: &[u8]) -> Result { }) } -/// Convert parsed commands into renderable segments with absolute coordinates. +/// Convert parsed PES design into renderable segments with absolute coordinates. pub fn resolve(design: &PesDesign) -> Result { - let mut segments = Vec::new(); - let mut x: f32 = 0.0; - let mut y: f32 = 0.0; - let mut color_idx: usize = 0; - let mut pen_down = true; - - for cmd in &design.commands { - match cmd { - StitchCommand::Stitch { dx, dy } => { - let nx = x + *dx as f32; - let ny = y + *dy as f32; - if pen_down { - segments.push(StitchSegment { - x0: x, - y0: y, - x1: nx, - y1: ny, - color_index: color_idx, - }); - } - x = nx; - y = ny; - pen_down = true; - } - StitchCommand::Jump { dx, dy } => { - x += *dx as f32; - y += *dy as f32; - pen_down = false; - } - StitchCommand::Trim => { - pen_down = false; - } - StitchCommand::ColorChange => { - color_idx += 1; - pen_down = false; - } - StitchCommand::End => break, - } - } - - if segments.is_empty() { - return Err(Error::EmptyDesign); - } - - // Compute bounding box - let mut min_x = f32::MAX; - let mut max_x = f32::MIN; - let mut min_y = f32::MAX; - let mut max_y = f32::MIN; - - for seg in &segments { - min_x = min_x.min(seg.x0).min(seg.x1); - max_x = max_x.max(seg.x0).max(seg.x1); - min_y = min_y.min(seg.y0).min(seg.y1); - max_y = max_y.max(seg.y0).max(seg.y1); - } - - // Resolve colors from palette indices let colors: Vec<(u8, u8, u8)> = design .pec_header .color_indices @@ -143,14 +47,5 @@ pub fn resolve(design: &PesDesign) -> Result { }) .collect(); - Ok(ResolvedDesign { - segments, - colors, - bounds: BoundingBox { - min_x, - max_x, - min_y, - max_y, - }, - }) + crate::resolve::resolve(&design.commands, colors) } diff --git a/rustitch/src/pes/palette.rs b/rustitch/src/pes/palette.rs index ea6002b..423c1bc 100644 --- a/rustitch/src/pes/palette.rs +++ b/rustitch/src/pes/palette.rs @@ -1,69 +1,2 @@ -/// Brother PEC thread color palette (65 entries). -/// Index 0 is a fallback; indices 1–64 correspond to standard Brother thread colors. -pub const PEC_PALETTE: [(u8, u8, u8); 65] = [ - (0, 0, 0), // 0: Unknown - (14, 31, 124), // 1: Prussian Blue - (10, 85, 163), // 2: Blue - (0, 135, 119), // 3: Teal Green - (75, 107, 175), // 4: Cornflower Blue - (237, 23, 31), // 5: Red - (209, 92, 0), // 6: Reddish Brown - (145, 54, 151), // 7: Magenta - (228, 154, 203), // 8: Light Lilac - (145, 95, 172), // 9: Lilac - (158, 214, 125), // 10: Mint Green - (232, 169, 0), // 11: Deep Gold - (254, 186, 53), // 12: Orange - (255, 255, 0), // 13: Yellow - (112, 188, 31), // 14: Lime Green - (186, 152, 0), // 15: Brass - (168, 168, 168), // 16: Silver - (125, 111, 0), // 17: Russet Brown - (255, 255, 179), // 18: Cream Brown - (79, 85, 86), // 19: Pewter - (0, 0, 0), // 20: Black - (11, 61, 145), // 21: Ultramarine - (119, 1, 118), // 22: Royal Purple - (41, 49, 51), // 23: Dark Gray - (42, 19, 1), // 24: Dark Brown - (246, 74, 138), // 25: Deep Rose - (178, 118, 36), // 26: Light Brown - (252, 187, 197), // 27: Salmon Pink - (254, 55, 15), // 28: Vermilion - (240, 240, 240), // 29: White - (106, 28, 138), // 30: Violet - (168, 221, 196), // 31: Seacrest - (37, 132, 187), // 32: Sky Blue - (254, 179, 67), // 33: Pumpkin - (255, 243, 107), // 34: Cream Yellow - (208, 166, 96), // 35: Khaki - (209, 84, 0), // 36: Clay Brown - (102, 186, 73), // 37: Leaf Green - (19, 74, 70), // 38: Peacock Blue - (135, 135, 135), // 39: Gray - (216, 204, 198), // 40: Warm Gray - (67, 86, 7), // 41: Dark Olive - (253, 217, 222), // 42: Flesh Pink - (249, 147, 188), // 43: Pink - (0, 56, 34), // 44: Deep Green - (178, 175, 212), // 45: Lavender - (104, 106, 176), // 46: Wisteria Violet - (239, 227, 185), // 47: Beige - (247, 56, 102), // 48: Carmine - (181, 75, 100), // 49: Amber Red - (19, 43, 26), // 50: Olive Green - (199, 1, 86), // 51: Dark Fuchsia - (254, 158, 50), // 52: Tangerine - (168, 222, 235), // 53: Light Blue - (0, 103, 62), // 54: Emerald Green - (78, 41, 144), // 55: Purple - (47, 126, 32), // 56: Moss Green - (255, 204, 204), // 57: Flesh Pink - (255, 217, 17), // 58: Harvest Gold - (9, 91, 166), // 59: Electric Blue - (240, 249, 112), // 60: Lemon Yellow - (227, 243, 91), // 61: Fresh Green - (255, 153, 0), // 62: Orange - (255, 240, 141), // 63: Cream Yellow - (255, 200, 200), // 64: Applique -]; +// Re-export from crate root for backward compatibility +pub use crate::palette::PEC_PALETTE; diff --git a/rustitch/src/pes/pec.rs b/rustitch/src/pes/pec.rs index b3c95d1..25cf2dd 100644 --- a/rustitch/src/pes/pec.rs +++ b/rustitch/src/pes/pec.rs @@ -1,4 +1,5 @@ -use super::Error; +use crate::error::Error; +use crate::types::StitchCommand; pub struct PecHeader { pub label: String, @@ -6,19 +7,9 @@ pub struct PecHeader { pub color_indices: Vec, } -#[derive(Debug, Clone)] -pub enum StitchCommand { - Stitch { dx: i16, dy: i16 }, - Jump { dx: i16, dy: i16 }, - Trim, - ColorChange, - End, -} - /// Parse the PEC header starting at the PEC section offset. /// Returns the header and the byte offset (relative to pec_data start) where stitch data begins. pub fn parse_pec_header(pec_data: &[u8]) -> Result<(PecHeader, usize), Error> { - // PEC section starts with "LA:" label field (19 bytes total) if pec_data.len() < 532 { return Err(Error::TooShort { expected: 532, @@ -26,57 +17,15 @@ pub fn parse_pec_header(pec_data: &[u8]) -> Result<(PecHeader, usize), Error> { }); } - // Label: bytes 0..19, starts with "LA:" let label_raw = &pec_data[3..19]; let label = std::str::from_utf8(label_raw) .unwrap_or("") .trim() .to_string(); - // Color count at offset 48 from PEC start let color_count = pec_data[48] + 1; - - // Color indices follow at offset 49 let color_indices: Vec = pec_data[49..49 + color_count as usize].to_vec(); - // Stitch data starts at offset 532 from PEC section start - // (48 bytes header + 463 bytes padding/thumbnail = 512, plus 20 bytes of graphic data = 532) - // Actually the standard offset is 512 + the two thumbnail sections. - // The typical approach: skip to offset 48 + 1 + color_count + padding to 512, then skip thumbnails. - // Simplified: PEC stitch data offset = 512 + 20 (for the stitch data header that contains the graphic offsets) - // A more robust approach: read the stitch data offset from the header. - - // At PEC offset + 20..24 there are two u16 LE values for the thumbnail image offsets. - // The stitch data typically starts after a fixed 532-byte header region. - // Let's use the more standard approach from libpes: - // Offset 514-515 (relative to PEC start): thumbnail1 image offset (u16 LE, relative) - // But the simplest reliable approach is to find the stitch data after the fixed header. - - // The standard PEC header is 512 bytes, followed by two thumbnail images. - // Thumbnail 1: 6 bytes wide × 38 bytes high = 228 bytes (48×38 pixel, 1bpp padded) - // Actually, typical PEC has: after the 512-byte block, there are two graphics sections. - // The stitch data starts after those graphics. - // - // More robust: bytes 514..516 give the thumbnail offset (little-endian u16). - // We can derive stitch data from there, but let's use the standard fixed sizes. - // Thumbnail 1: at offset 512, size = ceil(width*2/8) * height, with default 48×38 = 6*38=228 - // Thumbnail 2: at offset 512+228=740, size = ceil(width*2/8) * height, default 96×76=12*76=912 - // Stitch data at: 512 + 228 + 912 = 1652? That doesn't seem right. - // - // Actually from libpes wiki: PEC header is 20 bytes, then color info, then padding to - // reach a 512-byte boundary. At byte 512 is the beginning of the PEC graphic section. - // After the graphics come the stitch data. But graphic sizes vary. - // - // The correct approach: at PEC_start + 514 (bytes 514-515), read a u16 LE which gives - // the absolute offset from PEC_start to the first thumbnail. Then after thumbnails come stitches. - // BUT actually, the standard approach used by most parsers is simpler: - // - // pyembroidery approach: seek to PEC_start + 532, that's where stitch data starts. - // The 532 = 512 + 20 (20 bytes for graphic header). - // - // Let's verify: pyembroidery's PecReader reads stitches starting 532 bytes after PEC start. - // Let's go with 532. - let stitch_data_offset = 532; if pec_data.len() <= stitch_data_offset { @@ -101,31 +50,26 @@ pub fn decode_stitches(data: &[u8]) -> Result, Error> { while i < data.len() { let b1 = data[i]; - // End marker if b1 == 0xFF { commands.push(StitchCommand::End); break; } - // Color change if b1 == 0xFE { commands.push(StitchCommand::ColorChange); - i += 2; // skip the 0xFE and the following byte (typically 0xB0) + i += 2; continue; } - // Parse dx let (dx, dx_flags, bytes_dx) = decode_coordinate(data, i)?; i += bytes_dx; - // Parse dy let (dy, dy_flags, bytes_dy) = decode_coordinate(data, i)?; i += bytes_dy; let flags = dx_flags | dy_flags; if flags & 0x20 != 0 { - // Trim + jump commands.push(StitchCommand::Trim); commands.push(StitchCommand::Jump { dx, dy }); } else if flags & 0x10 != 0 { @@ -155,7 +99,6 @@ fn decode_coordinate(data: &[u8], pos: usize) -> Result<(i16, u8, usize), Error> let b = data[pos]; if b & 0x80 != 0 { - // Extended 12-bit encoding (2 bytes) if pos + 1 >= data.len() { return Err(Error::TooShort { expected: pos + 2, @@ -163,7 +106,7 @@ fn decode_coordinate(data: &[u8], pos: usize) -> Result<(i16, u8, usize), Error> }); } let b2 = data[pos + 1]; - let flags = b & 0x70; // bits 6-4 for jump/trim flags + let flags = b & 0x70; let raw = (((b & 0x0F) as u16) << 8) | (b2 as u16); let value = if raw > 0x7FF { raw as i16 - 0x1000 @@ -172,7 +115,6 @@ fn decode_coordinate(data: &[u8], pos: usize) -> Result<(i16, u8, usize), Error> }; Ok((value, flags, 2)) } else { - // 7-bit encoding (1 byte) let value = if b > 0x3F { b as i16 - 0x80 } else { b as i16 }; Ok((value, 0, 1)) } @@ -192,7 +134,6 @@ mod tests { #[test] fn decode_simple_stitch() { - // dx=10 (0x0A), dy=20 (0x14), then end let data = [0x0A, 0x14, 0xFF]; let cmds = decode_stitches(&data).unwrap(); assert_eq!(cmds.len(), 2); @@ -207,7 +148,6 @@ mod tests { #[test] fn decode_negative_7bit() { - // dx=0x50 (80 decimal, > 0x3F so value = 80-128 = -48), dy=0x60 (96-128=-32), end let data = [0x50, 0x60, 0xFF]; let cmds = decode_stitches(&data).unwrap(); match &cmds[0] { @@ -229,9 +169,6 @@ mod tests { #[test] fn decode_extended_12bit() { - // Extended dx: high bit set, flags=0x10 (jump), value = 0x100 = 256 - // byte1 = 0x80 | 0x10 | 0x01 = 0x91, byte2 = 0x00 -> raw = 0x100 = 256 - // dy: simple 0x05 = 5 let data = [0x91, 0x00, 0x05, 0xFF]; let cmds = decode_stitches(&data).unwrap(); assert!(matches!(cmds[0], StitchCommand::Jump { dx: 256, dy: 5 })); @@ -239,8 +176,6 @@ mod tests { #[test] fn decode_trim_jump() { - // dx with trim flag (0x20): byte1 = 0x80 | 0x20 | 0x00 = 0xA0, byte2 = 0x0A -> raw=10 - // dy: simple 0x05 let data = [0xA0, 0x0A, 0x05, 0xFF]; let cmds = decode_stitches(&data).unwrap(); assert!(matches!(cmds[0], StitchCommand::Trim)); diff --git a/rustitch/src/render.rs b/rustitch/src/render.rs index a6621a0..f2e59f8 100644 --- a/rustitch/src/render.rs +++ b/rustitch/src/render.rs @@ -1,6 +1,7 @@ use tiny_skia::{LineCap, Paint, PathBuilder, Pixmap, Stroke, Transform}; -use crate::pes::{Error, ResolvedDesign}; +use crate::error::Error; +use crate::types::ResolvedDesign; /// Render a resolved embroidery design to a PNG image of the given size. pub fn render_thumbnail(design: &ResolvedDesign, size: u32) -> Result, Error> { @@ -23,7 +24,6 @@ pub fn render_thumbnail(design: &ResolvedDesign, size: u32) -> Result, E let line_width = (scale * 0.3).max(1.0); - // Group segments by color index and draw each group let max_color = design .segments .iter() @@ -82,7 +82,6 @@ fn encode_png(pixmap: &Pixmap) -> Result, Error> { let height = pixmap.height(); let src = pixmap.data(); - // Unpremultiply alpha let mut data = Vec::with_capacity(src.len()); for chunk in src.chunks_exact(4) { let (r, g, b, a) = (chunk[0], chunk[1], chunk[2], chunk[3]); diff --git a/rustitch/src/resolve.rs b/rustitch/src/resolve.rs new file mode 100644 index 0000000..bd4371f --- /dev/null +++ b/rustitch/src/resolve.rs @@ -0,0 +1,75 @@ +use crate::error::Error; +use crate::types::{BoundingBox, ResolvedDesign, StitchCommand, StitchSegment}; + +/// Convert parsed stitch commands into renderable segments with absolute coordinates. +pub fn resolve( + commands: &[StitchCommand], + colors: Vec<(u8, u8, u8)>, +) -> Result { + let mut segments = Vec::new(); + let mut x: f32 = 0.0; + let mut y: f32 = 0.0; + let mut color_idx: usize = 0; + let mut pen_down = true; + + for cmd in commands { + match cmd { + StitchCommand::Stitch { dx, dy } => { + let nx = x + *dx as f32; + let ny = y + *dy as f32; + if pen_down { + segments.push(StitchSegment { + x0: x, + y0: y, + x1: nx, + y1: ny, + color_index: color_idx, + }); + } + x = nx; + y = ny; + pen_down = true; + } + StitchCommand::Jump { dx, dy } => { + x += *dx as f32; + y += *dy as f32; + pen_down = false; + } + StitchCommand::Trim => { + pen_down = false; + } + StitchCommand::ColorChange => { + color_idx += 1; + pen_down = false; + } + StitchCommand::End => break, + } + } + + if segments.is_empty() { + return Err(Error::EmptyDesign); + } + + let mut min_x = f32::MAX; + let mut max_x = f32::MIN; + let mut min_y = f32::MAX; + let mut max_y = f32::MIN; + + for seg in &segments { + min_x = min_x.min(seg.x0).min(seg.x1); + max_x = max_x.max(seg.x0).max(seg.x1); + min_y = min_y.min(seg.y0).min(seg.y1); + max_y = max_y.max(seg.y0).max(seg.y1); + } + + Ok(ResolvedDesign { + segments, + colors, + bounds: BoundingBox { + min_x, + max_x, + min_y, + max_y, + }, + }) +} diff --git a/rustitch/src/types.rs b/rustitch/src/types.rs new file mode 100644 index 0000000..bb8face --- /dev/null +++ b/rustitch/src/types.rs @@ -0,0 +1,29 @@ +#[derive(Debug, Clone)] +pub enum StitchCommand { + Stitch { dx: i16, dy: i16 }, + Jump { dx: i16, dy: i16 }, + Trim, + ColorChange, + End, +} + +pub struct StitchSegment { + pub x0: f32, + pub y0: f32, + pub x1: f32, + pub y1: f32, + pub color_index: usize, +} + +pub struct BoundingBox { + pub min_x: f32, + pub max_x: f32, + pub min_y: f32, + pub max_y: f32, +} + +pub struct ResolvedDesign { + pub segments: Vec, + pub colors: Vec<(u8, u8, u8)>, + pub bounds: BoundingBox, +} diff --git a/rustitch/src/vp3.rs b/rustitch/src/vp3.rs new file mode 100644 index 0000000..902c02a --- /dev/null +++ b/rustitch/src/vp3.rs @@ -0,0 +1,343 @@ +use crate::error::Error; +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 +/// +/// 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). +pub fn parse(data: &[u8]) -> Result<(Vec, Vec<(u8, u8, u8)>), Error> { + if data.len() < 20 { + return Err(Error::TooShort { + expected: 20, + actual: data.len(), + }); + } + + let mut reader = Reader::new(data); + + // 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 mut colors = Vec::new(); + let mut commands = Vec::new(); + + // 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)?; + colors.push(color); + } + + if commands.is_empty() { + return Err(Error::NoStitchData); + } + + if !matches!(commands.last(), Some(StitchCommand::End)) { + commands.push(StitchCommand::End); + } + + Ok((commands, colors)) +} + +/// Parse a VP3 file and resolve to a renderable design. +pub fn parse_and_resolve(data: &[u8]) -> Result { + let (commands, colors) = parse(data)?; + 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(), + }); + } + + // 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 { + if len < 1000 && (len as usize) + 2 <= reader.remaining() { + skip_string(reader)?; + } + } + } + + Ok(()) +} + +fn read_color_section( + reader: &mut Reader, + commands: &mut Vec, +) -> Result<(u8, u8, u8), Error> { + // Color change between sections (except first) + if !commands.is_empty() { + 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(), + }); + } + + // 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 { + 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)) +} + +fn decode_vp3_stitches(reader: &mut Reader, commands: &mut Vec, end: usize) { + while reader.pos < end && reader.remaining() >= 2 { + let b1 = reader.data[reader.pos]; + + // Check for 3-byte extended coordinates (high bit set on first byte) + if b1 & 0x80 != 0 { + if reader.remaining() < 4 { + break; + } + 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 { + commands.push(StitchCommand::Stitch { dx, dy }); + } + } + } +} + +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!( + "string length {} exceeds remaining data {}", + len, + reader.remaining() + ))); + } + reader.skip(len)?; + Ok(()) +} + +fn read_i16_be(data: &[u8], pos: usize) -> i16 { + i16::from_be_bytes([data[pos], data[pos + 1]]) +} + +struct Reader<'a> { + data: &'a [u8], + pos: usize, +} + +impl<'a> Reader<'a> { + fn new(data: &'a [u8]) -> Self { + Self { data, pos: 0 } + } + + fn remaining(&self) -> usize { + self.data.len().saturating_sub(self.pos) + } + + fn read_u8(&mut self) -> Result { + if self.pos >= self.data.len() { + return Err(Error::TooShort { + expected: self.pos + 1, + actual: self.data.len(), + }); + } + let v = self.data[self.pos]; + self.pos += 1; + Ok(v) + } + + fn read_u16_be(&mut self) -> Result { + if self.pos + 2 > self.data.len() { + return Err(Error::TooShort { + expected: self.pos + 2, + actual: self.data.len(), + }); + } + let v = u16::from_be_bytes([self.data[self.pos], self.data[self.pos + 1]]); + self.pos += 2; + Ok(v) + } + + fn peek_u16_be(&self) -> Result { + 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 { + if self.pos + 4 > self.data.len() { + return Err(Error::TooShort { + expected: self.pos + 4, + actual: self.data.len(), + }); + } + let v = u32::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 { + expected: self.pos + n, + actual: self.data.len(), + }); + } + self.pos += n; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[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()); + assert_eq!(commands.len(), 2); + assert!(matches!( + commands[0], + StitchCommand::Stitch { dx: 10, dy: -20 } + )); + } + + #[test] + fn decode_large_jump() { + 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 mut reader = Reader::new(&data); + decode_vp3_stitches(&mut reader, &mut commands, data.len()); + assert_eq!(commands.len(), 1); + assert!(matches!(commands[0], StitchCommand::Jump { .. })); + } +}