rustitch 0.2.2

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