fixed bug with color layering issue
Some checks failed
CI / Lint and Test (pull_request) Successful in 44s
CI / Version Check (pull_request) Failing after 3s

This commit is contained in:
2026-03-31 12:18:53 +02:00
parent 69d0269270
commit 7c8ecda29a
11 changed files with 126 additions and 51 deletions

2
Cargo.lock generated
View File

@@ -235,7 +235,7 @@ dependencies = [
[[package]] [[package]]
name = "rustitch" name = "rustitch"
version = "0.1.2" version = "0.2.0"
dependencies = [ dependencies = [
"png", "png",
"thiserror", "thiserror",

View File

@@ -7,4 +7,20 @@
<match type="string" offset="0" value="#PES"/> <match type="string" offset="0" value="#PES"/>
</magic> </magic>
</mime-type> </mime-type>
<mime-type type="application/x-dst">
<comment>DST Tajima embroidery file</comment>
<glob pattern="*.dst"/>
</mime-type>
<mime-type type="application/x-exp">
<comment>EXP Melco embroidery file</comment>
<glob pattern="*.exp"/>
</mime-type>
<mime-type type="application/x-jef">
<comment>JEF Janome embroidery file</comment>
<glob pattern="*.jef"/>
</mime-type>
<mime-type type="application/x-vp3">
<comment>VP3 Pfaff embroidery file</comment>
<glob pattern="*.vp3"/>
</mime-type>
</mime-info> </mime-info>

View File

@@ -1,4 +1,4 @@
[Thumbnailer Entry] [Thumbnailer Entry]
TryExec=stitch-peek TryExec=stitch-peek
Exec=stitch-peek -i %i -o %o -s %s 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

View File

@@ -77,16 +77,36 @@ pub fn parse(data: &[u8]) -> Result<Vec<StitchCommand>, Error> {
/// Standard bit layout for dx across bytes b0, b1, b2. /// Standard bit layout for dx across bytes b0, b1, b2.
fn decode_dx(b0: u8, b1: u8, b2: u8) -> i16 { fn decode_dx(b0: u8, b1: u8, b2: u8) -> i16 {
let mut x: i16 = 0; let mut x: i16 = 0;
if b0 & 0x01 != 0 { x += 1; } if b0 & 0x01 != 0 {
if b0 & 0x02 != 0 { x -= 1; } x += 1;
if b0 & 0x04 != 0 { x += 9; } }
if b0 & 0x08 != 0 { x -= 9; } if b0 & 0x02 != 0 {
if b1 & 0x01 != 0 { x += 3; } x -= 1;
if b1 & 0x02 != 0 { x -= 3; } }
if b1 & 0x04 != 0 { x += 27; } if b0 & 0x04 != 0 {
if b1 & 0x08 != 0 { x -= 27; } x += 9;
if b2 & 0x04 != 0 { x += 81; } }
if b2 & 0x08 != 0 { x -= 81; } 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 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. /// Standard bit layout for dy across bytes b0, b1, b2.
fn decode_dy(b0: u8, b1: u8, b2: u8) -> i16 { fn decode_dy(b0: u8, b1: u8, b2: u8) -> i16 {
let mut y: i16 = 0; let mut y: i16 = 0;
if b0 & 0x80 != 0 { y += 1; } if b0 & 0x80 != 0 {
if b0 & 0x40 != 0 { y -= 1; } y += 1;
if b0 & 0x20 != 0 { y += 9; } }
if b0 & 0x10 != 0 { y -= 9; } if b0 & 0x40 != 0 {
if b1 & 0x80 != 0 { y += 3; } y -= 1;
if b1 & 0x40 != 0 { y -= 3; } }
if b1 & 0x20 != 0 { y += 27; } if b0 & 0x20 != 0 {
if b1 & 0x10 != 0 { y -= 27; } y += 9;
if b2 & 0x20 != 0 { y += 81; } }
if b2 & 0x10 != 0 { y -= 81; } 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) // DST Y axis is inverted (positive = up in machine coords, down in screen coords)
-y -y
} }
@@ -166,7 +206,7 @@ mod tests {
assert_eq!(decode_dx(0x04, 0x00, 0x00), 9); assert_eq!(decode_dx(0x04, 0x00, 0x00), 9);
assert_eq!(decode_dx(0x00, 0x04, 0x00), 27); assert_eq!(decode_dx(0x00, 0x04, 0x00), 27);
assert_eq!(decode_dx(0x00, 0x00, 0x04), 81); 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] #[test]

View File

@@ -88,10 +88,7 @@ mod tests {
fn parse_simple_stitches() { fn parse_simple_stitches() {
let data = [0x0A, 0x14, 0x05, 0x03]; let data = [0x0A, 0x14, 0x05, 0x03];
let cmds = parse(&data).unwrap(); let cmds = parse(&data).unwrap();
assert!(matches!( assert!(matches!(cmds[0], StitchCommand::Stitch { dx: 10, dy: 20 }));
cmds[0],
StitchCommand::Stitch { dx: 10, dy: 20 }
));
assert!(matches!(cmds[1], StitchCommand::Stitch { dx: 5, dy: 3 })); assert!(matches!(cmds[1], StitchCommand::Stitch { dx: 5, dy: 3 }));
assert!(matches!(cmds[2], StitchCommand::End)); assert!(matches!(cmds[2], StitchCommand::End));
} }
@@ -120,10 +117,7 @@ mod tests {
fn parse_jump() { fn parse_jump() {
let data = [0x80, 0x04, 0x0A, 0x14]; let data = [0x80, 0x04, 0x0A, 0x14];
let cmds = parse(&data).unwrap(); let cmds = parse(&data).unwrap();
assert!(matches!( assert!(matches!(cmds[0], StitchCommand::Jump { dx: 10, dy: 20 }));
cmds[0],
StitchCommand::Jump { dx: 10, dy: 20 }
));
} }
#[test] #[test]

View File

@@ -15,7 +15,9 @@ use palette::JEF_PALETTE;
/// ///
/// Stitch data: 2 bytes per stitch (signed i8 dx, dy). /// Stitch data: 2 bytes per stitch (signed i8 dx, dy).
/// Control codes: 0x80 0x01 = color change, 0x80 0x02 = jump, 0x80 0x10 = end. /// Control codes: 0x80 0x01 = color change, 0x80 0x02 = jump, 0x80 0x10 = end.
pub fn parse(data: &[u8]) -> Result<(Vec<StitchCommand>, Vec<(u8, u8, u8)>), Error> { type ParseResult = Result<(Vec<StitchCommand>, Vec<(u8, u8, u8)>), Error>;
pub fn parse(data: &[u8]) -> ParseResult {
if data.len() < 116 { if data.len() < 116 {
return Err(Error::TooShort { return Err(Error::TooShort {
expected: 116, expected: 116,
@@ -146,10 +148,7 @@ mod tests {
fn decode_simple_stitches() { fn decode_simple_stitches() {
let data = [0x0A, 0x14, 0x05, 0x03, 0x80, 0x10]; let data = [0x0A, 0x14, 0x05, 0x03, 0x80, 0x10];
let cmds = decode_stitches(&data).unwrap(); let cmds = decode_stitches(&data).unwrap();
assert!(matches!( assert!(matches!(cmds[0], StitchCommand::Stitch { dx: 10, dy: -20 }));
cmds[0],
StitchCommand::Stitch { dx: 10, dy: -20 }
));
assert!(matches!(cmds[1], StitchCommand::Stitch { dx: 5, dy: -3 })); assert!(matches!(cmds[1], StitchCommand::Stitch { dx: 5, dy: -3 }));
assert!(matches!(cmds[2], StitchCommand::End)); assert!(matches!(cmds[2], StitchCommand::End));
} }
@@ -165,9 +164,6 @@ mod tests {
fn decode_jump() { fn decode_jump() {
let data = [0x80, 0x02, 0x0A, 0x14, 0x80, 0x10]; let data = [0x80, 0x02, 0x0A, 0x14, 0x80, 0x10];
let cmds = decode_stitches(&data).unwrap(); let cmds = decode_stitches(&data).unwrap();
assert!(matches!( assert!(matches!(cmds[0], StitchCommand::Jump { dx: 10, dy: -20 }));
cmds[0],
StitchCommand::Jump { dx: 10, dy: -20 }
));
} }
} }

View File

@@ -3,7 +3,11 @@ pub mod format;
pub mod palette; pub mod palette;
pub mod types; pub mod types;
pub mod dst;
pub mod exp;
pub mod jef;
pub mod pes; pub mod pes;
pub mod vp3;
mod render; mod render;
mod resolve; mod resolve;
@@ -32,6 +36,9 @@ fn parse_and_resolve(data: &[u8], fmt: Format) -> Result<ResolvedDesign, Error>
let design = pes::parse(data)?; let design = pes::parse(data)?;
pes::resolve(&design) 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),
} }
} }

View File

@@ -64,6 +64,19 @@ pub fn decode_stitches(data: &[u8]) -> Result<Vec<StitchCommand>, Error> {
let (dx, dx_flags, bytes_dx) = decode_coordinate(data, i)?; let (dx, dx_flags, bytes_dx) = decode_coordinate(data, i)?;
i += bytes_dx; 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)?; let (dy, dy_flags, bytes_dy) = decode_coordinate(data, i)?;
i += bytes_dy; i += bytes_dy;

View File

@@ -12,7 +12,9 @@ use crate::types::{ResolvedDesign, StitchCommand};
/// ///
/// Byte order: mixed, but length-prefixed strings and section sizes use big-endian. /// 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). /// Stitch encoding: variable-length (1 byte for small moves, 3 bytes for large).
pub fn parse(data: &[u8]) -> Result<(Vec<StitchCommand>, Vec<(u8, u8, u8)>), Error> { type ParseResult = Result<(Vec<StitchCommand>, Vec<(u8, u8, u8)>), Error>;
pub fn parse(data: &[u8]) -> ParseResult {
if data.len() < 20 { if data.len() < 20 {
return Err(Error::TooShort { return Err(Error::TooShort {
expected: 20, expected: 20,
@@ -93,10 +95,11 @@ fn skip_vp3_header(reader: &mut Reader) -> Result<(), Error> {
// Skip another potential string // Skip another potential string
if reader.remaining() >= 2 { if reader.remaining() >= 2 {
let peek = reader.peek_u16_be(); let peek = reader.peek_u16_be();
if let Ok(len) = peek { if let Ok(len) = peek
if len < 1000 && (len as usize) + 2 <= reader.remaining() { && len < 1000
skip_string(reader)?; && (len as usize) + 2 <= reader.remaining()
} {
skip_string(reader)?;
} }
} }

View File

@@ -11,6 +11,6 @@ categories = ["graphics", "command-line-utilities"]
readme = "README.md" readme = "README.md"
[dependencies] [dependencies]
rustitch = { version = "0.1.1", path = "../rustitch" } rustitch = { version = "0.2", path = "../rustitch" }
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
anyhow = "1" anyhow = "1"

View File

@@ -3,9 +3,12 @@ use clap::Parser;
use std::fs; use std::fs;
#[derive(Parser)] #[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 { struct Args {
/// Input PES file path /// Input embroidery file path
#[arg(short = 'i', long = "input")] #[arg(short = 'i', long = "input")]
input: std::path::PathBuf, input: std::path::PathBuf,
@@ -21,11 +24,14 @@ struct Args {
fn main() -> Result<()> { fn main() -> Result<()> {
let args = Args::parse(); 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) let data = fs::read(&args.input)
.with_context(|| format!("failed to read {}", args.input.display()))?; .with_context(|| format!("failed to read {}", args.input.display()))?;
let png = let png = rustitch::thumbnail_format(&data, args.size, format)
rustitch::thumbnail(&data, args.size).with_context(|| "failed to generate thumbnail")?; .with_context(|| "failed to generate thumbnail")?;
fs::write(&args.output, &png) fs::write(&args.output, &png)
.with_context(|| format!("failed to write {}", args.output.display()))?; .with_context(|| format!("failed to write {}", args.output.display()))?;