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: 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; } 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 flags & 0x80 != 0 { commands.push(StitchCommand::ColorChange); continue; } // Jump: byte 2 bit 6 if flags & 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() { // DST EOF = 0x00, 0x00, 0xF3 let data = [0x00, 0x00, 0xF3]; 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=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)); } #[test] fn decode_jump() { // 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, 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)); } #[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 + 3 + 27 + 81); // 121 } #[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); } }