226 lines
6.0 KiB
Rust
226 lines
6.0 KiB
Rust
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<Vec<StitchCommand>, 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<ResolvedDesign, Error> {
|
|
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);
|
|
}
|
|
}
|