rustitch 0.1.2

PES embroidery file parser and thumbnail renderer
Documentation
use super::Error;

pub struct PecHeader {
    pub label: String,
    pub color_count: u8,
    pub color_indices: Vec<u8>,
}

#[derive(Debug, Clone)]
pub enum StitchCommand {
    Stitch { dx: i16, dy: i16 },
    Jump { dx: i16, dy: i16 },
    Trim,
    ColorChange,
    End,
}

/// Parse the PEC header starting at the PEC section offset.
/// Returns the header and the byte offset (relative to pec_data start) where stitch data begins.
pub fn parse_pec_header(pec_data: &[u8]) -> Result<(PecHeader, usize), Error> {
    // PEC section starts with "LA:" label field (19 bytes total)
    if pec_data.len() < 532 {
        return Err(Error::TooShort {
            expected: 532,
            actual: pec_data.len(),
        });
    }

    // Label: bytes 0..19, starts with "LA:"
    let label_raw = &pec_data[3..19];
    let label = std::str::from_utf8(label_raw)
        .unwrap_or("")
        .trim()
        .to_string();

    // Color count at offset 48 from PEC start
    let color_count = pec_data[48] + 1;

    // Color indices follow at offset 49
    let color_indices: Vec<u8> = pec_data[49..49 + color_count as usize].to_vec();

    // Stitch data starts at offset 532 from PEC section start
    // (48 bytes header + 463 bytes padding/thumbnail = 512, plus 20 bytes of graphic data = 532)
    // Actually the standard offset is 512 + the two thumbnail sections.
    // The typical approach: skip to offset 48 + 1 + color_count + padding to 512, then skip thumbnails.
    // Simplified: PEC stitch data offset = 512 + 20 (for the stitch data header that contains the graphic offsets)
    // A more robust approach: read the stitch data offset from the header.

    // At PEC offset + 20..24 there are two u16 LE values for the thumbnail image offsets.
    // The stitch data typically starts after a fixed 532-byte header region.
    // Let's use the more standard approach from libpes:
    // Offset 514-515 (relative to PEC start): thumbnail1 image offset (u16 LE, relative)
    // But the simplest reliable approach is to find the stitch data after the fixed header.

    // The standard PEC header is 512 bytes, followed by two thumbnail images.
    // Thumbnail 1: 6 bytes wide × 38 bytes high = 228 bytes (48×38 pixel, 1bpp padded)
    // Actually, typical PEC has: after the 512-byte block, there are two graphics sections.
    // The stitch data starts after those graphics.
    //
    // More robust: bytes 514..516 give the thumbnail offset (little-endian u16).
    // We can derive stitch data from there, but let's use the standard fixed sizes.
    // Thumbnail 1: at offset 512, size = ceil(width*2/8) * height, with default 48×38 = 6*38=228
    // Thumbnail 2: at offset 512+228=740, size = ceil(width*2/8) * height, default 96×76=12*76=912
    // Stitch data at: 512 + 228 + 912 = 1652? That doesn't seem right.
    //
    // Actually from libpes wiki: PEC header is 20 bytes, then color info, then padding to
    // reach a 512-byte boundary. At byte 512 is the beginning of the PEC graphic section.
    // After the graphics come the stitch data. But graphic sizes vary.
    //
    // The correct approach: at PEC_start + 514 (bytes 514-515), read a u16 LE which gives
    // the absolute offset from PEC_start to the first thumbnail. Then after thumbnails come stitches.
    // BUT actually, the standard approach used by most parsers is simpler:
    //
    // pyembroidery approach: seek to PEC_start + 532, that's where stitch data starts.
    // The 532 = 512 + 20 (20 bytes for graphic header).
    //
    // Let's verify: pyembroidery's PecReader reads stitches starting 532 bytes after PEC start.
    // Let's go with 532.

    let stitch_data_offset = 532;

    if pec_data.len() <= stitch_data_offset {
        return Err(Error::NoStitchData);
    }

    Ok((
        PecHeader {
            label,
            color_count,
            color_indices,
        },
        stitch_data_offset,
    ))
}

