rustitch 0.2.2

PES embroidery file parser and thumbnail renderer
Documentation
use crate::error::Error;
use crate::types::{ResolvedDesign, StitchCommand};

/// Parse a VP3 (Pfaff/Viking) file from raw bytes.
///
/// VP3 is a hierarchical format:
///   - `%vsm%` magic + null byte
///   - UTF-16 BE producer string
///   - Design metadata (including center coordinates)
///   - `xxPP` section marker
///   - Producer string (again)
///   - Color count
///   - Color blocks, each containing:
///       - 3-byte marker `\x00\x05\x00`
///       - 4-byte block size (u32 BE)
///       - Start position (2 × i32 BE, units ÷ 100, Y negated)
///       - Thread info (RGB, catalog, name, brand)
///       - 15 bytes metadata + 3 bytes preamble (`\x0A\xF6\x00`)
///       - Stitch data
///
/// Stitch encoding: 2-byte signed pairs (i8 dx, i8 dy).
/// Escape byte 0x80: next byte is sub-command:
///   - 0x01: extended move (2 × i16 BE dx, dy), followed by 2 bytes to skip
///   - 0x03: trim
type ParseResult = Result<(Vec<StitchCommand>, Vec<(u8, u8, u8)>), Error>;

pub fn parse(data: &[u8]) -> ParseResult {
    if data.len() < 20 {
        return Err(Error::TooShort {
            expected: 20,
            actual: data.len(),
        });
    }

    if &data[0..5] != b"%vsm%" {
        return Err(Error::InvalidHeader("missing %vsm% magic".into()));
    }

    let xxpp_pos = find_marker(data, b"xxPP")
        .ok_or_else(|| Error::InvalidHeader("missing xxPP section marker".into()))?;

    let mut reader = Reader::new(data);
    reader.pos = xxpp_pos + 4;

    // Skip 2 bytes + producer string after xxPP
    reader.skip(2)?;
    skip_string(&mut reader)?;

    let color_count = reader.read_u16_be()? as usize;

    let mut colors = Vec::new();
    let mut commands = Vec::new();
    let mut cursor = (0i32, 0i32);

    for ci in 0..color_count {
        let color = read_color_block(&mut reader, &mut commands, &mut cursor, ci > 0)?;
        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<ResolvedDesign, Error> {
    let (commands, colors) = parse(data)?;
    crate::resolve::resolve(&commands, colors)
}

fn find_marker(data: &[u8], marker: &[u8]) -> Option<usize> {
    data.windows(marker.len()).position(|w| w == marker)
}

fn read_color_block(
    reader: &mut Reader,
    commands: &mut Vec<StitchCommand>,
    cursor: &mut (i32, i32),
    add_color_change: bool,
) -> Result<(u8, u8, u8), Error> {
    // 3-byte marker: \x00\x05\x00
    reader.skip(3)?;

    // 4-byte block size (distance to next block from current position)
    let block_size = reader.read_u32_be()? as usize;
    let block_end = reader.pos + block_size;

    // Start position: i32 BE x, i32 BE y (units ÷ 100, Y negated)
    let start_x_raw = reader.read_i32_be()?;
    let start_y_raw = reader.read_i32_be()?;
    let start_x = start_x_raw / 100;
    let start_y = -(start_y_raw / 100);

    // Jump to section start position if cursor is not already there
    let jump_dx = start_x - cursor.0;
    let jump_dy = start_y - cursor.1;
    if jump_dx != 0 || jump_dy != 0 {
        commands.push(StitchCommand::Trim);
        commands.push(StitchCommand::Jump {
            dx: jump_dx.clamp(-32768, 32767) as i16,
            dy: jump_dy.clamp(-32768, 32767) as i16,
        });
        cursor.0 = start_x;
        cursor.1 = start_y;
    }

    if add_color_change {
        commands.push(StitchCommand::ColorChange);
    }

    // Read thread info
    let (r, g, b) = read_thread_info(reader)?;

    // Skip 15 bytes post-thread metadata + 3 bytes preamble (\x0A\xF6\x00)
    reader.skip(18)?;

    // Decode stitches until block end
    decode_vp3_stitches(reader, commands, block_end, cursor);

    reader.pos = block_end;

    Ok((r, g, b))
}

fn read_thread_info(reader: &mut Reader) -> Result<(u8, u8, u8), Error> {
    // Color table: count of sub-colors, transition byte
    let colors_count = reader.read_u8()?;
    let _transition = reader.read_u8()?;

    let mut r = 0u8;
    let mut g = 0u8;
    let mut b = 0u8;

    for _ in 0..colors_count {
        r = reader.read_u8()?;
        g = reader.read_u8()?;
        b = reader.read_u8()?;
        let _parts = reader.read_u8()?;
        let _color_length = reader.read_u16_be()?;
    }

    // Thread type + weight
    reader.skip(2)?;

    // 3 strings: catalog number, color name, brand name
    skip_string(reader)?;
    skip_string(reader)?;
    skip_string(reader)?;

    Ok((r, g, b))
}

fn decode_vp3_stitches(
    reader: &mut Reader,
    commands: &mut Vec<StitchCommand>,
    end: usize,
    cursor: &mut (i32, i32),
) {
    while reader.pos + 1 < end && reader.pos + 1 < reader.data.len() {
        let bx = reader.data[reader.pos] as i8;
        let by = reader.data[reader.pos + 1] as i8;
        reader.pos += 2;

        if (bx as u8) != 0x80 {
            // Normal stitch
            let dx = bx as i16;
            let dy = by as i16;
            cursor.0 += dx as i32;
            cursor.1 += dy as i32;
            commands.push(StitchCommand::Stitch { dx, dy });
            continue;
        }

        // Escape byte 0x80 — check sub-command
        match by as u8 {
            0x01 => {
                // Extended move: 2 × i16 BE
                if reader.pos + 4 <= end {
                    let dx = read_i16_be(reader.data, reader.pos);
                    reader.pos += 2;
                    let dy = read_i16_be(reader.data, reader.pos);
                    reader.pos += 2;
                    cursor.0 += dx as i32;
                    cursor.1 += dy as i32;
                    commands.push(StitchCommand::Stitch { dx, dy });
                    // Skip trailing 0x80 0x02
                    if reader.pos + 2 <= end {
                        reader.pos += 2;
                    }
                }
            }
            0x03 => {
                // Trim
                commands.push(StitchCommand::Trim);
            }
            _ => {
                // Unknown or no-op (0x00, 0x02, etc.)
            }
        }
    }
}

fn skip_string(reader: &mut Reader) -> Result<(), Error> {
    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<u8, Error> {
        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<u16, Error> {
        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 read_u32_be(&mut self) -> Result<u32, Error> {
        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 read_i32_be(&mut self) -> Result<i32, Error> {
        if self.pos + 4 > self.data.len() {
            return Err(Error::TooShort {
                expected: self.pos + 4,
                actual: self.data.len(),
            });
        }
        let v = i32::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();
        let data = [0x0A, 0x14, 0x05, 0x03];
        let mut reader = Reader::new(&data);
        let mut cursor = (0i32, 0i32);
        decode_vp3_stitches(&mut reader, &mut commands, data.len(), &mut cursor);
        assert_eq!(commands.len(), 2);
        assert!(matches!(
            commands[0],
            StitchCommand::Stitch { dx: 10, dy: 20 }
        ));
    }

    #[test]
    fn decode_escape_trim() {
        let mut commands = Vec::new();
        let data = [0x80, 0x03, 0x05, 0x03];
        let mut reader = Reader::new(&data);
        let mut cursor = (0i32, 0i32);
        decode_vp3_stitches(&mut reader, &mut commands, data.len(), &mut cursor);
        assert_eq!(commands.len(), 2);
        assert!(matches!(commands[0], StitchCommand::Trim));
        assert!(matches!(
            commands[1],
            StitchCommand::Stitch { dx: 5, dy: 3 }
        ));
    }

    #[test]
    fn decode_extended_move() {
        // 0x80 0x01 + i16 BE dx(0x0100=256) + i16 BE dy(0xFF00=-256) + 0x80 0x02
        let data = [0x80, 0x01, 0x01, 0x00, 0xFF, 0x00, 0x80, 0x02];
        let mut commands = Vec::new();
        let mut reader = Reader::new(&data);
        let mut cursor = (0i32, 0i32);
        decode_vp3_stitches(&mut reader, &mut commands, data.len(), &mut cursor);
        assert_eq!(commands.len(), 1);
        assert!(matches!(
            commands[0],
            StitchCommand::Stitch { dx: 256, dy: -256 }
        ));
    }
}