use image::codecs::jpeg::JpegEncoder;
use image::codecs::png::PngEncoder;
use image::{ColorType, GenericImageView, ImageEncoder, ImageReader, Rgba, RgbaImage};
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_file_path(name: &str) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("current time")
.as_nanos();
std::env::temp_dir().join(format!("truss-cli-pixel-{name}-{unique}.bin"))
}
fn create_red_blue_4x2_png() -> Vec<u8> {
let mut img = RgbaImage::new(4, 2);
let red = Rgba([255, 0, 0, 255]);
let blue = Rgba([0, 0, 255, 255]);
for y in 0..2 {
for x in 0..4 {
if x < 2 {
img.put_pixel(x, y, red);
} else {
img.put_pixel(x, y, blue);
}
}
}
let mut bytes = Vec::new();
let encoder = PngEncoder::new(&mut bytes);
encoder
.write_image(&img, 4, 2, ColorType::Rgba8.into())
.expect("encode png");
bytes
}
fn create_solid_blue_4x2_png() -> Vec<u8> {
let img = RgbaImage::from_pixel(4, 2, Rgba([0, 0, 255, 255]));
let mut bytes = Vec::new();
let encoder = PngEncoder::new(&mut bytes);
encoder
.write_image(&img, 4, 2, ColorType::Rgba8.into())
.expect("encode png");
bytes
}
fn create_solid_green_2x2_png() -> Vec<u8> {
let img = RgbaImage::from_pixel(2, 2, Rgba([0, 255, 0, 255]));
let mut bytes = Vec::new();
let encoder = PngEncoder::new(&mut bytes);
encoder
.write_image(&img, 2, 2, ColorType::Rgba8.into())
.expect("encode png");
bytes
}
fn create_4x2_jpeg_with_orientation6() -> Vec<u8> {
use image::{Rgb, RgbImage};
let img = RgbImage::from_pixel(4, 2, Rgb([10, 20, 30]));
let mut bytes = Vec::new();
let mut encoder = JpegEncoder::new_with_quality(&mut bytes, 90);
let exif = vec![
0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x12, 0x01, 0x03, 0x00, 0x01,
0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
encoder
.set_exif_metadata(exif)
.expect("set jpeg exif metadata");
encoder
.write_image(&img, 4, 2, ColorType::Rgb8.into())
.expect("encode jpeg");
bytes
}
fn create_4x2_jpeg_with_orientation1() -> Vec<u8> {
use image::{Rgb, RgbImage};
let img = RgbImage::from_pixel(4, 2, Rgb([10, 20, 30]));
let mut bytes = Vec::new();
let mut encoder = JpegEncoder::new_with_quality(&mut bytes, 90);
let exif = vec![
0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x12, 0x01, 0x03, 0x00, 0x01,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
encoder
.set_exif_metadata(exif)
.expect("set jpeg exif metadata");
encoder
.write_image(&img, 4, 2, ColorType::Rgb8.into())
.expect("encode jpeg");
bytes
}
#[test]
fn fit_cover_position_left_keeps_red() {
let input_path = temp_file_path("cover-left-in").with_extension("png");
let output_path = temp_file_path("cover-left-out").with_extension("png");
fs::write(&input_path, create_red_blue_4x2_png()).expect("write input");
let output = Command::new(env!("CARGO_BIN_EXE_truss"))
.arg(&input_path)
.arg("-o")
.arg(&output_path)
.arg("--width")
.arg("2")
.arg("--height")
.arg("2")
.arg("--fit")
.arg("cover")
.arg("--position")
.arg("left")
.output()
.expect("run truss convert");
assert!(output.status.success(), "{output:?}");
let result = image::open(&output_path).expect("open output").to_rgba8();
let _ = fs::remove_file(&input_path);
let _ = fs::remove_file(&output_path);
let px = result.get_pixel(0, 0);
assert!(
px[0] > 200 && px[1] < 50 && px[2] < 50,
"expected red-ish pixel at (0,0), got {px:?}"
);
}
#[test]
fn fit_cover_position_right_keeps_blue() {
let input_path = temp_file_path("cover-right-in").with_extension("png");
let output_path = temp_file_path("cover-right-out").with_extension("png");
fs::write(&input_path, create_red_blue_4x2_png()).expect("write input");
let output = Command::new(env!("CARGO_BIN_EXE_truss"))
.arg(&input_path)
.arg("-o")
.arg(&output_path)
.arg("--width")
.arg("2")
.arg("--height")
.arg("2")
.arg("--fit")
.arg("cover")
.arg("--position")
.arg("right")
.output()
.expect("run truss convert");
assert!(output.status.success(), "{output:?}");
let result = image::open(&output_path).expect("open output").to_rgba8();
let _ = fs::remove_file(&input_path);
let _ = fs::remove_file(&output_path);
let px = result.get_pixel(0, 0);
assert!(
px[0] < 50 && px[1] < 50 && px[2] > 200,
"expected blue-ish pixel at (0,0), got {px:?}"
);
}
#[test]
fn fit_contain_background_padding_color() {
let input_path = temp_file_path("contain-bg-in").with_extension("png");
let output_path = temp_file_path("contain-bg-out").with_extension("png");
fs::write(&input_path, create_solid_blue_4x2_png()).expect("write input");
let output = Command::new(env!("CARGO_BIN_EXE_truss"))
.arg(&input_path)
.arg("-o")
.arg(&output_path)
.arg("--width")
.arg("4")
.arg("--height")
.arg("4")
.arg("--fit")
.arg("contain")
.arg("--background")
.arg("ff0000")
.arg("--format")
.arg("png")
.output()
.expect("run truss convert");
assert!(output.status.success(), "{output:?}");
let result = image::open(&output_path).expect("open output").to_rgba8();
let _ = fs::remove_file(&input_path);
let _ = fs::remove_file(&output_path);
assert_eq!(
result.dimensions(),
(4, 4),
"expected 4x4 output, got {:?}",
result.dimensions()
);
let top_left = result.get_pixel(0, 0);
assert!(
top_left[0] > 200 && top_left[1] < 50 && top_left[2] < 50,
"expected red padding at (0,0), got {top_left:?}"
);
let bottom_left = result.get_pixel(0, 3);
assert!(
bottom_left[0] > 200 && bottom_left[1] < 50 && bottom_left[2] < 50,
"expected red padding at (0,3), got {bottom_left:?}"
);
let content = result.get_pixel(0, 1);
assert!(
content[0] < 50 && content[1] < 50 && content[2] > 200,
"expected blue content at (0,1), got {content:?}"
);
}
#[test]
fn auto_orient_rotates_dimensions() {
let input_path = temp_file_path("orient-auto-in").with_extension("jpg");
let output_path = temp_file_path("orient-auto-out").with_extension("png");
fs::write(&input_path, create_4x2_jpeg_with_orientation6()).expect("write input");
let output = Command::new(env!("CARGO_BIN_EXE_truss"))
.arg(&input_path)
.arg("-o")
.arg(&output_path)
.arg("--format")
.arg("png")
.output()
.expect("run truss convert");
assert!(output.status.success(), "{output:?}");
let result = ImageReader::open(&output_path)
.expect("open output")
.decode()
.expect("decode output");
let _ = fs::remove_file(&input_path);
let _ = fs::remove_file(&output_path);
assert_eq!(
result.dimensions(),
(2, 4),
"auto-orient should rotate 4x2 to 2x4, got {:?}",
result.dimensions()
);
}
#[test]
fn no_auto_orient_preserves_dimensions() {
let input_path = temp_file_path("orient-no-in").with_extension("jpg");
let output_path = temp_file_path("orient-no-out").with_extension("png");
fs::write(&input_path, create_4x2_jpeg_with_orientation6()).expect("write input");
let output = Command::new(env!("CARGO_BIN_EXE_truss"))
.arg(&input_path)
.arg("-o")
.arg(&output_path)
.arg("--format")
.arg("png")
.arg("--no-auto-orient")
.output()
.expect("run truss convert");
assert!(output.status.success(), "{output:?}");
let result = ImageReader::open(&output_path)
.expect("open output")
.decode()
.expect("decode output");
let _ = fs::remove_file(&input_path);
let _ = fs::remove_file(&output_path);
assert_eq!(
result.dimensions(),
(4, 2),
"--no-auto-orient should keep 4x2, got {:?}",
result.dimensions()
);
}
#[test]
fn keep_metadata_webp_preserves_exif_without_warning() {
let input_path = temp_file_path("warn-meta-in").with_extension("jpg");
let output_path = temp_file_path("warn-meta-out").with_extension("webp");
fs::write(&input_path, create_4x2_jpeg_with_orientation1()).expect("write input");
let output = Command::new(env!("CARGO_BIN_EXE_truss"))
.arg(&input_path)
.arg("-o")
.arg(&output_path)
.arg("--keep-metadata")
.arg("--format")
.arg("webp")
.arg("--quality")
.arg("80")
.output()
.expect("run truss convert");
let encoded = fs::read(&output_path).unwrap_or_default();
let _ = fs::remove_file(&input_path);
let _ = fs::remove_file(&output_path);
assert!(
output.status.success(),
"expected exit code 0, got {output:?}"
);
assert!(
encoded.windows(4).any(|window| window == b"EXIF"),
"expected an EXIF chunk in the WebP container"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.to_lowercase().contains("warning"),
"nothing was dropped, so nothing should warn; got: {stderr}"
);
}
fn resize_green_2x2_into_4x4(label: &str, extra: &[&str]) -> image::RgbaImage {
let input_path = temp_file_path(&format!("{label}-in")).with_extension("png");
let output_path = temp_file_path(&format!("{label}-out")).with_extension("png");
fs::write(&input_path, create_solid_green_2x2_png()).expect("write input");
let output = Command::new(env!("CARGO_BIN_EXE_truss"))
.arg(&input_path)
.arg("-o")
.arg(&output_path)
.arg("--width")
.arg("4")
.arg("--height")
.arg("4")
.arg("--background")
.arg("ff0000")
.arg("--format")
.arg("png")
.args(extra)
.output()
.expect("run truss convert");
assert!(output.status.success(), "{output:?}");
let result = image::open(&output_path).expect("open output").to_rgba8();
let _ = fs::remove_file(&input_path);
let _ = fs::remove_file(&output_path);
result
}
#[test]
fn fit_inside_adds_no_padding() {
let result = resize_green_2x2_into_4x4("inside-nopad", &["--fit", "inside"]);
assert_eq!(result.dimensions(), (4, 4));
for (x, y, pixel) in result.enumerate_pixels() {
assert!(
pixel[1] > 200 && pixel[0] < 50,
"pixel ({x},{y}) should be green content, not padding: {pixel:?}"
);
}
}
#[test]
fn fit_inside_with_without_enlargement_keeps_the_source_size() {
let result =
resize_green_2x2_into_4x4("inside-noup", &["--fit", "inside", "--without-enlargement"]);
assert_eq!(result.dimensions(), (2, 2));
let pixel = result.get_pixel(0, 0);
assert!(
pixel[1] > 200 && pixel[0] < 50,
"expected untouched green content, got {pixel:?}"
);
}
#[test]
fn fit_contain_with_without_enlargement_pads_around_the_source() {
let result = resize_green_2x2_into_4x4(
"contain-noup",
&["--fit", "contain", "--without-enlargement"],
);
assert_eq!(result.dimensions(), (4, 4));
let corner = result.get_pixel(0, 0);
assert!(
corner[0] > 200 && corner[1] < 50 && corner[2] < 50,
"expected red padding at corner (0,0), got {corner:?}"
);
let center = result.get_pixel(1, 1);
assert!(
center[1] > 200 && center[0] < 50 && center[2] < 50,
"expected green content at center, got {center:?}"
);
}
#[cfg(feature = "avif")]
#[test]
fn convert_decodes_an_avif_through_the_binary() {
let source = temp_file_path("avif-source").with_extension("png");
let avif = temp_file_path("avif-middle").with_extension("avif");
let output = temp_file_path("avif-output").with_extension("png");
fs::write(&source, create_red_blue_4x2_png()).expect("write png source");
let to_avif = Command::new(env!("CARGO_BIN_EXE_truss"))
.arg(&source)
.arg("-o")
.arg(&avif)
.output()
.expect("run truss convert to avif");
assert!(to_avif.status.success(), "{to_avif:?}");
let from_avif = Command::new(env!("CARGO_BIN_EXE_truss"))
.arg(&avif)
.arg("-o")
.arg(&output)
.output()
.expect("run truss convert from avif");
let decoded = fs::read(&output).ok();
let _ = fs::remove_file(&source);
let _ = fs::remove_file(&avif);
let _ = fs::remove_file(&output);
assert!(from_avif.status.success(), "{from_avif:?}");
let decoded = ImageReader::new(std::io::Cursor::new(decoded.expect("the png was written")))
.with_guessed_format()
.expect("guess the output format")
.decode()
.expect("decode the output");
assert_eq!(decoded.dimensions(), (4, 2));
}