diff --git a/Cargo.lock b/Cargo.lock index ff33dad..e0c2016 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -235,7 +235,7 @@ dependencies = [ [[package]] name = "rustitch" -version = "0.1.2" +version = "0.2.0" dependencies = [ "png", "thiserror", diff --git a/data/pes.xml b/data/pes.xml index b188082..a9f3dae 100644 --- a/data/pes.xml +++ b/data/pes.xml @@ -7,4 +7,20 @@ + + DST Tajima embroidery file + + + + EXP Melco embroidery file + + + + JEF Janome embroidery file + + + + VP3 Pfaff embroidery file + + diff --git a/data/stitch-peek.thumbnailer b/data/stitch-peek.thumbnailer index 7b0c6c7..30548e0 100644 --- a/data/stitch-peek.thumbnailer +++ b/data/stitch-peek.thumbnailer @@ -1,4 +1,4 @@ [Thumbnailer Entry] TryExec=stitch-peek Exec=stitch-peek -i %i -o %o -s %s -MimeType=application/x-pes +MimeType=application/x-pes;application/x-dst;application/x-exp;application/x-jef;application/x-vp3 diff --git a/rustitch/src/dst/mod.rs b/rustitch/src/dst/mod.rs index a73ec6c..ca2cc8c 100644 --- a/rustitch/src/dst/mod.rs +++ b/rustitch/src/dst/mod.rs @@ -77,16 +77,36 @@ pub fn parse(data: &[u8]) -> Result, Error> { /// 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; } + 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 } @@ -94,16 +114,36 @@ fn decode_dx(b0: u8, b1: u8, b2: u8) -> i16 { /// 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; } + 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 } @@ -166,7 +206,7 @@ mod tests { 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 + 27 + 81); // 118 + assert_eq!(decode_dx(0x05, 0x05, 0x04), 1 + 9 + 3 + 27 + 81); // 121 } #[test] diff --git a/rustitch/src/exp.rs b/rustitch/src/exp.rs index 6ff0b58..19bf7c6 100644 --- a/rustitch/src/exp.rs +++ b/rustitch/src/exp.rs @@ -88,10 +88,7 @@ mod tests { fn parse_simple_stitches() { let data = [0x0A, 0x14, 0x05, 0x03]; let cmds = parse(&data).unwrap(); - assert!(matches!( - cmds[0], - StitchCommand::Stitch { dx: 10, dy: 20 } - )); + assert!(matches!(cmds[0], StitchCommand::Stitch { dx: 10, dy: 20 })); assert!(matches!(cmds[1], StitchCommand::Stitch { dx: 5, dy: 3 })); assert!(matches!(cmds[2], StitchCommand::End)); } @@ -120,10 +117,7 @@ mod tests { fn parse_jump() { let data = [0x80, 0x04, 0x0A, 0x14]; let cmds = parse(&data).unwrap(); - assert!(matches!( - cmds[0], - StitchCommand::Jump { dx: 10, dy: 20 } - )); + assert!(matches!(cmds[0], StitchCommand::Jump { dx: 10, dy: 20 })); } #[test] diff --git a/rustitch/src/jef/mod.rs b/rustitch/src/jef/mod.rs index bcedee5..22ce9d4 100644 --- a/rustitch/src/jef/mod.rs +++ b/rustitch/src/jef/mod.rs @@ -15,7 +15,9 @@ use palette::JEF_PALETTE; /// /// Stitch data: 2 bytes per stitch (signed i8 dx, dy). /// Control codes: 0x80 0x01 = color change, 0x80 0x02 = jump, 0x80 0x10 = end. -pub fn parse(data: &[u8]) -> Result<(Vec, Vec<(u8, u8, u8)>), Error> { +type ParseResult = Result<(Vec, Vec<(u8, u8, u8)>), Error>; + +pub fn parse(data: &[u8]) -> ParseResult { if data.len() < 116 { return Err(Error::TooShort { expected: 116, @@ -146,10 +148,7 @@ mod tests { fn decode_simple_stitches() { let data = [0x0A, 0x14, 0x05, 0x03, 0x80, 0x10]; let cmds = decode_stitches(&data).unwrap(); - assert!(matches!( - cmds[0], - StitchCommand::Stitch { dx: 10, dy: -20 } - )); + assert!(matches!(cmds[0], StitchCommand::Stitch { dx: 10, dy: -20 })); assert!(matches!(cmds[1], StitchCommand::Stitch { dx: 5, dy: -3 })); assert!(matches!(cmds[2], StitchCommand::End)); } @@ -165,9 +164,6 @@ mod tests { fn decode_jump() { let data = [0x80, 0x02, 0x0A, 0x14, 0x80, 0x10]; let cmds = decode_stitches(&data).unwrap(); - assert!(matches!( - cmds[0], - StitchCommand::Jump { dx: 10, dy: -20 } - )); + assert!(matches!(cmds[0], StitchCommand::Jump { dx: 10, dy: -20 })); } } diff --git a/rustitch/src/lib.rs b/rustitch/src/lib.rs index a51b4c2..31587ee 100644 --- a/rustitch/src/lib.rs +++ b/rustitch/src/lib.rs @@ -3,7 +3,11 @@ pub mod format; pub mod palette; pub mod types; +pub mod dst; +pub mod exp; +pub mod jef; pub mod pes; +pub mod vp3; mod render; mod resolve; @@ -32,6 +36,9 @@ fn parse_and_resolve(data: &[u8], fmt: Format) -> Result let design = pes::parse(data)?; pes::resolve(&design) } - _ => Err(Error::UnsupportedFormat), + Format::Dst => dst::parse_and_resolve(data), + Format::Exp => exp::parse_and_resolve(data), + Format::Jef => jef::parse_and_resolve(data), + Format::Vp3 => vp3::parse_and_resolve(data), } } diff --git a/rustitch/src/pes/pec.rs b/rustitch/src/pes/pec.rs index 25cf2dd..c520f7d 100644 --- a/rustitch/src/pes/pec.rs +++ b/rustitch/src/pes/pec.rs @@ -64,6 +64,19 @@ pub fn decode_stitches(data: &[u8]) -> Result, Error> { let (dx, dx_flags, bytes_dx) = decode_coordinate(data, i)?; i += bytes_dx; + // Check for special bytes at dy position — color change or end markers + // can appear between dx and dy when the preceding stitch ends on an + // odd byte boundary relative to the next control byte. + if i < data.len() && data[i] == 0xFF { + commands.push(StitchCommand::End); + break; + } + if i < data.len() && data[i] == 0xFE { + commands.push(StitchCommand::ColorChange); + i += 2; + continue; + } + let (dy, dy_flags, bytes_dy) = decode_coordinate(data, i)?; i += bytes_dy; diff --git a/rustitch/src/vp3.rs b/rustitch/src/vp3.rs index 902c02a..ce88b36 100644 --- a/rustitch/src/vp3.rs +++ b/rustitch/src/vp3.rs @@ -12,7 +12,9 @@ use crate::types::{ResolvedDesign, StitchCommand}; /// /// Byte order: mixed, but length-prefixed strings and section sizes use big-endian. /// Stitch encoding: variable-length (1 byte for small moves, 3 bytes for large). -pub fn parse(data: &[u8]) -> Result<(Vec, Vec<(u8, u8, u8)>), Error> { +type ParseResult = Result<(Vec, Vec<(u8, u8, u8)>), Error>; + +pub fn parse(data: &[u8]) -> ParseResult { if data.len() < 20 { return Err(Error::TooShort { expected: 20, @@ -93,10 +95,11 @@ fn skip_vp3_header(reader: &mut Reader) -> Result<(), Error> { // Skip another potential string if reader.remaining() >= 2 { let peek = reader.peek_u16_be(); - if let Ok(len) = peek { - if len < 1000 && (len as usize) + 2 <= reader.remaining() { - skip_string(reader)?; - } + if let Ok(len) = peek + && len < 1000 + && (len as usize) + 2 <= reader.remaining() + { + skip_string(reader)?; } } diff --git a/stitch-peek/Cargo.toml b/stitch-peek/Cargo.toml index 8c016a2..ba4449c 100644 --- a/stitch-peek/Cargo.toml +++ b/stitch-peek/Cargo.toml @@ -11,6 +11,6 @@ categories = ["graphics", "command-line-utilities"] readme = "README.md" [dependencies] -rustitch = { version = "0.1.1", path = "../rustitch" } +rustitch = { version = "0.2", path = "../rustitch" } clap = { version = "4", features = ["derive"] } anyhow = "1" diff --git a/stitch-peek/src/main.rs b/stitch-peek/src/main.rs index a2dba8f..ac31467 100644 --- a/stitch-peek/src/main.rs +++ b/stitch-peek/src/main.rs @@ -3,9 +3,12 @@ use clap::Parser; use std::fs; #[derive(Parser)] -#[command(name = "stitch-peek", about = "PES embroidery file thumbnailer")] +#[command( + name = "stitch-peek", + about = "Embroidery file thumbnailer (PES, DST, EXP, JEF, VP3)" +)] struct Args { - /// Input PES file path + /// Input embroidery file path #[arg(short = 'i', long = "input")] input: std::path::PathBuf, @@ -21,11 +24,14 @@ struct Args { fn main() -> Result<()> { let args = Args::parse(); + let format = rustitch::format::detect_from_extension(&args.input) + .with_context(|| format!("unsupported file extension: {}", args.input.display()))?; + let data = fs::read(&args.input) .with_context(|| format!("failed to read {}", args.input.display()))?; - let png = - rustitch::thumbnail(&data, args.size).with_context(|| "failed to generate thumbnail")?; + let png = rustitch::thumbnail_format(&data, args.size, format) + .with_context(|| "failed to generate thumbnail")?; fs::write(&args.output, &png) .with_context(|| format!("failed to write {}", args.output.display()))?;