rustitch 0.2.2

PES embroidery file parser and thumbnail renderer
Documentation
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);
    }
}