/// Decode PEC stitch byte stream into a list of commands.
pub fn decode_stitches(data: &[u8]) -> Result<Vec<StitchCommand>, Error> {
    let mut commands = Vec::new();
    let mut i = 0;

    while i < data.len() {
        let b1 = data[i];

        // End marker
        if b1 == 0xFF {
            commands.push(StitchCommand::End);
            break;
        }

        // Color change
        if b1 == 0xFE {
            commands.push(StitchCommand::ColorChange);
            i += 2; // skip the 0xFE and the following byte (typically 0xB0)
            continue;
        }

        // Parse dx
        let (dx, dx_flags, bytes_dx) = decode_coordinate(data, i)?;
        i += bytes_dx;

        // Parse dy
        let (dy, dy_flags, bytes_dy) = decode_coordinate(data, i)?;
        i += bytes_dy;

        let flags = dx_flags | dy_flags;

        if flags & 0x20 != 0 {
            // Trim + jump
            commands.push(StitchCommand::Trim);
            commands.push(StitchCommand::Jump { dx, dy });
        } else if flags & 0x10 != 0 {
            commands.push(StitchCommand::Jump { dx, dy });
        } else {
            commands.push(StitchCommand::Stitch { dx, dy });
        }
    }

    if commands.is_empty() {
        return Err(Error::NoStitchData);
    }

    Ok(commands)
}

/// Decode a single coordinate (dx or dy) from the byte stream.
/// Returns (value, flags, bytes_consumed).
fn decode_coordinate(data: &[u8], pos: usize) -> Result<(i16, u8, usize), Error> {
    if pos >= data.len() {
        return Err(Error::TooShort {
            expected: pos + 1,
            actual: data.len(),
        });
    }

    let b = data[pos];

    if b & 0x80 != 0 {
        // Extended 12-bit encoding (2 bytes)
        if pos + 1 >= data.len() {
            return Err(Error::TooShort {
                expected: pos + 2,
                actual: data.len(),
            });
        }
        let b2 = data[pos + 1];
        let flags = b & 0x70; // bits 6-4 for jump/trim flags
        let raw = (((b & 0x0F) as u16) << 8) | (b2 as u16);
        let value = if raw > 0x7FF {
            raw as i16 - 0x1000
        } else {
            raw as i16
        };
        Ok((value, flags, 2))
    } else {
        // 7-bit encoding (1 byte)
        let value = if b > 0x3F { b as i16 - 0x80 } else { b as i16 };
        Ok((value, 0, 1))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn decode_end_marker() {
        let data = [0xFF];
        let cmds = decode_stitches(&data).unwrap();
        assert_eq!(cmds.len(), 1);
        assert!(matches!(cmds[0], StitchCommand::End));
    }

    #[test]
    fn decode_simple_stitch() {
        // dx=10 (0x0A), dy=20 (0x14), then end
        let data = [0x0A, 0x14, 0xFF];
        let cmds = decode_stitches(&data).unwrap();
        assert_eq!(cmds.len(), 2);
        match &cmds[0] {
            StitchCommand::Stitch { dx, dy } => {
                assert_eq!(*dx, 10);
                assert_eq!(*dy, 20);
            }
            _ => panic!("expected Stitch"),
        }
    }

    #[test]
    fn decode_negative_7bit() {
        // dx=0x50 (80 decimal, > 0x3F so value = 80-128 = -48), dy=0x60 (96-128=-32), end
        let data = [0x50, 0x60, 0xFF];
        let cmds = decode_stitches(&data).unwrap();
        match &cmds[0] {
            StitchCommand::Stitch { dx, dy } => {
                assert_eq!(*dx, -48);
                assert_eq!(*dy, -32);
            }
            _ => panic!("expected Stitch"),
        }
    }

    #[test]
    fn decode_color_change() {
        let data = [0xFE, 0xB0, 0x0A, 0x14, 0xFF];
        let cmds = decode_stitches(&data).unwrap();
        assert!(matches!(cmds[0], StitchCommand::ColorChange));
        assert!(matches!(cmds[1], StitchCommand::Stitch { dx: 10, dy: 20 }));
    }

    #[test]
    fn decode_extended_12bit() {
        // Extended dx: high bit set, flags=0x10 (jump), value = 0x100 = 256
        // byte1 = 0x80 | 0x10 | 0x01 = 0x91, byte2 = 0x00 -> raw = 0x100 = 256
        // dy: simple 0x05 = 5
        let data = [0x91, 0x00, 0x05, 0xFF];
        let cmds = decode_stitches(&data).unwrap();
        assert!(matches!(cmds[0], StitchCommand::Jump { dx: 256, dy: 5 }));
    }

    #[test]
    fn decode_trim_jump() {
        // dx with trim flag (0x20): byte1 = 0x80 | 0x20 | 0x00 = 0xA0, byte2 = 0x0A -> raw=10
        // dy: simple 0x05
        let data = [0xA0, 0x0A, 0x05, 0xFF];
        let cmds = decode_stitches(&data).unwrap();
        assert!(matches!(cmds[0], StitchCommand::Trim));
        assert!(matches!(cmds[1], StitchCommand::Jump { dx: 10, dy: 5 }));
    }
}