mod common;
use common::render_helpers;
use labelize::elements::drawer_options::DrawerOptions;
use labelize::elements::graphic_box::GraphicBox;
use labelize::elements::graphic_circle::GraphicCircle;
use labelize::elements::label_element::LabelElement;
use labelize::elements::label_info::LabelInfo;
use labelize::elements::label_position::LabelPosition;
use labelize::elements::line_color::LineColor;
use labelize::elements::reverse_print::ReversePrint;
fn default_options() -> DrawerOptions {
render_helpers::default_options()
}
fn empty_label() -> LabelInfo {
LabelInfo {
print_width: 0,
inverted: false,
elements: vec![],
}
}
fn render_label(label: &LabelInfo, options: DrawerOptions) -> Vec<u8> {
render_helpers::render_label_to_png(label, options)
}
fn decode_png(png: &[u8]) -> image::RgbaImage {
image::load_from_memory(png).expect("decode png").to_rgba8()
}
#[test]
fn empty_label_produces_correct_canvas_dimensions() {
let opts = DrawerOptions {
label_width_mm: 50.0,
label_height_mm: 30.0,
dpmm: 8,
enable_inverted_labels: false,
};
let png = render_label(&empty_label(), opts.clone());
let img = decode_png(&png);
let expected_w = (50.0 * 8.0_f64).ceil() as u32;
let expected_h = (30.0 * 8.0_f64).ceil() as u32;
assert_eq!(img.width(), expected_w);
assert_eq!(img.height(), expected_h);
}
#[test]
fn dpmm_6_scales_canvas() {
let opts = DrawerOptions {
label_width_mm: 100.0,
label_height_mm: 150.0,
dpmm: 6,
enable_inverted_labels: false,
};
let png = render_label(&empty_label(), opts);
let img = decode_png(&png);
assert_eq!(img.width(), 600);
assert_eq!(img.height(), 900);
}
#[test]
fn graphic_box_renders_black_pixels_in_box_region() {
let label = LabelInfo {
print_width: 0,
inverted: false,
elements: vec![LabelElement::GraphicBox(GraphicBox {
reverse_print: ReversePrint { value: false },
position: LabelPosition {
x: 10,
y: 10,
calculate_from_bottom: false,
automatic_position: false,
},
width: 100,
height: 50,
border_thickness: 3,
corner_rounding: 0,
line_color: LineColor::Black,
})],
};
let png = render_label(&label, default_options());
let img = decode_png(&png);
let pixel = img.get_pixel(10, 10);
assert!(
pixel[0] < 128,
"expected black pixel at box border, got {:?}",
pixel
);
let outside = img.get_pixel(img.width() - 1, img.height() - 1);
assert!(
outside[0] > 128,
"expected white pixel outside box, got {:?}",
outside
);
}
#[test]
fn graphic_circle_renders_non_white_pixels() {
let label = LabelInfo {
print_width: 0,
inverted: false,
elements: vec![LabelElement::GraphicCircle(GraphicCircle {
reverse_print: ReversePrint { value: false },
position: LabelPosition {
x: 50,
y: 50,
calculate_from_bottom: false,
automatic_position: false,
},
circle_diameter: 80,
border_thickness: 3,
line_color: LineColor::Black,
})],
};
let png = render_label(&label, default_options());
let img = decode_png(&png);
let has_dark = img.pixels().any(|p| p[0] < 128);
assert!(has_dark, "expected non-white pixels for circle");
}
#[test]
fn text_renders_non_white_pixels() {
let zpl = "^XA^FO50,50^A0N,40,40^FDRendered Text^FS^XZ";
let png = render_helpers::render_zpl_to_png(zpl, default_options());
let img = decode_png(&png);
let has_dark = img.pixels().any(|p| p[0] < 128);
assert!(has_dark, "expected non-white pixels for text");
}
#[test]
fn barcode_renders_at_position() {
let zpl = "^XA^FO100,100^BCN,100,Y,N,N^FD123456^FS^XZ";
let png = render_helpers::render_zpl_to_png(zpl, default_options());
let img = decode_png(&png);
let mut dark_in_region = false;
for y in 100..200u32 {
for x in 100..300u32 {
if x < img.width() && y < img.height() && img.get_pixel(x, y)[0] < 128 {
dark_in_region = true;
break;
}
}
if dark_in_region {
break;
}
}
assert!(dark_in_region, "expected barcode pixels in region");
}
#[test]
fn inverted_label_produces_different_output() {
let zpl = "^XA^FO50,50^A0N,40,40^FDInvert^FS^XZ";
let opts_normal = DrawerOptions {
enable_inverted_labels: true,
..default_options()
};
let mut parser = labelize::ZplParser::new();
let labels = parser.parse(zpl.as_bytes()).expect("parse");
let normal_png = render_helpers::render_label_to_png(&labels[0], opts_normal.clone());
let mut inverted_label = labels[0].clone();
inverted_label.inverted = true;
let inverted_png = render_helpers::render_label_to_png(&inverted_label, opts_normal);
assert_ne!(
normal_png, inverted_png,
"inverted should differ from normal"
);
}
#[test]
fn print_width_smaller_than_label_width() {
let zpl = "^XA^PW400^FO10,10^GB380,50,3^FS^XZ";
let png = render_helpers::render_zpl_to_png(zpl, default_options());
let img = decode_png(&png);
let expected_w = (101.625 * 8.0_f64).ceil() as u32;
assert_eq!(img.width(), expected_w);
}
#[test]
fn poi_inverted_label_flips_content() {
let zpl = "^XA^POI^FO10,10^GB100,50,50,B,0^FS^XZ";
let opts = DrawerOptions {
enable_inverted_labels: true,
..default_options()
};
let png = render_helpers::render_zpl_to_png(zpl, opts);
let img = decode_png(&png);
let top_left = img.get_pixel(10, 10);
assert!(
top_left[0] > 128,
"top-left should be white in inverted label, got {:?}",
top_left
);
let mut dark_in_lower_right = false;
let w = img.width();
let h = img.height();
for y in (h - 100)..h {
for x in (w - 200)..w {
if img.get_pixel(x, y)[0] < 128 {
dark_in_lower_right = true;
break;
}
}
if dark_in_lower_right {
break;
}
}
assert!(
dark_in_lower_right,
"expected dark pixels in lower-right for inverted label"
);
}
#[test]
fn barcode_background_is_transparent_text_shows_through() {
let zpl = "^XA\
^FO100,100^A0N,80,80^FDBIG^FS\
^FO100,100^BCN,80,N,N,N^FD12345^FS\
^XZ";
let png = render_helpers::render_zpl_to_png(zpl, default_options());
let img = decode_png(&png);
let mut dark_count = 0u32;
for y in 100..180u32 {
for x in 100..300u32 {
if x < img.width() && y < img.height() && img.get_pixel(x, y)[0] < 128 {
dark_count += 1;
}
}
}
assert!(
dark_count > 100,
"expected dark pixels from both text and barcode, got {}",
dark_count
);
}
#[test]
fn ft_baseline_positions_text_with_margin_from_top() {
let zpl = "^XA^FT30,30^A0N,,28^FDGOGREEN^FS^XZ";
let png = render_helpers::render_zpl_to_png(zpl, default_options());
let img = decode_png(&png);
let mut row0_has_dark = false;
let mut row1_has_dark = false;
for x in 0..img.width() {
if img.get_pixel(x, 0)[0] < 128 {
row0_has_dark = true;
}
if img.get_pixel(x, 1)[0] < 128 {
row1_has_dark = true;
}
}
assert!(
!row0_has_dark,
"row 0 should be white: text should not touch the top edge"
);
assert!(
!row1_has_dark,
"row 1 should be white: there should be margin from top"
);
}
#[test]
fn diagonal_line_thick_border_draws_triangle() {
let zpl = "^XA^FO100,100^GD115,120,120,B,L^FS^XZ";
let png = render_helpers::render_zpl_to_png(zpl, default_options());
let img = decode_png(&png);
let tr = img.get_pixel(210, 105);
assert!(
tr[0] < 128,
"top-right of triangle should be black, got {:?}",
tr
);
let bl = img.get_pixel(105, 215);
assert!(
bl[0] > 128,
"bottom-left should be white (opposite triangle), got {:?}",
bl
);
let upper_right = img.get_pixel(200, 110);
assert!(
upper_right[0] < 128,
"upper-right region should be black, got {:?}",
upper_right
);
}
#[test]
fn qr_code_has_quiet_zone() {
let zpl = "^XA^FO0,0^BQN,2,10^FDQA,Test Data^FS^XZ";
let png = render_helpers::render_zpl_to_png(zpl, default_options());
let img = decode_png(&png);
let mut all_white_before_modules = true;
for y in 0..10u32 {
for x in 0..10u32 {
if x < img.width() && y < img.height() && img.get_pixel(x, y)[0] < 128 {
all_white_before_modules = false;
break;
}
}
if !all_white_before_modules {
break;
}
}
assert!(
all_white_before_modules,
"rows before QR module start should be white"
);
let mut has_dark_at_modules = false;
for y in 10..20u32 {
for x in 0..20u32 {
if x < img.width() && y < img.height() && img.get_pixel(x, y)[0] < 128 {
has_dark_at_modules = true;
break;
}
}
if has_dark_at_modules {
break;
}
}
assert!(
has_dark_at_modules,
"QR should have dark modules starting at y=FO_y+by_height=10"
);
}
#[test]
fn barcode_interpretation_line_rotates_with_barcode() {
let zpl = "^XA^FO200,200^BCI,100,Y,N,N^FD12345^FS^XZ";
let png = render_helpers::render_zpl_to_png(zpl, default_options());
let img = decode_png(&png);
let has_dark_pixels = img.pixels().any(|p| p[0] < 128);
assert!(
has_dark_pixels,
"barcode with rotated interpretation line should render"
);
let zpl_normal = "^XA^FO200,200^BCN,100,Y,N,N^FD12345^FS^XZ";
let png_normal = render_helpers::render_zpl_to_png(zpl_normal, default_options());
assert_ne!(
png, png_normal,
"rotated barcode should differ from normal orientation"
);
}
#[test]
fn ean13_guard_bars_taller_than_data_bars() {
let img =
labelize::barcodes::ean13::encode("123456789012", 200, 2).expect("ean13 encode failed");
let total_height = img.height();
assert!(
total_height > 200,
"EAN-13 image height ({}) should exceed barcode height (200) for guard bars",
total_height
);
let bar_width = 2usize;
let extended_row = 205u32; let has_guard_extension = (0..bar_width).any(|x| {
x < img.width() as usize
&& extended_row < img.height()
&& img.get_pixel(x as u32, extended_row)[0] < 128
});
assert!(
has_guard_extension,
"guard bar at start should extend below data bar area"
);
}
#[test]
fn code39_interpretation_line_shows_asterisks() {
let zpl = "^XA^FO100,100^B3N,,100,Y^FDABC^FS^XZ";
let png = render_helpers::render_zpl_to_png(zpl, default_options());
let img = decode_png(&png);
let mut text_dark_count = 0u32;
for y in 202..220u32 {
for x in 0..img.width() {
if y < img.height() && img.get_pixel(x, y)[0] < 128 {
text_dark_count += 1;
}
}
}
assert!(
text_dark_count > 0,
"interpretation line should render below barcode"
);
}
#[test]
fn text_block_center_alignment_centers_text() {
let zpl = "^XA^FO100,100^A0N,40,40^FB400,1,0,C,^FDCenter^FS^XZ";
let png = render_helpers::render_zpl_to_png(zpl, default_options());
let img = decode_png(&png);
let mut left_most = img.width();
let mut right_most = 0u32;
for y in 100..140u32 {
for x in 100..500u32 {
if x < img.width() && y < img.height() && img.get_pixel(x, y)[0] < 128 {
left_most = left_most.min(x);
right_most = right_most.max(x);
}
}
}
let text_center = (left_most + right_most) / 2;
let diff = (text_center as i32 - 300i32).unsigned_abs();
assert!(
diff < 30,
"text center ({}) should be near block center (300), diff={}",
text_center,
diff
);
}
#[test]
fn font_0_width_ratio_produces_narrower_text() {
let zpl = "^XA^FO10,10^A0N,40,40^FDWWWWW^FS^XZ";
let png = render_helpers::render_zpl_to_png(zpl, default_options());
let img = decode_png(&png);
let mut right_most = 0u32;
for y in 10..50u32 {
for x in 10..img.width() {
if y < img.height() && img.get_pixel(x, y)[0] < 128 {
right_most = right_most.max(x);
}
}
}
let text_width = right_most - 10;
assert!(
text_width < 160,
"font 0 text width ({}) should reflect ~60% width ratio, not full 1:1",
text_width
);
}
#[test]
fn rotated90_ft_text_positions_within_expected_region() {
let zpl = "^XA\
^FO400,100^GB80,400,80,B^FS\
^FWB^FT440,500^A0,,60^FB400,1,0,C,^FDHELLO^FS\
^XZ";
let png = render_helpers::render_zpl_to_png(zpl, default_options());
let img = decode_png(&png);
let mut dark_in_box = 0u32;
for y in 100..500u32 {
for x in 400..480u32 {
if x < img.width() && y < img.height() && img.get_pixel(x, y)[0] < 128 {
dark_in_box += 1;
}
}
}
assert!(
dark_in_box > 50,
"rotated text should have dark pixels inside the box region, got {}",
dark_in_box
);
let mut dark_below = 0u32;
for y in 550..700u32 {
for x in 400..480u32 {
if x < img.width() && y < img.height() && img.get_pixel(x, y)[0] < 128 {
dark_below += 1;
}
}
}
assert!(
dark_below < 10,
"rotated text should not appear below the box, got {} dark pixels",
dark_below
);
}