use anyhow::{bail, Context, Result}; use base64::Engine; use eframe::egui; use printpdf::*; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use crate::core::print::parsing::{PrinterMargins, PrinterSettings, ScaleMode}; // ----------------------------------------------------------------------------- // Constants // ----------------------------------------------------------------------------- const POINTS_TO_MM: f32 = 0.352_777_78; // 1 pt -> mm const TEXT_DESCENT_RATIO: f32 = 0.2; // Fallback page if no printer settings provided const DEFAULT_CANVAS_WIDTH_MM: f32 = 100.0; const DEFAULT_CANVAS_HEIGHT_MM: f32 = 50.0; // ----------------------------------------------------------------------------- // Grid / Space definition for new layout system // ----------------------------------------------------------------------------- #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct LayoutSpace { pub width: f32, pub height: f32, } impl Default for LayoutSpace { fn default() -> Self { Self { width: 256.0, height: 128.0, } } } // ----------------------------------------------------------------------------- // Core layout structs (grid-based) // ----------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LabelLayout { #[serde(default)] pub background: Option, #[serde(default)] pub space: LayoutSpace, #[serde(default)] pub elements: Vec, } fn default_font_size() -> f32 { 12.0 } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "lowercase")] pub enum LabelElement { Text { field: String, x: f32, y: f32, #[serde(rename = "fontSize", default = "default_font_size")] font_size: f32, #[serde(rename = "fontWeight", default)] font_weight: Option, #[serde(rename = "fontFamily", default)] font_family: Option, #[serde(rename = "maxWidth", default)] max_width: Option, #[serde(default)] wrap: Option, #[serde(default)] color: Option, }, QrCode { field: String, x: f32, y: f32, size: f32, #[serde(rename = "showText", default)] show_text: Option, }, Barcode { field: String, x: f32, y: f32, width: f32, height: f32, #[serde(default)] format: Option, #[serde(rename = "showText", default)] show_text: Option, }, DataMatrix { field: String, x: f32, y: f32, size: f32, #[serde(rename = "showText", default)] show_text: Option, }, Rect { x: f32, y: f32, width: f32, height: f32, #[serde(default)] fill: Option, }, Svg { data: String, x: f32, y: f32, width: f32, height: f32, }, } #[derive(Debug, Clone)] pub struct LabelRenderer { pub layout: LabelLayout, } impl LabelRenderer { pub fn new(layout: LabelLayout) -> Self { Self { layout } } } // Bounds actually used by elements (tight box) #[derive(Debug, Clone, Copy)] struct LayoutBounds { min_x: f32, min_y: f32, max_x: f32, max_y: f32, } impl LayoutBounds { fn empty() -> Self { Self { min_x: f32::INFINITY, min_y: f32::INFINITY, max_x: f32::NEG_INFINITY, max_y: f32::NEG_INFINITY, } } fn is_empty(&self) -> bool { !self.min_x.is_finite() } fn extend_point(&mut self, x: f32, y: f32) { self.min_x = self.min_x.min(x); self.min_y = self.min_y.min(y); self.max_x = self.max_x.max(x); self.max_y = self.max_y.max(y); } fn extend_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { let (min_x, max_x) = if w >= 0.0 { (x, x + w) } else { (x + w, x) }; let (min_y, max_y) = if h >= 0.0 { (y, y + h) } else { (y + h, y) }; self.extend_point(min_x, min_y); self.extend_point(max_x, max_y); } fn width(&self) -> f32 { (self.max_x - self.min_x).max(0.0) } fn height(&self) -> f32 { (self.max_y - self.min_y).max(0.0) } fn normalize_x(&self, x: f32) -> f32 { if self.min_x.is_finite() { x - self.min_x } else { x } } fn normalize_y(&self, y: f32) -> f32 { if self.min_y.is_finite() { y - self.min_y } else { y } } } // LayoutTransform maps layout-space units onto final mm coordinates #[derive(Debug, Clone)] #[allow(dead_code)] struct LayoutTransform { bounds: LayoutBounds, page_width: f32, page_height: f32, printable_width: f32, printable_height: f32, scale_x: f32, scale_y: f32, uniform_scale: f32, offset_x: f32, offset_y: f32, rendered_width: f32, rendered_height: f32, margins: PrinterMargins, } impl LayoutTransform { fn new(bounds: LayoutBounds, settings: Option<&PrinterSettings>) -> Result { let margins = settings.map(|s| s.margins.clone()).unwrap_or_default(); let (page_w, page_h) = if let Some(s) = settings { // Respect printer-provided orientation (already canonicalized by get_dimensions_mm) s.get_dimensions_mm() } else { // No settings: default preview page matches design aspect ( bounds.width().max(DEFAULT_CANVAS_WIDTH_MM), bounds.height().max(DEFAULT_CANVAS_HEIGHT_MM), ) }; let printable_w = (page_w - margins.left - margins.right).max(1.0); let printable_h = (page_h - margins.top - margins.bottom).max(1.0); let design_w = bounds.width().max(1.0); let design_h = bounds.height().max(1.0); let scale_mode = settings.map(|s| s.scale_mode).unwrap_or(ScaleMode::Fit); let user_factor = settings.map(|s| s.scale_factor).unwrap_or(1.0).max(0.0); let mut sx = printable_w / design_w; let mut sy = printable_h / design_h; match scale_mode { ScaleMode::Fit => { let uni = sx.min(sy); sx = uni; sy = uni; } ScaleMode::FitX => { sy = sx; } ScaleMode::FitY => { sx = sy; } ScaleMode::MaxBoth => { /* stretch independently */ } ScaleMode::MaxX => { sy = sx; } ScaleMode::MaxY => { sx = sy; } ScaleMode::Manual => { sx = user_factor; sy = user_factor; } } sx *= user_factor; sy *= user_factor; // Manual already multiplies; harmless if 1.0 if !sx.is_finite() || sx <= 0.0 { sx = 1.0; } if !sy.is_finite() || sy <= 0.0 { sy = 1.0; } let uniform = sx.min(sy); let rendered_w = design_w * sx; let rendered_h = design_h * sy; // Centering let mut offset_x = margins.left; let mut offset_y = margins.top; if let Some(s) = settings { if let Some(center_mode) = s.center.filter(|_| !s.center_disabled) { if center_mode.includes_horizontal() { let extra = printable_w - rendered_w; if extra > 0.0 { offset_x = margins.left + extra / 2.0; } } if center_mode.includes_vertical() { let extra = printable_h - rendered_h; if extra > 0.0 { offset_y = margins.top + extra / 2.0; } } } } log::info!("layout_transform: page {:.2}x{:.2}mm printable {:.2}x{:.2}mm design {:.2}x{:.2} units scale_x {:.4} scale_y {:.4} uniform {:.4} offsets {:.2},{:.2}", page_w, page_h, printable_w, printable_h, design_w, design_h, sx, sy, uniform, offset_x, offset_y); Ok(Self { bounds, page_width: page_w, page_height: page_h, printable_width: printable_w, printable_height: printable_h, scale_x: sx, scale_y: sy, uniform_scale: uniform, offset_x, offset_y, rendered_width: rendered_w, rendered_height: rendered_h, margins, }) } fn x_mm(&self, x: f32) -> f32 { self.offset_x + self.scale_x * self.bounds.normalize_x(x) } fn y_mm(&self, y: f32) -> f32 { self.offset_y + self.scale_y * self.bounds.normalize_y(y) } fn width_mm(&self, w: f32) -> f32 { self.scale_x * w } fn height_mm(&self, h: f32) -> f32 { self.scale_y * h } fn uniform_mm(&self, s: f32) -> f32 { self.uniform_scale * s } } impl LabelRenderer { fn render_pdf_internal( &self, data: &HashMap, printer_settings: Option<&PrinterSettings>, ) -> Result<( PdfDocumentReference, PdfPageIndex, PdfLayerIndex, LayoutTransform, )> { let bounds = self.calculate_layout_bounds()?; let transform = LayoutTransform::new(bounds, printer_settings)?; let (doc, page_index, layer_index) = PdfDocument::new( "BeepZone Label", Mm(transform.page_width), Mm(transform.page_height), "Layer 1", ); let font = doc .add_builtin_font(printpdf::BuiltinFont::Helvetica) .context("Failed to add Helvetica font")?; let layer = doc.get_page(page_index).get_layer(layer_index); self.render_pdf_elements(&layer, &font, data, &transform)?; Ok((doc, page_index, layer_index, transform)) } fn render_pdf_elements( &self, layer: &PdfLayerReference, font: &IndirectFontRef, data: &HashMap, transform: &LayoutTransform, ) -> Result<()> { for element in &self.layout.elements { match element { LabelElement::Text { field, x, y, font_size, color, max_width, wrap, .. } => { let value = Self::resolve_field(field, data); let x_mm = transform.x_mm(*x); let y_mm = transform.y_mm(*y); let font_pt = (*font_size * transform.uniform_scale).max(0.1); let color_ref = color.as_deref(); let wrap_enabled = wrap.unwrap_or(false); if wrap_enabled { if let Some(max_w) = max_width { let max_w_mm = transform.width_mm(*max_w); let lines = Self::wrap_lines(&value, max_w_mm, font_pt); let line_gap_mm = font_pt * POINTS_TO_MM * 1.2; for (i, line) in lines.iter().enumerate() { let line_top_mm = y_mm + (i as f32) * line_gap_mm; let baseline = Self::layout_text_baseline( line_top_mm, font_pt, transform.page_height, ); self.render_text_to_pdf( layer, font, x_mm, baseline, font_pt, line, color_ref, )?; } } else { let baseline = Self::layout_text_baseline(y_mm, font_pt, transform.page_height); self.render_text_to_pdf( layer, font, x_mm, baseline, font_pt, &value, color_ref, )?; } } else { let baseline = Self::layout_text_baseline(y_mm, font_pt, transform.page_height); self.render_text_to_pdf( layer, font, x_mm, baseline, font_pt, &value, color_ref, )?; } } LabelElement::QrCode { field, x, y, size, show_text, .. } => { let value = Self::resolve_field(field, data); let x_mm = transform.x_mm(*x); let y_mm = transform.y_mm(*y); let side_mm = transform.uniform_mm(*size); let y_bottom = Self::layout_top_to_pdf_bottom(y_mm, side_mm, transform.page_height); self.render_qrcode_to_pdf(layer, x_mm, y_bottom, side_mm, &value)?; if show_text.unwrap_or(false) { let gap_mm = 1.5 * transform.uniform_scale; let label_top_mm = y_mm + side_mm + gap_mm; let label_font_pt = 10.0 * transform.uniform_scale; let baseline = Self::layout_text_baseline( label_top_mm, label_font_pt, transform.page_height, ); self.render_text_to_pdf( layer, font, x_mm, baseline, label_font_pt, &value, None, )?; } } LabelElement::DataMatrix { field, x, y, size, show_text, } => { let value = Self::resolve_field(field, data); let x_mm = transform.x_mm(*x); let y_mm = transform.y_mm(*y); let side_mm = transform.uniform_mm(*size); let y_bottom = Self::layout_top_to_pdf_bottom(y_mm, side_mm, transform.page_height); self.render_datamatrix_to_pdf(layer, x_mm, y_bottom, side_mm, &value)?; if show_text.unwrap_or(false) { let gap_mm = 1.5 * transform.uniform_scale; let label_top_mm = y_mm + side_mm + gap_mm; let label_font_pt = 8.0 * transform.uniform_scale; let baseline = Self::layout_text_baseline( label_top_mm, label_font_pt, transform.page_height, ); self.render_text_to_pdf( layer, font, x_mm, baseline, label_font_pt, &value, None, )?; } } LabelElement::Barcode { field, x, y, width, height, format, show_text, } => { let value = Self::resolve_field(field, data); let x_mm = transform.x_mm(*x); let y_mm = transform.y_mm(*y); let width_mm = transform.width_mm(*width); let height_mm = transform.height_mm(*height); let y_bottom = Self::layout_top_to_pdf_bottom(y_mm, height_mm, transform.page_height); self.render_barcode_to_pdf( layer, x_mm, y_bottom, width_mm, height_mm, &value, format.as_deref(), )?; if show_text.unwrap_or(false) { let gap_mm = 1.5 * transform.uniform_scale; let label_top_mm = y_mm + height_mm + gap_mm; let label_font_pt = 8.0 * transform.uniform_scale; let baseline = Self::layout_text_baseline( label_top_mm, label_font_pt, transform.page_height, ); self.render_text_to_pdf( layer, font, x_mm, baseline, label_font_pt, &value, None, )?; } } LabelElement::Rect { x, y, width, height, fill, } => { let x_mm = transform.x_mm(*x); let y_mm = transform.y_mm(*y); let width_mm = transform.width_mm(*width); let height_mm = transform.height_mm(*height); let y_bottom = Self::layout_top_to_pdf_bottom(y_mm, height_mm, transform.page_height); self.render_rect_to_pdf( layer, x_mm, y_bottom, width_mm, height_mm, fill.as_deref(), )?; } LabelElement::Svg { x, y, width, height, data, } => { let x_mm = transform.x_mm(*x); let y_mm = transform.y_mm(*y); let width_mm = transform.width_mm(*width).max(0.1); let height_mm = transform.height_mm(*height).max(0.1); let y_bottom = Self::layout_top_to_pdf_bottom(y_mm, height_mm, transform.page_height); if let Some(svg_xml) = Self::decode_svg_data_uri(data) { let px_w = (width_mm * 3.78).ceil().max(1.0) as u32; let px_h = (height_mm * 3.78).ceil().max(1.0) as u32; if let Some(rgba) = Self::rasterize_svg_to_rgba(&svg_xml, px_w, px_h) { let mut rgb: Vec = Vec::with_capacity((px_w * px_h * 3) as usize); for chunk in rgba.chunks(4) { let (r, g, b, a) = (chunk[0], chunk[1], chunk[2], chunk[3]); if a == 255 { rgb.extend_from_slice(&[r, g, b]); } else { let af = a as f32 / 255.0; let blend = |c: u8| { ((c as f32 * af) + 255.0 * (1.0 - af)).round() as u8 }; rgb.extend_from_slice(&[blend(r), blend(g), blend(b)]); } } let image_xobj = printpdf::ImageXObject { width: printpdf::Px(px_w as usize), height: printpdf::Px(px_h as usize), color_space: printpdf::ColorSpace::Rgb, bits_per_component: printpdf::ColorBits::Bit8, interpolate: true, image_data: rgb, image_filter: None, clipping_bbox: None, smask: None, }; let image = printpdf::Image::from(image_xobj); let base_w_mm = (px_w as f32) * 25.4 / 300.0; let base_h_mm = (px_h as f32) * 25.4 / 300.0; let sx = if base_w_mm > 0.0 { width_mm / base_w_mm } else { 1.0 }; let sy = if base_h_mm > 0.0 { height_mm / base_h_mm } else { 1.0 }; let transform_img = printpdf::ImageTransform { translate_x: Some(printpdf::Mm(x_mm)), translate_y: Some(printpdf::Mm(y_bottom)), rotate: None, scale_x: Some(sx), scale_y: Some(sy), dpi: Some(300.0), }; image.add_to_layer(layer.clone(), transform_img); } else { self.render_rect_to_pdf( layer, x_mm, y_bottom, width_mm, height_mm, Some("#DDDDDD"), )?; } } else { self.render_rect_to_pdf( layer, x_mm, y_bottom, width_mm, height_mm, Some("#EEEEEE"), )?; } } } } Ok(()) } fn resolve_field(field: &str, data: &HashMap) -> String { if !field.contains("{{") { return data .get(field) .cloned() .unwrap_or_else(|| field.to_string()); } let mut result = String::new(); let mut rest = field; while let Some(open) = rest.find("{{") { let (prefix, tail) = rest.split_at(open); result.push_str(prefix); if let Some(close) = tail.find("}}") { let var = tail[2..close].trim(); // Exact match first, then case-insensitive fallback if let Some(value) = data.get(var) { result.push_str(value); } else if let Some((_, v)) = data.iter().find(|(k, _)| k.eq_ignore_ascii_case(var)) { result.push_str(v); } // else: missing vars become empty string rest = &tail[close + 2..]; } else { result.push_str(tail); return result; } } result.push_str(rest); result } fn calculate_layout_bounds(&self) -> Result { let space = self.layout.space; if !space.width.is_finite() || !space.height.is_finite() { bail!("layout space must provide finite width and height"); } if space.width <= 0.0 || space.height <= 0.0 { bail!("layout space must define positive width and height"); } let mut used = LayoutBounds::empty(); for element in &self.layout.elements { match element { LabelElement::Text { x, y, font_size, max_width, .. } => { let height = (*font_size * POINTS_TO_MM).max(0.1); // Wider heuristic for text width so long strings trigger downscaling. let width = max_width .and_then(|w| { if w.is_finite() && w > 0.0 { Some(w) } else { None } }) .unwrap_or_else(|| (*font_size * POINTS_TO_MM * 8.5).max(1.0)); used.extend_rect(*x, *y, width, height); } LabelElement::QrCode { x, y, size, .. } | LabelElement::DataMatrix { x, y, size, .. } => { used.extend_rect(*x, *y, *size, *size); } LabelElement::Barcode { x, y, width, height, .. } => { used.extend_rect(*x, *y, *width, *height); } LabelElement::Rect { x, y, width, height, .. } => { used.extend_rect(*x, *y, *width, *height); } LabelElement::Svg { x, y, width, height, .. } => { used.extend_rect(*x, *y, *width, *height); } } } if used.is_empty() { return Ok(LayoutBounds { min_x: 0.0, min_y: 0.0, max_x: space.width, max_y: space.height, }); } let mut bounds = used; bounds.min_x = bounds.min_x.max(0.0); bounds.min_y = bounds.min_y.max(0.0); let min_width = (space.width * 0.01).max(1.0); let min_height = (space.height * 0.01).max(1.0); if bounds.width() < min_width { bounds.min_x = 0.0; bounds.max_x = space.width; } else if bounds.max_x > space.width { // allow overhang but ensure width positive bounds.max_x = bounds.max_x.max(bounds.min_x + min_width); } if bounds.height() < min_height { bounds.min_y = 0.0; bounds.max_y = space.height; } else if bounds.max_y > space.height { bounds.max_y = bounds.max_y.max(bounds.min_y + min_height); } // No seal() needed; bounds already finalized Ok(bounds) } fn parse_hex_color(hex: &str) -> Option { let raw = hex.trim(); let raw = raw.strip_prefix('#').unwrap_or(raw); if raw.len() != 6 { return None; } let r = u8::from_str_radix(&raw[0..2], 16).ok()?; let g = u8::from_str_radix(&raw[2..4], 16).ok()?; let b = u8::from_str_radix(&raw[4..6], 16).ok()?; Some(egui::Color32::from_rgb(r, g, b)) } #[allow(dead_code)] pub fn generate_pdf( &self, data: &HashMap, ) -> Result<(PdfDocumentReference, PdfPageIndex, PdfLayerIndex)> { let (doc, page, layer, _) = self.render_pdf_internal(data, None)?; Ok((doc, page, layer)) } pub fn generate_pdf_with_settings( &self, data: &HashMap, printer_settings: &PrinterSettings, ) -> Result<(PdfDocumentReference, PdfPageIndex, PdfLayerIndex)> { let (doc, page, layer, _) = self.render_pdf_internal(data, Some(printer_settings))?; Ok((doc, page, layer)) } // Removed legacy template bounds calculation (numeric widths now direct) fn render_text_to_pdf( &self, layer: &PdfLayerReference, font: &IndirectFontRef, x: f32, baseline_y: f32, font_size_pt: f32, text: &str, color: Option<&str>, ) -> Result<()> { let (r, g, b) = color.map(Self::parse_hex_to_rgb).unwrap_or((0.0, 0.0, 0.0)); layer.set_fill_color(printpdf::Color::Rgb(printpdf::Rgb::new(r, g, b, None))); layer.use_text(text, font_size_pt, Mm(x), Mm(baseline_y), font); Ok(()) } fn render_qrcode_to_pdf( &self, layer: &PdfLayerReference, x: f32, y_bottom: f32, size: f32, data: &str, ) -> Result<()> { use qrcodegen::{QrCode, QrCodeEcc}; let qr = QrCode::encode_text(data, QrCodeEcc::Medium).context("Failed to generate QR code")?; let qr_size = qr.size() as usize; let module_mm = size / qr_size as f32; layer.set_fill_color(printpdf::Color::Rgb(printpdf::Rgb::new( 0.0, 0.0, 0.0, None, ))); for y in 0..qr_size { for x_idx in 0..qr_size { if qr.get_module(x_idx as i32, y as i32) { let px = x + (x_idx as f32 * module_mm); let py = y_bottom + ((qr_size - 1 - y) as f32 * module_mm); Self::draw_filled_rect(layer, px, py, module_mm, module_mm); } } } Ok(()) } fn render_datamatrix_to_pdf( &self, layer: &PdfLayerReference, x: f32, y_bottom: f32, size: f32, data: &str, ) -> Result<()> { use datamatrix::{DataMatrix, SymbolList}; let encoded = match DataMatrix::encode_str(data, SymbolList::default()) { Ok(dm) => dm, Err(e) => { log::error!("Failed to generate DataMatrix for '{}': {:?}", data, e); return Ok(()); } }; let bmp = encoded.bitmap(); let rows = bmp.height() as usize; let cols = bmp.width() as usize; if rows == 0 || cols == 0 { return Ok(()); } let module_mm = size / rows.max(cols) as f32; layer.set_fill_color(printpdf::Color::Rgb(printpdf::Rgb::new( 0.0, 0.0, 0.0, None, ))); for (px_idx, py_idx) in bmp.pixels() { // (x,y) let px_mm = x + px_idx as f32 * module_mm; let py_mm = y_bottom + ((rows - 1 - py_idx) as f32 * module_mm); Self::draw_filled_rect(layer, px_mm, py_mm, module_mm, module_mm); } Ok(()) } fn render_barcode_to_pdf( &self, layer: &PdfLayerReference, x: f32, y_bottom: f32, width: f32, height: f32, data: &str, format: Option<&str>, ) -> Result<()> { use barcoders::sym::{code11::Code11, code128::Code128}; // Choose symbology enum Sym { C128(String), C11(String), } let sym = match format.map(|s| s.to_lowercase()) { Some(ref f) if f == "code11" => { // Code11 supports digits and '-' let cleaned: String = data .chars() .filter(|c| c.is_ascii_digit() || *c == '-') .collect(); if cleaned.is_empty() { log::warn!("Skipping Code11 - invalid payload: '{}'", data); return Ok(()); } Sym::C11(cleaned) } _ => { // Default Code128 with smart preparation match Self::prepare_code128_payload(data) { Some(p) => Sym::C128(p), None => { log::warn!("Skipping barcode - unsupported payload: '{}'", data); return Ok(()); } } } }; let modules: Vec = match sym { Sym::C128(p) => match Code128::new(&p) { Ok(c) => c.encode(), Err(e) => { log::error!("Code128 encode failed: {:?}", e); return Ok(()); } }, Sym::C11(p) => match Code11::new(&p) { Ok(c) => c.encode(), Err(e) => { log::error!("Code11 encode failed: {:?}", e); return Ok(()); } }, }; if modules.is_empty() { log::warn!("Barcode produced no modules"); return Ok(()); } let module_width = width / modules.len() as f32; if module_width <= 0.0 { log::warn!("Computed non-positive module width, skipping"); return Ok(()); } layer.set_fill_color(printpdf::Color::Rgb(printpdf::Rgb::new( 0.0, 0.0, 0.0, None, ))); let mut run_start: Option = None; for (idx, bit) in modules.iter().enumerate() { if *bit == 1 { run_start.get_or_insert(idx); } else if let Some(start) = run_start.take() { let bar_start = x + start as f32 * module_width; let bar_width = (idx - start) as f32 * module_width; if bar_width > 0.0 { Self::draw_filled_rect(layer, bar_start, y_bottom, bar_width, height); } } } if let Some(start) = run_start.take() { let bar_start = x + start as f32 * module_width; let bar_width = (modules.len() - start) as f32 * module_width; if bar_width > 0.0 { Self::draw_filled_rect(layer, bar_start, y_bottom, bar_width, height); } } Ok(()) } fn render_rect_to_pdf( &self, layer: &PdfLayerReference, x: f32, y_bottom: f32, width_mm: f32, height_mm: f32, fill: Option<&str>, ) -> Result<()> { use printpdf::path::{PaintMode, WindingOrder}; let (r, g, b) = fill.map(Self::parse_hex_to_rgb).unwrap_or((0.5, 0.5, 0.5)); layer.set_fill_color(printpdf::Color::Rgb(printpdf::Rgb::new(r, g, b, None))); let points = vec![ (Point::new(Mm(x), Mm(y_bottom)), false), (Point::new(Mm(x + width_mm), Mm(y_bottom)), false), ( Point::new(Mm(x + width_mm), Mm(y_bottom + height_mm)), false, ), (Point::new(Mm(x), Mm(y_bottom + height_mm)), false), ]; let polygon = printpdf::Polygon { rings: vec![points], mode: PaintMode::Fill, winding_order: WindingOrder::NonZero, }; layer.add_polygon(polygon); Ok(()) } fn parse_hex_to_rgb(hex: &str) -> (f32, f32, f32) { let raw = hex.trim(); let raw = raw.strip_prefix('#').unwrap_or(raw); if raw.len() != 6 { return (0.0, 0.0, 0.0); } let r = u8::from_str_radix(&raw[0..2], 16).unwrap_or(0) as f32 / 255.0; let g = u8::from_str_radix(&raw[2..4], 16).unwrap_or(0) as f32 / 255.0; let b = u8::from_str_radix(&raw[4..6], 16).unwrap_or(0) as f32 / 255.0; (r, g, b) } fn draw_filled_rect(layer: &PdfLayerReference, x: f32, y_bottom: f32, width: f32, height: f32) { use printpdf::path::{PaintMode, WindingOrder}; let points = vec![ (Point::new(Mm(x), Mm(y_bottom)), false), (Point::new(Mm(x + width), Mm(y_bottom)), false), (Point::new(Mm(x + width), Mm(y_bottom + height)), false), (Point::new(Mm(x), Mm(y_bottom + height)), false), ]; let polygon = printpdf::Polygon { rings: vec![points], mode: PaintMode::Fill, winding_order: WindingOrder::NonZero, }; layer.add_polygon(polygon); } fn layout_top_to_pdf_bottom(y_top: f32, element_height: f32, page_height: f32) -> f32 { page_height - y_top - element_height } fn layout_text_baseline(y_top: f32, font_size_pt: f32, page_height: f32) -> f32 { let text_height_mm = font_size_pt * POINTS_TO_MM; let bottom = Self::layout_top_to_pdf_bottom(y_top, text_height_mm, page_height); bottom + text_height_mm * TEXT_DESCENT_RATIO } // Removed resolve_rect_width: Rect.width is now numeric grid units directly pub fn from_json(raw: &str) -> Result { let json = if raw.trim_start().starts_with('{') { raw.to_string() } else { // Attempt base64 decode; fall back to raw match base64::engine::general_purpose::STANDARD.decode(raw) { Ok(bytes) => String::from_utf8(bytes).unwrap_or_else(|_| raw.to_string()), Err(_) => raw.to_string(), } }; let layout: LabelLayout = serde_json::from_str(&json).context("Failed to parse label layout JSON")?; Ok(LabelRenderer::new(layout)) } pub fn render_preview( &self, ui: &mut egui::Ui, data: &HashMap, preview_scale: f32, printer_settings: Option<&PrinterSettings>, ) -> Result<()> { let bounds = self.calculate_layout_bounds()?; let transform = LayoutTransform::new(bounds, printer_settings)?; let canvas_w_px = (transform.page_width * preview_scale).ceil().max(1.0); let canvas_h_px = (transform.page_height * preview_scale).ceil().max(1.0); let (resp, painter) = ui.allocate_painter(egui::vec2(canvas_w_px, canvas_h_px), egui::Sense::hover()); let rect = resp.rect; // Background let page_bg = egui::Color32::from_rgb(250, 250, 250); painter.rect_filled(rect, egui::CornerRadius::ZERO, page_bg); // Draw printable area to visualize margins let printable_rect = egui::Rect::from_min_size( egui::pos2( rect.min.x + transform.margins.left * preview_scale, rect.min.y + transform.margins.top * preview_scale, ), egui::vec2( transform.printable_width * preview_scale, transform.printable_height * preview_scale, ), ); let printable_bg = self .layout .background .as_deref() .and_then(Self::parse_hex_color) .unwrap_or(egui::Color32::WHITE); painter.rect_filled(printable_rect, egui::CornerRadius::ZERO, printable_bg); for element in &self.layout.elements { match element { LabelElement::Text { field, x, y, font_size, color, max_width, wrap, .. } => { let value = Self::resolve_field(field, data); let x_px = transform.x_mm(*x) * preview_scale; let y_top_mm = transform.y_mm(*y); let font_pt = (*font_size * transform.uniform_scale).max(0.5); let color32 = color .as_deref() .and_then(Self::parse_hex_color) .unwrap_or(egui::Color32::BLACK); let line_height_mm = font_pt * POINTS_TO_MM * 1.2; let lines: Vec = if wrap.unwrap_or(false) && max_width.is_some() { Self::wrap_lines(&value, transform.width_mm(max_width.unwrap()), font_pt) } else { vec![value] }; for (i, line) in lines.iter().enumerate() { let line_y_mm = y_top_mm + i as f32 * line_height_mm; let baseline_mm = line_y_mm + font_pt * POINTS_TO_MM * (1.0 - TEXT_DESCENT_RATIO); let baseline_px = baseline_mm * preview_scale; painter.text( egui::pos2(rect.min.x + x_px, rect.min.y + baseline_px), egui::Align2::LEFT_BOTTOM, line, egui::FontId::proportional(font_pt), color32, ); } } LabelElement::QrCode { field, x, y, size, show_text, .. } => { let value = Self::resolve_field(field, data); let side_mm = transform.uniform_mm(*size).max(0.1); let x_px = transform.x_mm(*x) * preview_scale; let y_px = transform.y_mm(*y) * preview_scale; let side_px = side_mm * preview_scale; // Simple placeholder squares for modules (not rendering actual QR in preview for speed) painter.rect_filled( egui::Rect::from_min_size( egui::pos2(rect.min.x + x_px, rect.min.y + y_px), egui::vec2(side_px, side_px), ), egui::CornerRadius::ZERO, egui::Color32::BLACK, ); if show_text.unwrap_or(false) { let font_pt = 10.0 * transform.uniform_scale; let text_y_px = rect.min.y + (transform.y_mm(*y) + side_mm + 1.5 * transform.uniform_scale) * preview_scale + font_pt * POINTS_TO_MM * (1.0 - TEXT_DESCENT_RATIO) * preview_scale; painter.text( egui::pos2(rect.min.x + x_px, text_y_px), egui::Align2::LEFT_BOTTOM, &value, egui::FontId::proportional(font_pt), egui::Color32::BLACK, ); } } LabelElement::DataMatrix { field, x, y, size, show_text, } => { let value = Self::resolve_field(field, data); let side_mm = transform.uniform_mm(*size).max(0.1); let x_px = transform.x_mm(*x) * preview_scale; let y_px = transform.y_mm(*y) * preview_scale; let side_px = side_mm * preview_scale; painter.rect_filled( egui::Rect::from_min_size( egui::pos2(rect.min.x + x_px, rect.min.y + y_px), egui::vec2(side_px, side_px), ), egui::CornerRadius::ZERO, egui::Color32::DARK_GRAY, ); if show_text.unwrap_or(false) { let font_pt = 8.0 * transform.uniform_scale; let text_y_px = rect.min.y + (transform.y_mm(*y) + side_mm + 1.5 * transform.uniform_scale) * preview_scale + font_pt * POINTS_TO_MM * (1.0 - TEXT_DESCENT_RATIO) * preview_scale; painter.text( egui::pos2(rect.min.x + x_px, text_y_px), egui::Align2::LEFT_BOTTOM, &value, egui::FontId::proportional(font_pt), egui::Color32::BLACK, ); } } LabelElement::Barcode { field, x, y, width, height, show_text, .. } => { let value = Self::resolve_field(field, data); let w_mm = transform.width_mm(*width).max(0.1); let h_mm = transform.height_mm(*height).max(0.1); let x_px = transform.x_mm(*x) * preview_scale; let y_px = transform.y_mm(*y) * preview_scale; let w_px = w_mm * preview_scale; let h_px = h_mm * preview_scale; painter.rect_filled( egui::Rect::from_min_size( egui::pos2(rect.min.x + x_px, rect.min.y + y_px), egui::vec2(w_px, h_px), ), egui::CornerRadius::ZERO, egui::Color32::BLACK, ); if show_text.unwrap_or(false) { let font_pt = 8.0 * transform.uniform_scale; let text_y_px = rect.min.y + (transform.y_mm(*y) + h_mm + 1.5 * transform.uniform_scale) * preview_scale + font_pt * POINTS_TO_MM * (1.0 - TEXT_DESCENT_RATIO) * preview_scale; painter.text( egui::pos2(rect.min.x + x_px, text_y_px), egui::Align2::LEFT_BOTTOM, &value, egui::FontId::proportional(font_pt), egui::Color32::BLACK, ); } } LabelElement::Rect { x, y, width, height, fill, } => { let w_mm = transform.width_mm(*width).max(0.1); let h_mm = transform.height_mm(*height).max(0.1); let x_px = transform.x_mm(*x) * preview_scale; let y_px = transform.y_mm(*y) * preview_scale; let w_px = w_mm * preview_scale; let h_px = h_mm * preview_scale; let color32 = fill .as_deref() .and_then(Self::parse_hex_color) .unwrap_or(egui::Color32::from_gray(180)); painter.rect_filled( egui::Rect::from_min_size( egui::pos2(rect.min.x + x_px, rect.min.y + y_px), egui::vec2(w_px, h_px), ), egui::CornerRadius::ZERO, color32, ); } LabelElement::Svg { x, y, width, height, .. } => { let w_mm = transform.width_mm(*width).max(0.1); let h_mm = transform.height_mm(*height).max(0.1); let x_px = transform.x_mm(*x) * preview_scale; let y_px = transform.y_mm(*y) * preview_scale; let w_px = w_mm * preview_scale; let h_px = h_mm * preview_scale; painter.rect_filled( egui::Rect::from_min_size( egui::pos2(rect.min.x + x_px, rect.min.y + y_px), egui::vec2(w_px, h_px), ), egui::CornerRadius::ZERO, egui::Color32::from_gray(200), ); } } } Ok(()) } fn prepare_code128_payload(data: &str) -> Option { // Strip BOM and surrounding whitespace let mut s = data.trim().trim_start_matches('\u{FEFF}').to_string(); if s.is_empty() { return None; } // Allow user-provided advanced Code128 sequence (already has start set char) if let Some(first) = s.chars().next() { if matches!(first, 'À' | 'Ɓ' | 'Ć') { // Minimal length check (library requires at least 2 chars total) if s.len() >= 2 { return Some(s); } else { return None; } } } // Remove internal whitespace s.retain(|c| !c.is_whitespace()); if s.is_empty() { return None; } // Pure digits: use Code Set C (double-density). Must be even length; pad leading 0 if needed. if s.chars().all(|c| c.is_ascii_digit()) { if s.len() % 2 == 1 { s.insert(0, '0'); } // Prefix with Set C start char 'Ć' return Some(format!("Ć{}", s)); } // General printable ASCII: choose Set B start ('Ɓ'). Filter to printable 32..=126. let mut cleaned = String::new(); for ch in s.chars() { let code = ch as u32; if (32..=126).contains(&code) { cleaned.push(ch); } } if cleaned.is_empty() { return None; } Some(format!("Ɓ{}", cleaned)) } // Naive word-wrap: estimate character width ~= 0.55 * font_size_pt * POINTS_TO_MM fn wrap_lines(text: &str, max_width_mm: f32, font_size_pt: f32) -> Vec { let approx_char_mm = font_size_pt * POINTS_TO_MM * 0.55; if approx_char_mm <= 0.0 || max_width_mm <= 0.0 { return vec![text.to_string()]; } let max_chars = (max_width_mm / approx_char_mm).floor().max(1.0) as usize; let mut lines = Vec::new(); let mut current = String::new(); for word in text.split_whitespace() { if current.is_empty() { current.push_str(word); continue; } if current.len() + 1 + word.len() <= max_chars { current.push(' '); current.push_str(word); } else { lines.push(std::mem::take(&mut current)); current.push_str(word); } } if !current.is_empty() { lines.push(current); } if lines.is_empty() { lines.push(String::new()); } lines } // Decode data URI to raw SVG XML string fn decode_svg_data_uri(data_uri: &str) -> Option { if let Some(idx) = data_uri.find(',') { let (header, payload) = data_uri.split_at(idx + 1); if header.contains("base64") { let bytes = base64::engine::general_purpose::STANDARD .decode(payload) .ok()?; String::from_utf8(bytes).ok() } else { Some(payload.to_string()) } } else { if data_uri.contains(" Option> { use tiny_skia::Pixmap; use usvg::Options; let opt = Options::default(); let tree = usvg::Tree::from_str(svg_xml, &opt).ok()?; let mut pixmap = Pixmap::new(target_w, target_h)?; // Compute uniform scale to fit preserving aspect let view_size = tree.size(); let sx = target_w as f32 / view_size.width(); let sy = target_h as f32 / view_size.height(); let scale = sx.min(sy); let transform = tiny_skia::Transform::from_scale(scale, scale); // Render using resvg resvg::render(&tree, transform, &mut pixmap.as_mut()); Some(pixmap.data().to_vec()) } } #[cfg(test)] mod tests { use super::*; #[test] fn code128_numeric_even_len_encodes() { let raw = "75650012"; // even length digits let payload = LabelRenderer::prepare_code128_payload(raw).expect("payload"); assert!(payload.starts_with('Ć')); let code = barcoders::sym::code128::Code128::new(&payload).expect("encode code128"); assert!(!code.encode().is_empty()); } #[test] fn code128_numeric_odd_len_padded() { let raw = "123"; let payload = LabelRenderer::prepare_code128_payload(raw).expect("payload"); assert!(payload.starts_with('Ć')); // Collect digits after the first unicode character (start set) let digits: String = payload.chars().skip(1).collect(); assert_eq!(digits.len() % 2, 0); let code = barcoders::sym::code128::Code128::new(&payload).expect("encode code128"); assert!(!code.encode().is_empty()); } #[test] fn code128_basic_ascii_encodes() { let payload = LabelRenderer::prepare_code128_payload("HELLO-123").expect("payload"); assert!(payload.starts_with('Ɓ')); let code = barcoders::sym::code128::Code128::new(&payload).expect("encode code128"); assert!(!code.encode().is_empty()); } #[test] fn code11_accepts_digits_and_dash() { use barcoders::sym::code11::Code11; // Valid payload containing digits and dash let c = Code11::new("123-45").expect("encode code11"); assert!(!c.encode().is_empty()); // Library should reject invalid characters; ensure it errors assert!(Code11::new("12A45").is_err()); } #[test] fn datamatrix_encodes_nonempty_bitmap() { use datamatrix::{DataMatrix, SymbolList}; let dm = DataMatrix::encode_str("DM-OK-123", SymbolList::default()).expect("encode datamatrix"); let bmp = dm.bitmap(); assert!(bmp.width() > 0 && bmp.height() > 0); assert!(bmp.pixels().next().is_some()); } #[test] fn layout_deserialize_show_text_flags_raw_and_base64() { // Minimal layout exercising showText flags across elements let raw_json = r##"{ "background": "#FFFFFF", "elements": [ {"type": "qrcode", "field": "A", "x": 5, "y": 5, "size": 20, "showText": true}, {"type": "datamatrix", "field": "B", "x": 30, "y": 5, "size": 20, "showText": false}, {"type": "barcode", "field": "C", "x": 5, "y": 30, "width": 40, "height": 12, "format": "code128", "showText": true} ] }"##; // Parse raw let r1 = LabelRenderer::from_json(raw_json).expect("raw parse"); assert_eq!(r1.layout.elements.len(), 3); // Parse base64 of same JSON let b64 = base64::engine::general_purpose::STANDARD.encode(raw_json); let r2 = LabelRenderer::from_json(&b64).expect("b64 parse"); assert_eq!(r2.layout.elements.len(), 3); // Spot-check variant fields carry show_text flags via serde mapping match &r1.layout.elements[0] { LabelElement::QrCode { show_text, .. } => assert_eq!(show_text.unwrap_or(false), true), _ => panic!("expected qrcode"), } match &r1.layout.elements[1] { LabelElement::DataMatrix { show_text, .. } => { assert_eq!(show_text.unwrap_or(true), false) } _ => panic!("expected datamatrix"), } match &r1.layout.elements[2] { LabelElement::Barcode { show_text, .. } => assert_eq!(show_text.unwrap_or(false), true), _ => panic!("expected barcode"), } } }