diff --git a/README.md b/README.md index 6fa27d2..739bf62 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,15 @@ [![docs.rs](https://img.shields.io/docsrs/rustitch)](https://docs.rs/rustitch) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -A Nautilus/GNOME thumbnailer for **PES embroidery files**. Browse your embroidery designs in the file manager with automatic thumbnail previews. +A Nautilus/GNOME thumbnailer for **embroidery files**. Browse your embroidery designs in the file manager with automatic thumbnail previews. + +Supported formats: **PES**, **DST**, **EXP**, **JEF**, **VP3** Built as two crates: | Crate | Description | |-------|-------------| -| [**rustitch**](rustitch/) | Library for parsing PES files and rendering stitch data to images | +| [**rustitch**](rustitch/) | Library for parsing embroidery files and rendering stitch data to images | | [**stitch-peek**](stitch-peek/) | CLI thumbnailer that integrates with GNOME/Nautilus | @@ -68,7 +70,7 @@ nautilus -q ### As a thumbnailer -Once installed, Nautilus will automatically generate thumbnails for `.pes` files. No manual action needed -- just open a folder containing PES files. +Once installed, Nautilus will automatically generate thumbnails for embroidery files. No manual action needed -- just open a folder containing `.pes`, `.dst`, `.exp`, `.jef`, or `.vp3` files. ### Standalone CLI @@ -76,11 +78,12 @@ Generate a thumbnail manually: ```sh stitch-peek -i design.pes -o preview.png -s 256 +stitch-peek -i pattern.dst -o preview.png -s 256 ``` | Flag | Description | Default | |------|-------------|---------| -| `-i` | Input PES file | required | +| `-i` | Input embroidery file | required | | `-o` | Output PNG path | required | | `-s` | Thumbnail size (pixels) | 128 | @@ -94,16 +97,26 @@ rustitch = "0.1" ``` ```rust +// PES (auto-detected) let pes_data = std::fs::read("design.pes")?; let png_bytes = rustitch::thumbnail(&pes_data, 256)?; -std::fs::write("preview.png", &png_bytes)?; + +// Any supported format (explicit) +let dst_data = std::fs::read("pattern.dst")?; +let png_bytes = rustitch::thumbnail_format(&dst_data, 256, rustitch::Format::Dst)?; ``` See the [rustitch README](rustitch/README.md) for more API examples. ## Supported formats -**PES** (Brother PE-Design) embroidery files, versions 1 through 10. The PEC section containing stitch data is consistent across versions. +| Format | Manufacturer | Colors | Notes | +|--------|-------------|--------|-------| +| **PES** | Brother PE-Design | Embedded (PEC palette) | Versions 1-10 | +| **DST** | Tajima | Default palette | 3-byte bit-packed records | +| **EXP** | Melco/Bernina | Default palette | Simple 2-byte encoding | +| **JEF** | Janome | Embedded (Janome palette) | Structured header with color table | +| **VP3** | Pfaff/Viking | Embedded (RGB) | Hierarchical format with per-section colors | ## Project structure @@ -112,16 +125,22 @@ stitch-peek-rs/ ├── rustitch/ # Library crate │ └── src/ │ ├── lib.rs # Public API -│ ├── pes/ # PES format parser -│ │ ├── header.rs # File header (#PES magic, version, PEC offset) -│ │ ├── pec.rs # PEC section (colors, stitch decoding) -│ │ └── palette.rs # Brother 65-color thread palette -│ └── render.rs # tiny-skia renderer +│ ├── types.rs # Shared types (StitchCommand, ResolvedDesign, ...) +│ ├── error.rs # Error types +│ ├── format.rs # Format detection (magic bytes, extension) +│ ├── palette.rs # Thread color palettes (PEC, default) +│ ├── resolve.rs # Stitch command to segment resolver +│ ├── render.rs # tiny-skia renderer +│ ├── pes/ # PES (Brother) parser +│ ├── dst/ # DST (Tajima) parser +│ ├── exp.rs # EXP (Melco) parser +│ ├── jef/ # JEF (Janome) parser +│ └── vp3.rs # VP3 (Pfaff/Viking) parser ├── stitch-peek/ # Binary crate (CLI thumbnailer) │ └── src/main.rs └── data/ ├── stitch-peek.thumbnailer # Nautilus integration - └── pes.xml # MIME type definition + └── pes.xml # MIME type definitions ``` ## Development diff --git a/rustitch/Cargo.toml b/rustitch/Cargo.toml index 579abfe..3eff2d1 100644 --- a/rustitch/Cargo.toml +++ b/rustitch/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustitch" -version = "0.2.0" +version = "0.2.1" edition = "2024" description = "PES embroidery file parser and thumbnail renderer" license = "MIT" diff --git a/rustitch/README.md b/rustitch/README.md index a549440..8158179 100644 --- a/rustitch/README.md +++ b/rustitch/README.md @@ -4,7 +4,9 @@ [![docs.rs](https://img.shields.io/docsrs/rustitch)](https://docs.rs/rustitch) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE) -A Rust library for parsing **PES embroidery files** and rendering stitch data to images. +A Rust library for parsing **embroidery files** and rendering stitch data to images. + +Supported formats: **PES**, **DST**, **EXP**, **JEF**, **VP3** Part of the [stitch-peek-rs](https://git.narl.io/nvrl/stitch-peek-rs) project. @@ -14,18 +16,24 @@ Add `rustitch` to your `Cargo.toml`: ```toml [dependencies] -rustitch = "0.1" +rustitch = "0.2" ``` ### Generate a thumbnail ```rust +// PES files (backward-compatible API) let pes_data = std::fs::read("design.pes")?; let png_bytes = rustitch::thumbnail(&pes_data, 256)?; std::fs::write("preview.png", &png_bytes)?; + +// Any supported format +let dst_data = std::fs::read("pattern.dst")?; +let png_bytes = rustitch::thumbnail_format(&dst_data, 256, rustitch::Format::Dst)?; +std::fs::write("preview.png", &png_bytes)?; ``` -### Parse and inspect a design +### Parse and inspect a PES design ```rust use rustitch::pes::{self, StitchCommand}; @@ -61,15 +69,39 @@ let png_bytes = rustitch::render_thumbnail(&resolved, 512)?; std::fs::write("large_preview.png", &png_bytes)?; ``` +### Format detection + +```rust +use rustitch::format::{self, Format}; +use std::path::Path; + +// Detect from file extension +let fmt = format::detect_from_extension(Path::new("design.jef")); +assert_eq!(fmt, Some(Format::Jef)); + +// Detect from file content (magic bytes) +let data = std::fs::read("design.pes")?; +let fmt = format::detect_from_bytes(&data); +assert_eq!(fmt, Some(Format::Pes)); +``` + ## Supported formats -**PES** (Brother PE-Design) embroidery files, versions 1 through 10. The PEC section containing stitch data is consistent across versions. +| Format | Manufacturer | Colors | Notes | +|--------|-------------|--------|-------| +| **PES** | Brother PE-Design | Embedded (PEC palette) | Versions 1-10 | +| **DST** | Tajima | Default palette | 3-byte bit-packed records | +| **EXP** | Melco/Bernina | Default palette | Simple 2-byte encoding | +| **JEF** | Janome | Embedded (Janome palette) | Structured header with color table | +| **VP3** | Pfaff/Viking | Embedded (RGB) | Hierarchical format with per-section colors | + +Formats without embedded color info (DST, EXP) use a default palette of 12 high-contrast colors, cycling on each color change. ## How it works -1. **Parse** the PES binary header to locate the PEC section -2. **Decode** the PEC stitch byte stream (7-bit and 12-bit encoded relative movements, jumps, trims, color changes) -3. **Resolve** relative movements into absolute coordinate segments grouped by thread color, using the 65-color Brother PEC palette +1. **Detect** the file format from magic bytes or extension +2. **Parse** the format-specific binary encoding into a common `StitchCommand` stream (stitch, jump, trim, color change, end) +3. **Resolve** relative movements into absolute coordinate segments grouped by thread color 4. **Render** anti-aliased line segments with [tiny-skia](https://github.com/nickel-org/tiny-skia), scaled to fit the requested size 5. **Encode** as PNG with proper alpha handling diff --git a/rustitch/src/exp.rs b/rustitch/src/exp.rs index 19bf7c6..0c654e8 100644 --- a/rustitch/src/exp.rs +++ b/rustitch/src/exp.rs @@ -42,7 +42,7 @@ pub fn parse(data: &[u8]) -> Result, Error> { break; } let dx = data[i] as i8 as i16; - let dy = data[i + 1] as i8 as i16; + let dy = -(data[i + 1] as i8 as i16); commands.push(StitchCommand::Jump { dx, dy }); i += 2; } @@ -53,7 +53,7 @@ pub fn parse(data: &[u8]) -> Result, Error> { } } else { let dx = b1 as i8 as i16; - let dy = b2 as i8 as i16; + let dy = -(b2 as i8 as i16); commands.push(StitchCommand::Stitch { dx, dy }); i += 2; } @@ -88,8 +88,8 @@ 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[1], StitchCommand::Stitch { dx: 5, dy: 3 })); + 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)); } @@ -98,10 +98,7 @@ mod tests { // -10 as i8 = 0xF6, -20 as i8 = 0xEC let data = [0xF6, 0xEC]; 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 })); } #[test] @@ -110,14 +107,14 @@ mod tests { let cmds = parse(&data).unwrap(); assert!(matches!(cmds[0], StitchCommand::Stitch { .. })); assert!(matches!(cmds[1], StitchCommand::ColorChange)); - assert!(matches!(cmds[2], StitchCommand::Stitch { dx: 5, dy: 3 })); + assert!(matches!(cmds[2], StitchCommand::Stitch { dx: 5, dy: -3 })); } #[test] 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/pes/pec.rs b/rustitch/src/pes/pec.rs index c520f7d..c41a29a 100644 --- a/rustitch/src/pes/pec.rs +++ b/rustitch/src/pes/pec.rs @@ -61,12 +61,13 @@ pub fn decode_stitches(data: &[u8]) -> Result, Error> { continue; } - let (dx, dx_flags, bytes_dx) = decode_coordinate(data, i)?; - i += bytes_dx; + // PEC encodes coordinates as (Y, X) — read first value as vertical, + // second as horizontal, then swap to (dx, dy) for screen coordinates. + let (val1, flags1, bytes1) = decode_coordinate(data, i)?; + i += bytes1; - // 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. + // Check for special bytes at second coordinate position — color change + // or end markers can appear between the two coordinates. if i < data.len() && data[i] == 0xFF { commands.push(StitchCommand::End); break; @@ -77,10 +78,12 @@ pub fn decode_stitches(data: &[u8]) -> Result, Error> { continue; } - let (dy, dy_flags, bytes_dy) = decode_coordinate(data, i)?; - i += bytes_dy; + let (val2, flags2, bytes2) = decode_coordinate(data, i)?; + i += bytes2; - let flags = dx_flags | dy_flags; + let flags = flags1 | flags2; + let dx = val2; + let dy = val1; if flags & 0x20 != 0 { commands.push(StitchCommand::Trim); @@ -147,13 +150,14 @@ mod tests { #[test] fn decode_simple_stitch() { + // PEC stores (Y, X): first=10 → dy, second=20 → dx 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); + assert_eq!(*dx, 20); + assert_eq!(*dy, 10); } _ => panic!("expected Stitch"), } @@ -161,12 +165,13 @@ mod tests { #[test] fn decode_negative_7bit() { + // PEC stores (Y, X): first=0x50(-48) → dy, second=0x60(-32) → dx 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); + assert_eq!(*dx, -32); + assert_eq!(*dy, -48); } _ => panic!("expected Stitch"), } @@ -174,24 +179,27 @@ mod tests { #[test] fn decode_color_change() { + // PEC stores (Y, X): first=10 → dy, second=20 → dx 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 })); + assert!(matches!(cmds[1], StitchCommand::Stitch { dx: 20, dy: 10 })); } #[test] fn decode_extended_12bit() { + // PEC stores (Y, X): first=0x91,0x00(256 with jump flag) → dy, second=0x05(5) → dx let data = [0x91, 0x00, 0x05, 0xFF]; let cmds = decode_stitches(&data).unwrap(); - assert!(matches!(cmds[0], StitchCommand::Jump { dx: 256, dy: 5 })); + assert!(matches!(cmds[0], StitchCommand::Jump { dx: 5, dy: 256 })); } #[test] fn decode_trim_jump() { + // PEC stores (Y, X): first=0xA0,0x0A(10 with trim flag) → dy, second=0x05(5) → dx 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 })); + assert!(matches!(cmds[1], StitchCommand::Jump { dx: 5, dy: 10 })); } } diff --git a/rustitch/tests/fixtures/0.3x1 INCHES.EXP b/rustitch/tests/fixtures/0.3x1 INCHES.EXP new file mode 100644 index 0000000..7b95a5a Binary files /dev/null and b/rustitch/tests/fixtures/0.3x1 INCHES.EXP differ diff --git a/rustitch/tests/fixtures/0.3x1 INCHES.JEF b/rustitch/tests/fixtures/0.3x1 INCHES.JEF new file mode 100644 index 0000000..d836da4 Binary files /dev/null and b/rustitch/tests/fixtures/0.3x1 INCHES.JEF differ diff --git a/rustitch/tests/fixtures/0.3x1 INCHES.PES b/rustitch/tests/fixtures/0.3x1 INCHES.PES new file mode 100644 index 0000000..90e2bfd Binary files /dev/null and b/rustitch/tests/fixtures/0.3x1 INCHES.PES differ diff --git a/stitch-peek/README.md b/stitch-peek/README.md index 2395650..7e60fbc 100644 --- a/stitch-peek/README.md +++ b/stitch-peek/README.md @@ -3,9 +3,11 @@ [![crates.io](https://img.shields.io/crates/v/stitch-peek)](https://crates.io/crates/stitch-peek) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE) -A CLI tool and **Nautilus/GNOME thumbnailer** for PES embroidery files. Generates PNG previews of embroidery designs directly in your file manager. +A CLI tool and **Nautilus/GNOME thumbnailer** for embroidery files. Generates PNG previews of embroidery designs directly in your file manager. -Part of the [stitch-peek-rs](https://git.narl.io/nvrl/stitch-peek-rs) project. Uses [rustitch](https://crates.io/crates/rustitch) for PES parsing and rendering. +Supported formats: **PES**, **DST**, **EXP**, **JEF**, **VP3** + +Part of the [stitch-peek-rs](https://git.narl.io/nvrl/stitch-peek-rs) project. Uses [rustitch](https://crates.io/crates/rustitch) for parsing and rendering. ## Installation @@ -49,23 +51,27 @@ nautilus -q ### As a thumbnailer -Once installed with the `.thumbnailer` file in place, Nautilus automatically generates thumbnails for `.pes` files. No manual action needed. +Once installed with the `.thumbnailer` file in place, Nautilus automatically generates thumbnails for `.pes`, `.dst`, `.exp`, `.jef`, and `.vp3` files. No manual action needed. ### Standalone CLI ```sh stitch-peek -i design.pes -o preview.png -s 256 +stitch-peek -i pattern.dst -o preview.png +stitch-peek -i motif.jef -o preview.png -s 512 ``` | Flag | Description | Default | |------|-------------|---------| -| `-i` | Input PES file | required | +| `-i` | Input embroidery file | required | | `-o` | Output PNG path | required | | `-s` | Thumbnail size (pixels) | 128 | +The format is detected automatically from the file extension. + ## How it works -The tool follows the [freedesktop thumbnail specification](https://specifications.freedesktop.org/thumbnail/latest/). Nautilus calls it with an input file, output path, and requested size. It parses the PES file, renders the stitch pattern as anti-aliased colored lines on a transparent background, and writes a PNG. +The tool follows the [freedesktop thumbnail specification](https://specifications.freedesktop.org/thumbnail/latest/). Nautilus calls it with an input file, output path, and requested size. It detects the embroidery format, parses the stitch data, renders the pattern as anti-aliased colored lines on a transparent background, and writes a PNG. ## License