use std::path::Path;
use image::RgbaImage;
use crate::elements::label_element::LabelElement;
use crate::elements::label_info::LabelInfo;
use super::error::AnalyzeError;
use super::models::{ElementBBox, ElementDiffContribution, ElementType};
pub fn compute_element_bbox(element: &LabelElement, index: usize) -> Option<ElementBBox> {
match element {
LabelElement::Text(t) => {
let font = &t.font;
let h = font.height.max(10.0) as i32;
let w = if let Some(ref block) = t.block {
block.max_width.max(1)
} else {
let char_w = (font.width.max(1.0) * font.get_scale_x()) as i32;
(char_w * t.text.len() as i32).max(1)
};
let block_h = if let Some(ref block) = t.block {
let lines = block.max_lines.max(1);
h * lines + block.line_spacing * (lines - 1)
} else {
h
};
Some(ElementBBox {
x: t.position.x,
y: t.position.y,
width: w,
height: block_h,
element_index: index,
element_type: ElementType::Text,
zpl_command: "^A/^FD".to_string(),
})
}
LabelElement::GraphicBox(g) => Some(ElementBBox {
x: g.position.x,
y: g.position.y,
width: g.width.max(1),
height: g.height.max(1),
element_index: index,
element_type: ElementType::GraphicBox,
zpl_command: "^GB".to_string(),
}),
LabelElement::GraphicCircle(g) => Some(ElementBBox {
x: g.position.x,
y: g.position.y,
width: g.circle_diameter.max(1),
height: g.circle_diameter.max(1),
element_index: index,
element_type: ElementType::GraphicCircle,
zpl_command: "^GC".to_string(),
}),
LabelElement::DiagonalLine(g) => Some(ElementBBox {
x: g.position.x,
y: g.position.y,
width: g.width.max(1),
height: g.height.max(1),
element_index: index,
element_type: ElementType::DiagonalLine,
zpl_command: "^GD".to_string(),
}),
LabelElement::GraphicField(g) => {
let rows = if g.row_bytes > 0 {
g.total_bytes / g.row_bytes
} else {
1
};
Some(ElementBBox {
x: g.position.x,
y: g.position.y,
width: (g.row_bytes * 8 * g.magnification_x).max(1),
height: (rows * g.magnification_y).max(1),
element_index: index,
element_type: ElementType::GraphicField,
zpl_command: "^GF".to_string(),
})
}
LabelElement::Barcode128(b) => {
let w = (b.width * 11 + 35).max(1); Some(ElementBBox {
x: b.position.x,
y: b.position.y,
width: w,
height: b.barcode.height.max(1),
element_index: index,
element_type: ElementType::Barcode128,
zpl_command: "^BC".to_string(),
})
}
LabelElement::BarcodeEan13(b) => {
let w = 95 * b.width.max(1);
Some(ElementBBox {
x: b.position.x,
y: b.position.y,
width: w,
height: b.barcode.height.max(1),
element_index: index,
element_type: ElementType::BarcodeEan13,
zpl_command: "^BE".to_string(),
})
}
LabelElement::Barcode2of5(b) => {
let data_len = b.data.len() as i32;
let w = ((data_len * 2 + 3) * b.width.max(1) * 3).max(1);
Some(ElementBBox {
x: b.position.x,
y: b.position.y,
width: w,
height: b.barcode.height.max(1),
element_index: index,
element_type: ElementType::Barcode2of5,
zpl_command: "^B2".to_string(),
})
}
LabelElement::Barcode39(b) => {
let data_len = b.data.len() as i32;
let w = ((data_len + 2) * 13 * b.width.max(1)).max(1);
Some(ElementBBox {
x: b.position.x,
y: b.position.y,
width: w,
height: b.barcode.height.max(1),
element_index: index,
element_type: ElementType::Barcode39,
zpl_command: "^B3".to_string(),
})
}
LabelElement::BarcodePdf417(b) => {
let cols = b.barcode.columns.max(1);
let rows = b.barcode.rows.max(3);
let mw = b.barcode.module_width.max(1);
let w = (cols * 17 + 69) * mw;
let h = rows * b.barcode.row_height.max(1);
Some(ElementBBox {
x: b.position.x,
y: b.position.y,
width: w.max(1),
height: h.max(1),
element_index: index,
element_type: ElementType::BarcodePdf417,
zpl_command: "^B7".to_string(),
})
}
LabelElement::BarcodeAztec(b) => {
let mag = b.barcode.magnification.max(1);
let size = if b.barcode.size > 0 {
b.barcode.size * mag
} else {
50 * mag
};
Some(ElementBBox {
x: b.position.x,
y: b.position.y,
width: size.max(1),
height: size.max(1),
element_index: index,
element_type: ElementType::BarcodeAztec,
zpl_command: "^BO".to_string(),
})
}
LabelElement::BarcodeDatamatrix(b) => {
let h = b.barcode.height.max(1);
let size = h * 10; Some(ElementBBox {
x: b.position.x,
y: b.position.y,
width: size.max(1),
height: size.max(1),
element_index: index,
element_type: ElementType::BarcodeDatamatrix,
zpl_command: "^BX".to_string(),
})
}
LabelElement::BarcodeQr(b) => {
let mag = b.barcode.magnification.max(1);
let size = (21 + 4) * mag; Some(ElementBBox {
x: b.position.x,
y: b.position.y,
width: size.max(1) * b.height.max(1),
height: size.max(1) * b.height.max(1),
element_index: index,
element_type: ElementType::BarcodeQr,
zpl_command: "^BQ".to_string(),
})
}
LabelElement::Maxicode(m) => {
Some(ElementBBox {
x: m.position.x,
y: m.position.y,
width: 200,
height: 200,
element_index: index,
element_type: ElementType::Maxicode,
zpl_command: "^BD".to_string(),
})
}
_ => None,
}
}
pub fn count_red_pixels(img: &RgbaImage) -> u64 {
let mut count = 0u64;
for pixel in img.pixels() {
if pixel[0] > 200 && pixel[1] < 50 && pixel[2] < 50 && pixel[3] > 200 {
count += 1;
}
}
count
}
pub fn count_red_pixels_in_rect(img: &RgbaImage, x1: i32, y1: i32, x2: i32, y2: i32) -> u64 {
let mut count = 0u64;
let img_w = img.width() as i32;
let img_h = img.height() as i32;
let x1 = x1.max(0);
let y1 = y1.max(0);
let x2 = x2.min(img_w);
let y2 = y2.min(img_h);
for y in y1..y2 {
for x in x1..x2 {
let pixel = img.get_pixel(x as u32, y as u32);
if pixel[0] > 200 && pixel[1] < 50 && pixel[2] < 50 && pixel[3] > 200 {
count += 1;
}
}
}
count
}
pub fn correlate_diff_regions(
diff_image: &RgbaImage,
bboxes: &[ElementBBox],
) -> Vec<ElementDiffContribution> {
let total_red = count_red_pixels(diff_image);
if total_red == 0 {
return Vec::new();
}
let mut contributions: Vec<ElementDiffContribution> = bboxes
.iter()
.map(|bbox| {
let diff_in_bbox = count_red_pixels_in_rect(
diff_image,
bbox.x,
bbox.y,
bbox.x + bbox.width,
bbox.y + bbox.height,
);
let total_in_bbox = (bbox.width as u64) * (bbox.height as u64);
let local_diff = if total_in_bbox > 0 {
diff_in_bbox as f64 / total_in_bbox as f64 * 100.0
} else {
0.0
};
let contribution = diff_in_bbox as f64 / total_red as f64 * 100.0;
ElementDiffContribution {
bbox: bbox.clone(),
diff_pixels_in_bbox: diff_in_bbox,
total_pixels_in_bbox: total_in_bbox,
local_diff_percent: local_diff,
contribution_to_total: contribution,
classification: None,
position_offset: None,
}
})
.collect();
contributions.sort_by(|a, b| {
b.contribution_to_total
.partial_cmp(&a.contribution_to_total)
.unwrap_or(std::cmp::Ordering::Equal)
});
contributions
}
pub fn analyze_label(
label: &LabelInfo,
diff_image_path: &Path,
) -> Result<Vec<ElementDiffContribution>, AnalyzeError> {
let bboxes: Vec<ElementBBox> = label
.elements
.iter()
.enumerate()
.filter_map(|(i, el)| compute_element_bbox(el, i))
.collect();
if bboxes.is_empty() {
return Err(AnalyzeError::NoElements);
}
let diff_img = image::open(diff_image_path)
.map_err(|_| AnalyzeError::DiffImageNotFound {
path: diff_image_path.display().to_string(),
})?
.to_rgba8();
Ok(correlate_diff_regions(&diff_img, &bboxes))
}
pub fn analyze_label_with_classification(
label: &LabelInfo,
diff_image_path: &Path,
label_zpl: &str,
use_labelary: bool,
) -> Result<Vec<ElementDiffContribution>, AnalyzeError> {
use super::diff_classifier::{classify_element_diffs, ClassifyOptions};
let mut contributions = analyze_label(label, diff_image_path)?;
let diff_img = image::open(diff_image_path)
.map_err(|_| AnalyzeError::DiffImageNotFound {
path: diff_image_path.display().to_string(),
})?
.to_rgba8();
let classify_opts = ClassifyOptions {
use_labelary,
..Default::default()
};
classify_element_diffs(&mut contributions, label_zpl, &diff_img, &classify_opts)?;
Ok(contributions)
}
pub fn format_analysis_report(contributions: &[ElementDiffContribution]) -> String {
use super::models::{DiffClassification, PositionOffsetInfo};
let mut report = String::new();
report.push_str("Element Diff Contribution Analysis\n");
report.push_str("==================================\n\n");
report.push_str(&format!(
"{:<6} {:<16} {:<8} {:<12} {:<8} {:<10} {:<14} {}\n",
"Index", "Type", "Command", "DiffPixels", "Local%", "Contrib%", "Classification", "Offset"
));
report.push_str(&"-".repeat(95));
report.push('\n');
for c in contributions {
if c.diff_pixels_in_bbox == 0 {
continue;
}
let class_str = match &c.classification {
None => "-".to_string(),
Some(DiffClassification::ContentDiff) => "Content".to_string(),
Some(DiffClassification::PositionDiff) => "Position".to_string(),
Some(DiffClassification::Mixed) => "Mixed".to_string(),
};
let offset_str = match &c.position_offset {
None => String::new(),
Some(PositionOffsetInfo {
dx, dy, confidence, ..
}) => {
format!("dx={:+} dy={:+} ({:.0}%)", dx, dy, confidence * 100.0)
}
};
report.push_str(&format!(
"{:<6} {:<16} {:<8} {:<12} {:<8.2} {:<10.2} {:<14} {}\n",
c.bbox.element_index,
format!("{:?}", c.bbox.element_type),
c.bbox.zpl_command,
c.diff_pixels_in_bbox,
c.local_diff_percent,
c.contribution_to_total,
class_str,
offset_str,
));
}
report
}