diff --git a/Cargo.lock b/Cargo.lock index fc0482a..d286753 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "bitflags" -version = "1.3.2" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bytemuck" @@ -204,9 +204,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "png" -version = "0.17.16" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ "bitflags", "crc32fast", @@ -235,7 +235,7 @@ dependencies = [ [[package]] name = "rustitch" -version = "0.1.0" +version = "0.1.1" dependencies = [ "png", "thiserror", @@ -250,7 +250,7 @@ checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "stitch-peek" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "clap", @@ -302,9 +302,9 @@ dependencies = [ [[package]] name = "tiny-skia" -version = "0.11.4" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea" dependencies = [ "arrayref", "arrayvec", @@ -317,9 +317,9 @@ dependencies = [ [[package]] name = "tiny-skia-path" -version = "0.11.4" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9" dependencies = [ "arrayref", "bytemuck", diff --git a/rustitch/Cargo.toml b/rustitch/Cargo.toml index dac3b48..af4d1d0 100644 --- a/rustitch/Cargo.toml +++ b/rustitch/Cargo.toml @@ -1,9 +1,12 @@ [package] name = "rustitch" -version = "0.1.0" -edition = "2021" +version = "0.1.1" +edition = "2024" [dependencies] thiserror = "2" -tiny-skia = "0.11" -png = "0.17" +tiny-skia = "0.12" +png = "0.18" + +[dev-dependencies] +png = "0.18" diff --git a/rustitch/tests/fixtures/JLS_Gnome Barfs.PES b/rustitch/tests/fixtures/JLS_Gnome Barfs.PES new file mode 100644 index 0000000..406786a Binary files /dev/null and b/rustitch/tests/fixtures/JLS_Gnome Barfs.PES differ diff --git a/rustitch/tests/fixtures/UFront.PES b/rustitch/tests/fixtures/UFront.PES new file mode 100644 index 0000000..3ff6dc9 Binary files /dev/null and b/rustitch/tests/fixtures/UFront.PES differ diff --git a/rustitch/tests/fixtures/UrsulaOne.PES b/rustitch/tests/fixtures/UrsulaOne.PES new file mode 100644 index 0000000..3a97641 Binary files /dev/null and b/rustitch/tests/fixtures/UrsulaOne.PES differ diff --git a/rustitch/tests/pes_files.rs b/rustitch/tests/pes_files.rs new file mode 100644 index 0000000..bc84ad1 --- /dev/null +++ b/rustitch/tests/pes_files.rs @@ -0,0 +1,201 @@ +use std::io::Cursor; + +use rustitch::pes::{self, StitchCommand}; + +const GNOME_BARFS: &[u8] = include_bytes!("fixtures/JLS_Gnome Barfs.PES"); +const URSULA_ONE: &[u8] = include_bytes!("fixtures/UrsulaOne.PES"); +const UFRONT: &[u8] = include_bytes!("fixtures/UFront.PES"); + +// -- Header parsing ---------------------------------------------------------- + +#[test] +fn parse_header_gnome_barfs() { + let design = pes::parse(GNOME_BARFS).unwrap(); + assert_eq!(&design.header.version, b"0100"); +} + +#[test] +fn parse_header_ursula_one() { + let design = pes::parse(URSULA_ONE).unwrap(); + assert_eq!(&design.header.version, b"0060"); +} + +#[test] +fn parse_header_ufront() { + let design = pes::parse(UFRONT).unwrap(); + assert_eq!(&design.header.version, b"0060"); +} + +// -- PEC color table --------------------------------------------------------- + +#[test] +fn gnome_barfs_has_colors() { + let design = pes::parse(GNOME_BARFS).unwrap(); + assert!( + design.pec_header.color_count > 0, + "expected at least one color" + ); + assert_eq!( + design.pec_header.color_indices.len(), + design.pec_header.color_count as usize + ); +} + +#[test] +fn ursula_one_has_colors() { + let design = pes::parse(URSULA_ONE).unwrap(); + assert!(design.pec_header.color_count > 0); + // All color indices should be valid palette entries (0..65) + for &idx in &design.pec_header.color_indices { + assert!( + (idx as usize) < pes::PEC_PALETTE.len(), + "color index {idx} out of palette range" + ); + } +} + +// -- Stitch commands --------------------------------------------------------- + +#[test] +fn gnome_barfs_commands_end_properly() { + let design = pes::parse(GNOME_BARFS).unwrap(); + let last = design.commands.last().unwrap(); + assert!( + matches!(last, StitchCommand::End), + "expected End command as last, got {last:?}" + ); +} + +#[test] +fn ursula_one_commands_end_properly() { + let design = pes::parse(URSULA_ONE).unwrap(); + let last = design.commands.last().unwrap(); + assert!( + matches!(last, StitchCommand::End), + "expected End command as last, got {last:?}" + ); +} + +#[test] +fn ufront_has_stitches() { + let design = pes::parse(UFRONT).unwrap(); + let stitch_count = design + .commands + .iter() + .filter(|c| matches!(c, StitchCommand::Stitch { .. })) + .count(); + assert!( + stitch_count > 100, + "expected many stitches, got {stitch_count}" + ); +} + +#[test] +fn gnome_barfs_has_color_changes() { + let design = pes::parse(GNOME_BARFS).unwrap(); + let changes = design + .commands + .iter() + .filter(|c| matches!(c, StitchCommand::ColorChange)) + .count(); + // Multi-color design should have at least one color change + assert!(changes > 0, "expected color changes, got none"); +} + +// -- Resolve to segments ----------------------------------------------------- + +#[test] +fn resolve_gnome_barfs() { + let design = pes::parse(GNOME_BARFS).unwrap(); + let resolved = pes::resolve(&design).unwrap(); + + assert!(!resolved.segments.is_empty()); + assert!(!resolved.colors.is_empty()); + + // Bounding box should be non-degenerate + assert!(resolved.bounds.max_x > resolved.bounds.min_x); + assert!(resolved.bounds.max_y > resolved.bounds.min_y); +} + +#[test] +fn resolve_ursula_one() { + let design = pes::parse(URSULA_ONE).unwrap(); + let resolved = pes::resolve(&design).unwrap(); + + assert!(!resolved.segments.is_empty()); + assert!(resolved.bounds.max_x > resolved.bounds.min_x); + assert!(resolved.bounds.max_y > resolved.bounds.min_y); +} + +#[test] +fn resolve_ufront() { + let design = pes::parse(UFRONT).unwrap(); + let resolved = pes::resolve(&design).unwrap(); + + assert!(!resolved.segments.is_empty()); + + // All segment color indices should be within the resolved color list + let max_ci = resolved + .segments + .iter() + .map(|s| s.color_index) + .max() + .unwrap(); + assert!( + max_ci < resolved.colors.len(), + "segment references color {max_ci} but only {} colors resolved", + resolved.colors.len() + ); +} + +// -- Full thumbnail pipeline ------------------------------------------------- + +#[test] +fn thumbnail_gnome_barfs_128() { + let png = rustitch::thumbnail(GNOME_BARFS, 128).unwrap(); + assert_png_dimensions(&png, 128, 128); +} + +#[test] +fn thumbnail_ursula_one_256() { + let png = rustitch::thumbnail(URSULA_ONE, 256).unwrap(); + assert_png_dimensions(&png, 256, 256); +} + +#[test] +fn thumbnail_ufront_64() { + let png = rustitch::thumbnail(UFRONT, 64).unwrap(); + assert_png_dimensions(&png, 64, 64); +} + +#[test] +fn thumbnail_gnome_barfs_not_blank() { + let png = rustitch::thumbnail(GNOME_BARFS, 128).unwrap(); + let pixels = decode_png_pixels(&png); + // At least some pixels should have non-zero alpha (not fully transparent) + let opaque_count = pixels.chunks_exact(4).filter(|px| px[3] > 0).count(); + assert!( + opaque_count > 100, + "thumbnail looks blank, only {opaque_count} non-transparent pixels" + ); +} + +// -- Helpers ----------------------------------------------------------------- + +fn assert_png_dimensions(png_data: &[u8], expected_w: u32, expected_h: u32) { + let decoder = png::Decoder::new(Cursor::new(png_data)); + let reader = decoder.read_info().unwrap(); + let info = reader.info(); + assert_eq!(info.width, expected_w, "unexpected PNG width"); + assert_eq!(info.height, expected_h, "unexpected PNG height"); + assert_eq!(info.color_type, png::ColorType::Rgba); + assert_eq!(info.bit_depth, png::BitDepth::Eight); +} + +fn decode_png_pixels(png_data: &[u8]) -> Vec { + let decoder = png::Decoder::new(Cursor::new(png_data)); + let mut reader = decoder.read_info().unwrap(); + let mut buf = vec![0u8; reader.output_buffer_size().unwrap()]; + reader.next_frame(&mut buf).unwrap(); + buf +} diff --git a/stitch-peek/Cargo.toml b/stitch-peek/Cargo.toml index cf6d9cc..8885085 100644 --- a/stitch-peek/Cargo.toml +++ b/stitch-peek/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "stitch-peek" -version = "0.1.0" -edition = "2021" +version = "0.1.1" +edition = "2024" [dependencies] rustitch = { path = "../rustitch" }