use bc_mur::{
AnimateParams, Color, CorrectionLevel, DEFAULT_MAX_MODULES, Logo,
LogoClearShape,
};
const TEST_SVG: &[u8] = include_bytes!("test_data/bc-logo.svg");
const SHORT_UR: &str = "ur:bytes/hdcxdwinvezm";
fn long_ur() -> bc_ur::UR {
let data: Vec<u8> = (0u16..500).map(|i| (i % 256) as u8).collect();
let cbor: dcbor::CBOR = data.into();
bc_ur::UR::new("bytes", cbor).unwrap()
}
#[test]
fn single_frame_png_dimensions() {
let img = bc_mur::render_ur_qr(
SHORT_UR,
CorrectionLevel::Low,
512,
Color::BLACK,
Color::WHITE,
1,
None,
)
.unwrap();
assert_eq!(img.width, 512);
assert_eq!(img.height, 512);
let png = img.to_png().unwrap();
assert!(png.len() > 100); assert_eq!(&png[..4], &[137, 80, 78, 71]); }
#[test]
fn single_frame_jpeg() {
let img = bc_mur::render_ur_qr(
SHORT_UR,
CorrectionLevel::Medium,
256,
Color::BLACK,
Color::WHITE,
1,
None,
)
.unwrap();
let jpeg = img.to_jpeg(85).unwrap();
assert_eq!(&jpeg[..3], &[0xFF, 0xD8, 0xFF]);
}
#[test]
fn single_frame_custom_colors() {
let img = bc_mur::render_qr(
b"HELLO",
CorrectionLevel::High,
128,
Color::from_hex("#0000FF").unwrap(),
Color::from_hex("#FFFF00").unwrap(),
1,
None,
)
.unwrap();
assert_eq!(img.width, 128);
let has_blue = img
.pixels
.chunks_exact(4)
.any(|px| px[0] == 0 && px[1] == 0 && px[2] == 255);
assert!(has_blue, "expected blue foreground pixels");
}
#[test]
fn single_frame_dark_mode() {
let img = bc_mur::render_ur_qr(
SHORT_UR,
CorrectionLevel::Low,
256,
Color::WHITE,
Color::BLACK,
1,
None,
)
.unwrap();
assert_eq!(img.pixels[0], 0); assert_eq!(img.pixels[1], 0); assert_eq!(img.pixels[2], 0); }
#[test]
fn single_frame_quiet_zone_0() {
let img = bc_mur::render_qr(
b"HELLO",
CorrectionLevel::Low,
256,
Color::BLACK,
Color::WHITE,
0,
None,
)
.unwrap();
assert_eq!(img.width, 256);
}
#[test]
fn single_frame_quiet_zone_4() {
let img = bc_mur::render_qr(
b"HELLO",
CorrectionLevel::Low,
512,
Color::BLACK,
Color::WHITE,
4,
None,
)
.unwrap();
assert_eq!(img.width, 512);
assert_eq!(img.pixels[0], 255);
}
#[test]
fn single_frame_with_svg_logo() {
let logo =
Logo::from_svg(TEST_SVG, 0.25, 1, LogoClearShape::Square).unwrap();
assert_eq!(logo.width, 512);
assert_eq!(logo.height, 512);
let img = bc_mur::render_ur_qr(
SHORT_UR,
CorrectionLevel::High,
512,
Color::BLACK,
Color::WHITE,
1,
Some(&logo),
)
.unwrap();
let png = img.to_png().unwrap();
assert!(png.len() > 100);
}
#[test]
fn single_frame_with_circle_logo() {
let logo =
Logo::from_svg(TEST_SVG, 0.30, 2, LogoClearShape::Circle).unwrap();
let img = bc_mur::render_qr(
b"UR:BYTES/TEST",
CorrectionLevel::High,
256,
Color::BLACK,
Color::WHITE,
1,
Some(&logo),
)
.unwrap();
assert_eq!(img.width, 256);
}
#[test]
fn animated_gif_basic() {
let ur = long_ur();
let params = AnimateParams {
max_fragment_len: 50,
size: 256,
cycles: 2,
fps: 4.0,
..Default::default()
};
let frames = bc_mur::generate_frames(&ur, ¶ms).unwrap();
assert!(frames.len() >= 2, "expected multiple frames");
let gif = bc_mur::encode_animated_gif(&frames, 4.0).unwrap();
assert_eq!(&gif[..6], b"GIF89a");
assert!(gif.len() > 100);
}
#[test]
fn animated_gif_with_logo() {
let ur = long_ur();
let logo =
Logo::from_svg(TEST_SVG, 0.20, 1, LogoClearShape::Square).unwrap();
let params = AnimateParams {
max_fragment_len: 50,
size: 256,
cycles: 1,
fps: 4.0,
logo: Some(logo),
..Default::default()
};
let frames = bc_mur::generate_frames(&ur, ¶ms).unwrap();
let gif = bc_mur::encode_animated_gif(&frames, 4.0).unwrap();
assert_eq!(&gif[..6], b"GIF89a");
}
#[test]
fn frame_dump() {
let ur = long_ur();
let params = AnimateParams {
max_fragment_len: 50,
size: 128,
cycles: 1,
fps: 4.0,
..Default::default()
};
let frames = bc_mur::generate_frames(&ur, ¶ms).unwrap();
let tmp = tempfile::tempdir().unwrap();
bc_mur::write_frame_pngs(&frames, tmp.path()).unwrap();
let entries: Vec<_> = std::fs::read_dir(tmp.path())
.unwrap()
.filter_map(|e| e.ok())
.collect();
assert_eq!(entries.len(), frames.len());
}
#[test]
fn invalid_color_hex() {
assert!(Color::from_hex("#ZZZZZZ").is_err());
}
#[test]
fn logo_fraction_out_of_range() {
assert!(Logo::from_svg(TEST_SVG, 0.0, 1, LogoClearShape::Square,).is_err());
assert!(Logo::from_svg(TEST_SVG, 1.0, 1, LogoClearShape::Square,).is_err());
}
#[test]
fn qr_module_count_small() {
let count =
bc_mur::qr_module_count(b"HELLO", CorrectionLevel::Low).unwrap();
assert_eq!(count, 21); }
#[test]
fn check_qr_density_passes() {
bc_mur::check_qr_density(21, DEFAULT_MAX_MODULES).unwrap();
}
#[test]
fn check_qr_density_fails() {
let err = bc_mur::check_qr_density(150, 117).unwrap_err();
match err {
bc_mur::Error::QrCodeTooDense { module_count, max_modules } => {
assert_eq!(module_count, 150);
assert_eq!(max_modules, 117);
}
other => panic!("expected QrCodeTooDense, got {other}"),
}
}
#[test]
fn density_check_on_dense_qr() {
let data: Vec<u8> = (0u16..1000).map(|i| (i % 256) as u8).collect();
let cbor: dcbor::CBOR = data.into();
let ur = bc_ur::UR::new("bytes", cbor).unwrap();
let ur_string = ur.qr_string();
let upper = ur_string.to_ascii_uppercase();
let modules =
bc_mur::qr_module_count(upper.as_bytes(), CorrectionLevel::Low)
.unwrap();
assert!(
modules > DEFAULT_MAX_MODULES,
"expected dense QR ({modules} modules), \
but it fits within {DEFAULT_MAX_MODULES}"
);
let err =
bc_mur::check_qr_density(modules, DEFAULT_MAX_MODULES).unwrap_err();
assert!(matches!(err, bc_mur::Error::QrCodeTooDense { .. }));
}
#[test]
fn insufficient_frames_error() {
let ur = long_ur();
let params = AnimateParams {
max_fragment_len: 50,
frame_count: Some(1), ..Default::default()
};
let result = bc_mur::generate_frames(&ur, ¶ms);
assert!(result.is_err());
let err = result.err().unwrap();
assert!(
matches!(err, bc_mur::Error::InsufficientFrames { .. }),
"expected InsufficientFrames, got {err}"
);
}
#[test]
fn frame_count_exact() {
let ur = long_ur();
let params = AnimateParams {
max_fragment_len: 50,
frame_count: Some(100),
..Default::default()
};
let frames = bc_mur::generate_frames(&ur, ¶ms).unwrap();
assert_eq!(frames.len(), 100);
}
#[test]
fn animate_density_check() {
let ur = long_ur();
let params = AnimateParams {
max_fragment_len: 500, max_modules: Some(21), ..Default::default()
};
let result = bc_mur::generate_frames(&ur, ¶ms);
assert!(result.is_err());
let err = result.err().unwrap();
assert!(
matches!(err, bc_mur::Error::QrCodeTooDense { .. }),
"expected QrCodeTooDense, got {err}"
);
}
#[test]
fn write_test_outputs() {
let out_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("out");
std::fs::create_dir_all(&out_dir).unwrap();
let logo =
Logo::from_svg(TEST_SVG, 0.25, 1, LogoClearShape::Square).unwrap();
let circle_logo =
Logo::from_svg(TEST_SVG, 0.25, 1, LogoClearShape::Circle).unwrap();
let img = bc_mur::render_ur_qr(
SHORT_UR,
CorrectionLevel::Low,
512,
Color::BLACK,
Color::WHITE,
1,
None,
)
.unwrap();
std::fs::write(out_dir.join("single-no-logo.png"), img.to_png().unwrap())
.unwrap();
let img = bc_mur::render_ur_qr(
SHORT_UR,
CorrectionLevel::High,
512,
Color::BLACK,
Color::WHITE,
1,
Some(&logo),
)
.unwrap();
std::fs::write(out_dir.join("single-with-logo.png"), img.to_png().unwrap())
.unwrap();
let img = bc_mur::render_ur_qr(
SHORT_UR,
CorrectionLevel::High,
512,
Color::BLACK,
Color::WHITE,
1,
Some(&circle_logo),
)
.unwrap();
std::fs::write(
out_dir.join("single-circle-logo.png"),
img.to_png().unwrap(),
)
.unwrap();
let img = bc_mur::render_ur_qr(
SHORT_UR,
CorrectionLevel::Low,
512,
Color::WHITE,
Color::BLACK,
1,
None,
)
.unwrap();
std::fs::write(
out_dir.join("single-dark-no-logo.png"),
img.to_png().unwrap(),
)
.unwrap();
let img = bc_mur::render_ur_qr(
SHORT_UR,
CorrectionLevel::High,
512,
Color::WHITE,
Color::BLACK,
1,
Some(&logo),
)
.unwrap();
std::fs::write(
out_dir.join("single-dark-with-logo.png"),
img.to_png().unwrap(),
)
.unwrap();
let img = bc_mur::render_ur_qr(
SHORT_UR,
CorrectionLevel::Low,
512,
Color::BLACK,
Color::WHITE,
0,
None,
)
.unwrap();
std::fs::write(out_dir.join("single-qz0.png"), img.to_png().unwrap())
.unwrap();
let img = bc_mur::render_ur_qr(
SHORT_UR,
CorrectionLevel::Low,
512,
Color::BLACK,
Color::WHITE,
4,
None,
)
.unwrap();
std::fs::write(out_dir.join("single-qz4.png"), img.to_png().unwrap())
.unwrap();
let img = bc_mur::render_ur_qr(
SHORT_UR,
CorrectionLevel::High,
512,
Color::WHITE,
Color::BLACK,
4,
Some(&logo),
)
.unwrap();
std::fs::write(
out_dir.join("single-dark-qz4-logo.png"),
img.to_png().unwrap(),
)
.unwrap();
let ur = long_ur();
let params = AnimateParams {
max_fragment_len: 50,
size: 512,
cycles: 2,
fps: 8.0,
..Default::default()
};
let frames = bc_mur::generate_frames(&ur, ¶ms).unwrap();
let gif = bc_mur::encode_animated_gif(&frames, 8.0).unwrap();
std::fs::write(out_dir.join("animated.gif"), &gif).unwrap();
let params_logo = AnimateParams {
max_fragment_len: 50,
size: 512,
cycles: 2,
fps: 8.0,
logo: Some(logo.clone()),
..Default::default()
};
let frames_logo = bc_mur::generate_frames(&ur, ¶ms_logo).unwrap();
let gif_logo = bc_mur::encode_animated_gif(&frames_logo, 8.0).unwrap();
std::fs::write(out_dir.join("animated-logo.gif"), &gif_logo).unwrap();
let params_circle_logo = AnimateParams {
max_fragment_len: 50,
size: 512,
cycles: 2,
fps: 8.0,
logo: Some(circle_logo),
..Default::default()
};
let frames_circle_logo =
bc_mur::generate_frames(&ur, ¶ms_circle_logo).unwrap();
let gif_circle_logo =
bc_mur::encode_animated_gif(&frames_circle_logo, 8.0).unwrap();
std::fs::write(out_dir.join("animated-circle-logo.gif"), &gif_circle_logo)
.unwrap();
let params_dark_logo = AnimateParams {
max_fragment_len: 50,
size: 512,
cycles: 2,
fps: 8.0,
foreground: Color::WHITE,
background: Color::BLACK,
logo: Some(logo.clone()),
..Default::default()
};
let frames_dark_logo =
bc_mur::generate_frames(&ur, ¶ms_dark_logo).unwrap();
let gif_dark_logo =
bc_mur::encode_animated_gif(&frames_dark_logo, 8.0).unwrap();
std::fs::write(out_dir.join("animated-dark-logo.gif"), &gif_dark_logo)
.unwrap();
let params_dark = AnimateParams {
max_fragment_len: 50,
size: 512,
cycles: 2,
fps: 8.0,
foreground: Color::WHITE,
background: Color::BLACK,
..Default::default()
};
let frames_dark = bc_mur::generate_frames(&ur, ¶ms_dark).unwrap();
let gif_dark = bc_mur::encode_animated_gif(&frames_dark, 8.0).unwrap();
std::fs::write(out_dir.join("animated-dark.gif"), &gif_dark).unwrap();
let prores_path = out_dir.join("animated.mov");
bc_mur::encode_prores(&frames, 8.0, &prores_path).unwrap();
assert!(prores_path.exists());
assert!(std::fs::metadata(&prores_path).unwrap().len() > 100);
}