Skip to main content

labelize/parsers/
epl_parser.rs

1use crate::elements::barcode_128::{Barcode128, Barcode128WithData, BarcodeMode};
2use crate::elements::barcode_2of5::{Barcode2of5, Barcode2of5WithData};
3use crate::elements::barcode_39::{Barcode39, Barcode39WithData};
4use crate::elements::barcode_ean13::{BarcodeEan13, BarcodeEan13WithData};
5use crate::elements::field_orientation::FieldOrientation;
6use crate::elements::font::FontInfo;
7use crate::elements::graphic_box::GraphicBox;
8use crate::elements::label_element::LabelElement;
9use crate::elements::label_info::LabelInfo;
10use crate::elements::label_position::LabelPosition;
11use crate::elements::line_color::LineColor;
12use crate::elements::reverse_print::ReversePrint;
13use crate::elements::text_field::TextField;
14
15pub struct EplParser;
16
17impl Default for EplParser {
18    fn default() -> Self {
19        Self
20    }
21}
22
23impl EplParser {
24    pub fn new() -> Self {
25        EplParser
26    }
27
28    pub fn parse(&self, epl_data: &[u8]) -> Result<Vec<LabelInfo>, String> {
29        let data_str = String::from_utf8_lossy(epl_data);
30        let lines: Vec<&str> = data_str.split('\n').collect();
31
32        let mut results = Vec::new();
33        let mut current_elements: Vec<LabelElement> = Vec::new();
34        let mut ref_x = 0i32;
35        let mut ref_y = 0i32;
36
37        for raw_line in &lines {
38            let line = raw_line.trim_end_matches('\r').trim();
39            if line.is_empty() {
40                continue;
41            }
42
43            if line == "N" {
44                current_elements.clear();
45                ref_x = 0;
46                ref_y = 0;
47                continue;
48            }
49
50            if is_epl_reference_point(line) {
51                let parts: Vec<&str> = line[1..].splitn(2, ',').collect();
52                if let Some(s) = parts.first() {
53                    ref_x = s.trim().parse().unwrap_or(0);
54                }
55                if let Some(s) = parts.get(1) {
56                    ref_y = s.trim().parse().unwrap_or(0);
57                }
58                continue;
59            }
60
61            if line.starts_with('A') {
62                if let Some(el) = parse_epl_text(line, ref_x, ref_y)? {
63                    current_elements.push(el);
64                }
65                continue;
66            }
67
68            if line.starts_with('B') {
69                if let Some(el) = parse_epl_barcode(line, ref_x, ref_y)? {
70                    current_elements.push(el);
71                }
72                continue;
73            }
74
75            if line.starts_with("LO") {
76                if let Some(el) = parse_epl_line(line, ref_x, ref_y)? {
77                    current_elements.push(el);
78                }
79                continue;
80            }
81
82            if is_epl_print_command(line) {
83                if !current_elements.is_empty() {
84                    results.push(LabelInfo {
85                        print_width: 0,
86                        inverted: false,
87                        elements: current_elements.clone(),
88                    });
89                }
90                current_elements.clear();
91            }
92        }
93
94        // Handle labels without trailing P
95        if !current_elements.is_empty() {
96            results.push(LabelInfo {
97                print_width: 0,
98                inverted: false,
99                elements: current_elements,
100            });
101        }
102
103        Ok(results)
104    }
105}
106
107fn is_epl_reference_point(line: &str) -> bool {
108    let bytes = line.as_bytes();
109    bytes.len() > 1 && bytes[0] == b'R' && bytes[1].is_ascii_digit()
110}
111
112fn is_epl_print_command(line: &str) -> bool {
113    let bytes = line.as_bytes();
114    if bytes.is_empty() || bytes[0] != b'P' {
115        return false;
116    }
117    if bytes.len() == 1 {
118        return true;
119    }
120    bytes[1..].iter().all(|b| b.is_ascii_digit())
121}
122
123fn epl_rotation(rotation: i32) -> FieldOrientation {
124    match rotation {
125        1 => FieldOrientation::Rotated90,
126        2 => FieldOrientation::Rotated180,
127        3 => FieldOrientation::Rotated270,
128        _ => FieldOrientation::Normal,
129    }
130}
131
132static EPL_FONT_SIZES: &[(i32, i32, i32)] = &[
133    // (font_num, width, height)
134    (1, 8, 12),
135    (2, 10, 16),
136    (3, 12, 20),
137    (4, 14, 24),
138    (5, 32, 48),
139];
140
141fn epl_font_size(font_num: i32) -> (i32, i32) {
142    for &(n, w, h) in EPL_FONT_SIZES {
143        if n == font_num {
144            return (w, h);
145        }
146    }
147    (8, 12) // default to font 1
148}
149
150fn parse_epl_text(line: &str, ref_x: i32, ref_y: i32) -> Result<Option<LabelElement>, String> {
151    let data_start = line.find('"');
152    let data_end = line.rfind('"');
153    match (data_start, data_end) {
154        (Some(s), Some(e)) if e > s => {
155            let text = &line[s + 1..e];
156            if text.is_empty() {
157                return Ok(None);
158            }
159
160            let param_str = line[1..s].trim_end_matches(',');
161            let parts: Vec<&str> = param_str.split(',').collect();
162
163            if parts.len() < 7 {
164                return Err(format!(
165                    "EPL A command requires at least 7 parameters, got {}",
166                    parts.len()
167                ));
168            }
169
170            let x: i32 = parts[0].trim().parse().unwrap_or(0);
171            let y: i32 = parts[1].trim().parse().unwrap_or(0);
172            let rotation: i32 = parts[2].trim().parse().unwrap_or(0);
173            let font_num: i32 = parts[3].trim().parse().unwrap_or(1);
174            let h_mult: i32 = parts[4].trim().parse::<i32>().unwrap_or(1).max(1);
175            let v_mult: i32 = parts[5].trim().parse::<i32>().unwrap_or(1).max(1);
176            let reverse = parts[6].trim();
177
178            let (base_w, base_h) = epl_font_size(font_num);
179
180            let font_height = (base_h * v_mult) as f64;
181            let font_width = if h_mult != v_mult {
182                font_height * (h_mult * base_w) as f64 / (v_mult * base_h) as f64
183            } else {
184                font_height
185            };
186
187            Ok(Some(LabelElement::Text(TextField {
188                reverse_print: ReversePrint {
189                    value: reverse == "R",
190                },
191                font: FontInfo {
192                    name: "0".to_string(),
193                    width: font_width,
194                    height: font_height,
195                    orientation: epl_rotation(rotation),
196                },
197                position: LabelPosition {
198                    x: x + ref_x,
199                    y: y + ref_y,
200                    ..Default::default()
201                },
202                text: text.to_string(),
203                alignment: Default::default(),
204                block: None,
205            })))
206        }
207        _ => Ok(None),
208    }
209}
210
211fn parse_epl_barcode(line: &str, ref_x: i32, ref_y: i32) -> Result<Option<LabelElement>, String> {
212    let data_start = line.find('"');
213    let data_end = line.rfind('"');
214    match (data_start, data_end) {
215        (Some(s), Some(e)) if e > s => {
216            let data = &line[s + 1..e];
217            if data.is_empty() {
218                return Ok(None);
219            }
220
221            let param_str = line[1..s].trim_end_matches(',');
222            let parts: Vec<&str> = param_str.split(',').collect();
223
224            if parts.len() < 8 {
225                return Err(format!(
226                    "EPL B command requires at least 8 parameters, got {}",
227                    parts.len()
228                ));
229            }
230
231            let x: i32 = parts[0].trim().parse().unwrap_or(0);
232            let y: i32 = parts[1].trim().parse().unwrap_or(0);
233            let rotation: i32 = parts[2].trim().parse().unwrap_or(0);
234            let bc_type = parts[3].trim();
235            let narrow_bar: i32 = parts[4].trim().parse::<i32>().unwrap_or(1).max(1);
236            let wide_bar: i32 = parts[5].trim().parse().unwrap_or(2);
237            let height: i32 = parts[6].trim().parse::<i32>().unwrap_or(10).max(1);
238            let human_readable = parts[7].trim();
239
240            let pos = LabelPosition {
241                x: x + ref_x,
242                y: y + ref_y,
243                ..Default::default()
244            };
245            let orient = epl_rotation(rotation);
246            let show_line = human_readable == "B";
247            let width_ratio = (wide_bar as f64 / narrow_bar as f64).max(2.0);
248
249            let el = match bc_type {
250                "0" => LabelElement::Barcode39(Barcode39WithData {
251                    reverse_print: ReversePrint::default(),
252                    barcode: Barcode39 {
253                        orientation: orient,
254                        height,
255                        line: show_line,
256                        line_above: false,
257                        check_digit: false,
258                    },
259                    width: narrow_bar,
260                    width_ratio,
261                    position: pos,
262                    data: data.to_string(),
263                }),
264                "B" => LabelElement::BarcodeEan13(BarcodeEan13WithData {
265                    reverse_print: ReversePrint::default(),
266                    barcode: BarcodeEan13 {
267                        orientation: orient,
268                        height,
269                        line: show_line,
270                        line_above: false,
271                    },
272                    width: narrow_bar,
273                    position: pos,
274                    data: data.to_string(),
275                }),
276                "G" | "H" => LabelElement::Barcode2of5(Barcode2of5WithData {
277                    reverse_print: ReversePrint::default(),
278                    barcode: Barcode2of5 {
279                        orientation: orient,
280                        height,
281                        line: show_line,
282                        line_above: false,
283                        check_digit: false,
284                    },
285                    width: narrow_bar,
286                    width_ratio,
287                    position: pos,
288                    data: data.to_string(),
289                }),
290                _ => {
291                    // Default to Code 128 Auto
292                    LabelElement::Barcode128(Barcode128WithData {
293                        reverse_print: ReversePrint::default(),
294                        barcode: Barcode128 {
295                            orientation: orient,
296                            height,
297                            line: show_line,
298                            line_above: false,
299                            check_digit: false,
300                            mode: BarcodeMode::Automatic,
301                        },
302                        width: narrow_bar,
303                        position: pos,
304                        data: data.to_string(),
305                    })
306                }
307            };
308
309            Ok(Some(el))
310        }
311        _ => Ok(None),
312    }
313}
314
315fn parse_epl_line(line: &str, ref_x: i32, ref_y: i32) -> Result<Option<LabelElement>, String> {
316    let param_str = &line[2..]; // Skip "LO"
317    let parts: Vec<&str> = param_str.split(',').collect();
318
319    if parts.len() < 4 {
320        return Err(format!(
321            "EPL LO command requires 4 parameters, got {}",
322            parts.len()
323        ));
324    }
325
326    let x: i32 = parts[0].trim().parse().unwrap_or(0);
327    let y: i32 = parts[1].trim().parse().unwrap_or(0);
328    let width: i32 = parts[2].trim().parse::<i32>().unwrap_or(1).max(1);
329    let height: i32 = parts[3].trim().parse::<i32>().unwrap_or(1).max(1);
330
331    Ok(Some(LabelElement::GraphicBox(GraphicBox {
332        position: LabelPosition {
333            x: x + ref_x,
334            y: y + ref_y,
335            ..Default::default()
336        },
337        width,
338        height,
339        border_thickness: width.min(height),
340        corner_rounding: 0,
341        line_color: LineColor::Black,
342        reverse_print: ReversePrint::default(),
343    })))
344}