init
Some checks failed
Release / build-deb (push) Has been cancelled
CI / check (pull_request) Has been cancelled
CI / version-check (pull_request) Has been cancelled

This commit is contained in:
2026-03-30 00:01:49 +02:00
commit da71b56f2d
18 changed files with 1406 additions and 0 deletions

253
rustitch/src/pes/pec.rs Normal file
View File

@@ -0,0 +1,253 @@
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 }));
}
}