diff --git a/src/barcodes/code128.rs b/src/barcodes/code128.rs index 5d7e64a..4f68bba 100644 --- a/src/barcodes/code128.rs +++ b/src/barcodes/code128.rs @@ -20,6 +20,22 @@ pub fn prepare_ucc_mode_data(content: &str) -> String { format!("{}{}{}", ESCAPE_FNC_1, padded, check) } +/// Prepare field data and human-readable text for ^BC mode D (UCC/EAN mode). +/// +/// Parentheses and spaces are retained in the interpretation line but omitted +/// from the encoded symbol. The ZPL `>8` invocation inserts FNC1 into the +/// symbol and is not itself printable field data. +pub fn prepare_ean_mode_data(content: &str) -> (String, String) { + let fnc1 = ESCAPE_FNC_1.to_string(); + let encoded: String = content + .replace(">8", &fnc1) + .chars() + .filter(|c| *c != '(' && *c != ')' && *c != ' ') + .collect(); + let display = content.replace(">8", ""); + (format!("{}{}", ESCAPE_FNC_1, encoded), display) +} + /// GS1 Mod-10 check digit calculation. /// Rightmost digit gets weight 3, alternating 3/1 from right to left. fn gs1_mod10_check(digits: &str) -> char { diff --git a/src/drawers/renderer.rs b/src/drawers/renderer.rs index 004ec22..8778f7c 100644 --- a/src/drawers/renderer.rs +++ b/src/drawers/renderer.rs @@ -520,31 +520,29 @@ impl Renderer { } _ => { // Modes U and D (UCC/EAN) automatically insert FNC1 at start per ZPL spec - let content_to_encode = match bc.barcode.mode { + let (content_to_encode, display_text) = match bc.barcode.mode { BarcodeMode::Ucc => { // Mode U: truncate to 19 digits, append GS1 Mod-10 check digit, prepend FNC1 - barcodes::code128::prepare_ucc_mode_data(content) + ( + barcodes::code128::prepare_ucc_mode_data(content), + content.clone(), + ) } BarcodeMode::Ean => { // Mode D: FNC1 prepended automatically; >8 in data = embedded FNC1 separator // for chaining GS1 application identifiers; parentheses and spaces stripped - // from encoding but preserved in display text per ZPL spec. - let fnc1 = barcodes::code128::ESCAPE_FNC_1.to_string(); - let for_encoding: String = content - .replace(">8", &fnc1) - .chars() - .filter(|c| *c != '(' && *c != ')' && *c != ' ') - .collect(); - format!("{}{}", barcodes::code128::ESCAPE_FNC_1, for_encoding) + // from encoding but preserved in display text per ZPL spec. Invocation + // codes affect the symbol only and must not be printed as glyphs. + barcodes::code128::prepare_ean_mode_data(content) } - _ => content.clone(), + _ => (content.clone(), content.clone()), }; let img = barcodes::code128::encode_auto( &content_to_encode, bc.barcode.height, bc.width, )?; - (img, content.clone()) + (img, display_text) } }; @@ -560,6 +558,7 @@ impl Renderer { bc.barcode.orientation, bc.barcode.line_above, bc.width, + bc.barcode.mode == BarcodeMode::Ean, ); } Ok(()) @@ -583,6 +582,7 @@ impl Renderer { bc.barcode.orientation, bc.barcode.line_above, bc.width, + false, ); } Ok(()) @@ -613,6 +613,7 @@ impl Renderer { bc.barcode.orientation, bc.barcode.line_above, bc.width, + false, ); } Ok(()) @@ -638,6 +639,7 @@ impl Renderer { bc.barcode.orientation, bc.barcode.line_above, bc.width, + false, ); } Ok(()) @@ -1213,6 +1215,7 @@ fn draw_filled_rounded_rect_region( } /// Draw the human-readable interpretation line below (or above) a barcode. +#[allow(clippy::too_many_arguments)] fn draw_barcode_interpretation_line( canvas: &mut RgbaImage, text: &str, @@ -1221,19 +1224,35 @@ fn draw_barcode_interpretation_line( orientation: FieldOrientation, line_above: bool, module_width: i32, + ucc_ean_font: bool, ) { - let font_data = FONT_DEJAVU_MONO; + // Code 128 mode D (UCC/EAN) uses a larger condensed bold interpretation font, + // matching Labelary (~14×module ink height, ~7×module per-char advance). + // Other modes use the standard monospace font that scales with module width. + let (font_data, font_size) = if ucc_ean_font { + ( + FONT_HELVETICA, + (module_width.max(1) as f32 * 14.3).clamp(14.0, 96.0), + ) + } else { + ( + FONT_DEJAVU_MONO, + (module_width.max(1) as f32 * 11.0).clamp(12.0, 72.0), + ) + }; let font = match ab_glyph::FontRef::try_from_slice(font_data) { Ok(f) => f, Err(_) => return, }; // Zebra's interpretation line font scales with the barcode module width. // At module_width=2 (default), the standard font is ~23px to match reference width. - let font_size = (module_width.max(1) as f32 * 11.0).clamp(12.0, 72.0); let scale = PxScale { x: font_size, y: font_size, }; + // Helvetica's ink starts at the buffer top, unlike DejaVu which has ~3px + // top padding — push the UCC/EAN text down to match Labelary's line position. + let text_y_off: i32 = if ucc_ean_font { 4 } else { 0 }; // Strip control characters (like FNC1 escape) from display text let display: String = text @@ -1261,7 +1280,7 @@ fn draw_barcode_interpretation_line( &mut big, Rgba([0, 0, 0, 255]), 0, - 0, + text_y_off * SS as i32, ss_scale, &font, &display, @@ -1293,19 +1312,19 @@ fn draw_barcode_interpretation_line( FieldOrientation::Normal => { let cx = pos.x + (bw - text_width as i32) / 2; let ty = if line_above { - pos.y - font_size as i32 - 2 + pos.y - font_size as i32 - 2 - text_y_off } else { pos.y + bh + 2 }; let buf_w = (text_width.ceil() as u32).max(1) + 2; - let buf_h = font_size.ceil() as u32 + 2; + let buf_h = font_size.ceil() as u32 + 2 + text_y_off as u32; let buf = render_text_crisp(buf_w, buf_h); overlay_at(canvas, &buf, cx, ty); } _ => { // Render text to buffer, rotate to match barcode orientation, then overlay let buf_w = (text_width.ceil() as u32).max(1) + 2; - let buf_h = font_size.ceil() as u32 + 2; + let buf_h = font_size.ceil() as u32 + 2 + text_y_off as u32; let buf = render_text_crisp(buf_w, buf_h); let rotated = match orientation { diff --git a/testdata/diffs/code128_mode_d_fnc1.png b/testdata/diffs/code128_mode_d_fnc1.png new file mode 100644 index 0000000..80c4831 Binary files /dev/null and b/testdata/diffs/code128_mode_d_fnc1.png differ diff --git a/testdata/diffs/code128_mode_d_fnc1_diff.png b/testdata/diffs/code128_mode_d_fnc1_diff.png new file mode 100644 index 0000000..add4e55 Binary files /dev/null and b/testdata/diffs/code128_mode_d_fnc1_diff.png differ diff --git a/testdata/diffs/diff_report_unit.txt b/testdata/diffs/diff_report_unit.txt index e353414..ae54115 100644 --- a/testdata/diffs/diff_report_unit.txt +++ b/testdata/diffs/diff_report_unit.txt @@ -19,6 +19,7 @@ ║ barcode128_rotated │ zpl │ 0.24% │ 813x1626 │ 813x1626 │ GOOD(<1%) ║ ║ cf_font_designator │ zpl │ 0.38% │ 813x1626 │ 812x1624 │ GOOD(<1%) ║ ║ cf_font_no_orientation │ zpl │ 0.43% │ 813x1626 │ 812x1624 │ GOOD(<1%) ║ +║ code128_mode_d_fnc1 │ zpl │ 0.48% │ 813x1626 │ 812x1624 │ GOOD(<1%) ║ ║ cp850_hex_chars │ zpl │ 0.56% │ 813x1626 │ 812x1624 │ GOOD(<1%) ║ ║ dhlparceluk_dhl_text │ zpl │ 0.45% │ 813x1626 │ 813x1626 │ GOOD(<1%) ║ ║ dhlparceluk_ver │ zpl │ 0.05% │ 813x1626 │ 813x1626 │ GOOD(<1%) ║ @@ -66,5 +67,5 @@ ║ usps_priority_mail │ zpl │ 0.54% │ 813x1626 │ 813x1626 │ GOOD(<1%) ║ ║ usps_test_merchant │ zpl │ 0.16% │ 813x1626 │ 813x1626 │ GOOD(<1%) ║ ╠══════════════════════════════════════════════════════════════════════════════╣ -║ Summary: 62 total │ 8 perfect │ 44 good │ 7 minor │ 3 moderate │ 0 high │ 0 skip │ 0 err +║ Summary: 63 total │ 8 perfect │ 45 good │ 7 minor │ 3 moderate │ 0 high │ 0 skip │ 0 err ╚══════════════════════════════════════════════════════════════════════════════╝ diff --git a/testdata/unit/code128_mode_d_fnc1.png b/testdata/unit/code128_mode_d_fnc1.png new file mode 100644 index 0000000..64e5370 Binary files /dev/null and b/testdata/unit/code128_mode_d_fnc1.png differ diff --git a/testdata/unit/code128_mode_d_fnc1.zpl b/testdata/unit/code128_mode_d_fnc1.zpl new file mode 100644 index 0000000..7207c83 --- /dev/null +++ b/testdata/unit/code128_mode_d_fnc1.zpl @@ -0,0 +1,7 @@ +^XA +^PW812^LL400 +^BY2,2.5,145 +^FO30,30 +^BCN,,Y,N,N,D +^FD(91)0005886>8(10)0000410549>8(99)05^FS +^XZ diff --git a/tests/e2e_golden.rs b/tests/e2e_golden.rs index 03a23f1..dfd6146 100644 --- a/tests/e2e_golden.rs +++ b/tests/e2e_golden.rs @@ -203,6 +203,10 @@ fn golden_barcode128_mode_d() { golden_zpl_with_tolerance("barcode128_mode_d", 2.0); } #[test] +fn golden_code128_mode_d_fnc1() { + golden_zpl_with_tolerance("code128_mode_d_fnc1", 1.0); +} +#[test] fn golden_barcode128_mode_n() { golden_zpl_with_tolerance("barcode128_mode_n", 2.0); } diff --git a/tests/unit_barcodes.rs b/tests/unit_barcodes.rs index 9adb4c6..a342d9d 100644 --- a/tests/unit_barcodes.rs +++ b/tests/unit_barcodes.rs @@ -98,6 +98,19 @@ fn code39_empty_input_handled() { let _result = code39::encode("", 100, 3, 2); } +#[test] +fn code128_ean_mode_keeps_ai_formatting_but_hides_fnc1_invocations() { + let (encoded, display) = code128::prepare_ean_mode_data("(91)0005886>8(10)0000410549>8(99)05"); + let fnc1 = code128::ESCAPE_FNC_1; + + assert_eq!( + encoded, + format!("{fnc1}910005886{fnc1}100000410549{fnc1}9905") + ); + assert_eq!(display, "(91)0005886(10)0000410549(99)05"); + assert!(!display.contains(">8")); +} + // --- EAN-13 --- #[test]