Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/barcodes/code128.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
55 changes: 37 additions & 18 deletions src/drawers/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
};

Expand All @@ -560,6 +558,7 @@ impl Renderer {
bc.barcode.orientation,
bc.barcode.line_above,
bc.width,
bc.barcode.mode == BarcodeMode::Ean,
);
}
Ok(())
Expand All @@ -583,6 +582,7 @@ impl Renderer {
bc.barcode.orientation,
bc.barcode.line_above,
bc.width,
false,
);
}
Ok(())
Expand Down Expand Up @@ -613,6 +613,7 @@ impl Renderer {
bc.barcode.orientation,
bc.barcode.line_above,
bc.width,
false,
);
}
Ok(())
Expand All @@ -638,6 +639,7 @@ impl Renderer {
bc.barcode.orientation,
bc.barcode.line_above,
bc.width,
false,
);
}
Ok(())
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
Binary file added testdata/diffs/code128_mode_d_fnc1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/diffs/code128_mode_d_fnc1_diff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion testdata/diffs/diff_report_unit.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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%) ║
Expand Down Expand Up @@ -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
╚══════════════════════════════════════════════════════════════════════════════╝
Binary file added testdata/unit/code128_mode_d_fnc1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions testdata/unit/code128_mode_d_fnc1.zpl
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions tests/e2e_golden.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
13 changes: 13 additions & 0 deletions tests/unit_barcodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading