Skip to main content

zpl_forge/engine/
engine.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use crate::{
5    FontManager, ZplError, ZplResult,
6    ast::parse_zpl,
7    engine::{backend, common, font, intr},
8};
9
10/// The main entry point for processing and rendering ZPL labels.
11///
12/// `ZplEngine` holds the parsed instructions, label dimensions, and configuration
13/// required to render a label using a specific backend.
14#[derive(Debug)]
15pub struct ZplEngine {
16    instructions: Vec<common::ZplInstruction>,
17    width: common::Unit,
18    height: common::Unit,
19    resolution: common::Resolution,
20    fonts: Option<Arc<font::FontManager>>,
21}
22
23impl ZplEngine {
24    /// Creates a new `ZplEngine` instance by parsing a ZPL string.
25    ///
26    /// # Arguments
27    /// * `zpl` - The raw ZPL string to parse.
28    /// * `width` - The physical width of the label.
29    /// * `height` - The physical height of the label.
30    /// * `resolution` - The printing resolution (DPI).
31    ///
32    /// # Errors
33    /// Returns an error if the ZPL is invalid or if the instruction building fails.
34    pub fn new(
35        zpl: &str,
36        width: common::Unit,
37        height: common::Unit,
38        resolution: common::Resolution,
39    ) -> ZplResult<Self> {
40        let commands = parse_zpl(zpl)?;
41        if commands.is_empty() {
42            return Err(ZplError::EmptyInput);
43        }
44
45        let instructions = intr::ZplInstructionBuilder::new(commands);
46        let instructions = instructions.build()?;
47
48        Ok(Self {
49            instructions,
50            width,
51            height,
52            resolution,
53            fonts: None,
54        })
55    }
56
57    /// Sets the font manager to be used during rendering.
58    ///
59    /// If no font manager is provided, a default one will be used.
60    pub fn set_fonts(&mut self, fonts: Arc<font::FontManager>) {
61        self.fonts = Some(fonts);
62    }
63
64    /// Renders the parsed instructions using the provided backend.
65    ///
66    /// # Arguments
67    /// * `backend` - An implementation of `ZplForgeBackend` (e.g., PNG, PDF).
68    /// * `variables` - A map of template variables to replace in text fields (format: `{{key}}`).
69    ///
70    /// # Errors
71    /// Returns an error if rendering fails at the backend level.
72    pub fn render<B: backend::ZplForgeBackend>(
73        &self,
74        mut backend: B,
75        variables: &HashMap<String, String>,
76    ) -> ZplResult<Vec<u8>> {
77        fn replace_vars<'a>(
78            s: &'a str,
79            variables: &HashMap<String, String>,
80        ) -> std::borrow::Cow<'a, str> {
81            if variables.is_empty() || !s.contains("{{") {
82                return std::borrow::Cow::Borrowed(s);
83            }
84
85            let mut result = String::new();
86            let mut last_pos = 0;
87            let mut found = false;
88            let mut cursor = 0;
89
90            while let Some(start_offset) = s[cursor..].find("{{") {
91                let start = cursor + start_offset;
92                if let Some(end_offset) = s[start + 2..].find("}}") {
93                    let end = start + 2 + end_offset;
94                    let key = &s[start + 2..end];
95                    if let Some(value) = variables.get(key) {
96                        if !found {
97                            result.reserve(s.len());
98                            found = true;
99                        }
100                        result.push_str(&s[last_pos..start]);
101                        result.push_str(value);
102                        last_pos = end + 2;
103                        cursor = last_pos;
104                        continue;
105                    }
106                }
107                cursor = start + 2;
108            }
109
110            if found {
111                result.push_str(&s[last_pos..]);
112                std::borrow::Cow::Owned(result)
113            } else {
114                std::borrow::Cow::Borrowed(s)
115            }
116        }
117
118        let w_dots = self.width.clone().to_dots(self.resolution);
119        let h_dots = self.height.clone().to_dots(self.resolution);
120        let font_manager = if let Some(fonts) = &self.fonts {
121            fonts.clone()
122        } else {
123            Arc::new(FontManager::default())
124        };
125
126        backend.setup_page(w_dots as f64, h_dots as f64, self.resolution.dpi());
127        backend.setup_font_manager(&font_manager);
128
129        for instruction in &self.instructions {
130            let condition = match instruction {
131                common::ZplInstruction::Text { condition, .. } => condition,
132                common::ZplInstruction::GraphicBox { condition, .. } => condition,
133                common::ZplInstruction::GraphicCircle { condition, .. } => condition,
134                common::ZplInstruction::GraphicEllipse { condition, .. } => condition,
135                common::ZplInstruction::GraphicField { condition, .. } => condition,
136                common::ZplInstruction::CustomImage { condition, .. } => condition,
137                common::ZplInstruction::Code128 { condition, .. } => condition,
138                common::ZplInstruction::QRCode { condition, .. } => condition,
139                common::ZplInstruction::Code39 { condition, .. } => condition,
140            };
141
142            if let Some((var, expected)) = condition
143                && variables.get(var) != Some(expected)
144            {
145                continue;
146            }
147
148            match instruction {
149                common::ZplInstruction::Text {
150                    condition: _,
151                    x,
152                    y,
153                    font,
154                    height,
155                    width,
156                    text,
157                    reverse_print,
158                    color,
159                } => {
160                    backend.draw_text(
161                        *x,
162                        *y,
163                        *font,
164                        *height,
165                        *width,
166                        &replace_vars(text, variables),
167                        *reverse_print,
168                        color.clone(),
169                    )?;
170                }
171                common::ZplInstruction::GraphicBox {
172                    condition: _,
173                    x,
174                    y,
175                    width,
176                    height,
177                    thickness,
178                    color,
179                    custom_color,
180                    rounding,
181                    reverse_print,
182                } => {
183                    backend.draw_graphic_box(
184                        *x,
185                        *y,
186                        *width,
187                        *height,
188                        *thickness,
189                        *color,
190                        custom_color.clone(),
191                        *rounding,
192                        *reverse_print,
193                    )?;
194                }
195                common::ZplInstruction::GraphicCircle {
196                    condition: _,
197                    x,
198                    y,
199                    radius,
200                    thickness,
201                    color,
202                    custom_color,
203                    reverse_print,
204                } => {
205                    backend.draw_graphic_circle(
206                        *x,
207                        *y,
208                        *radius,
209                        *thickness,
210                        *color,
211                        custom_color.clone(),
212                        *reverse_print,
213                    )?;
214                }
215                common::ZplInstruction::GraphicEllipse {
216                    condition: _,
217                    x,
218                    y,
219                    width,
220                    height,
221                    thickness,
222                    color,
223                    custom_color,
224                    reverse_print,
225                } => {
226                    backend.draw_graphic_ellipse(
227                        *x,
228                        *y,
229                        *width,
230                        *height,
231                        *thickness,
232                        *color,
233                        custom_color.clone(),
234                        *reverse_print,
235                    )?;
236                }
237                common::ZplInstruction::GraphicField {
238                    condition: _,
239                    x,
240                    y,
241                    width,
242                    height,
243                    data,
244                    reverse_print,
245                } => {
246                    backend.draw_graphic_field(*x, *y, *width, *height, data, *reverse_print)?;
247                }
248                common::ZplInstruction::Code128 {
249                    condition: _,
250                    x,
251                    y,
252                    orientation,
253                    height,
254                    module_width,
255                    interpretation_line,
256                    interpretation_line_above,
257                    check_digit,
258                    mode,
259                    data,
260                    reverse_print,
261                } => {
262                    backend.draw_code128(
263                        *x,
264                        *y,
265                        *orientation,
266                        *height,
267                        *module_width,
268                        *interpretation_line,
269                        *interpretation_line_above,
270                        *check_digit,
271                        *mode,
272                        &replace_vars(data, variables),
273                        *reverse_print,
274                    )?;
275                }
276                common::ZplInstruction::QRCode {
277                    condition: _,
278                    x,
279                    y,
280                    orientation,
281                    model,
282                    magnification,
283                    error_correction,
284                    mask,
285                    data,
286                    reverse_print,
287                } => {
288                    backend.draw_qr_code(
289                        *x,
290                        *y,
291                        *orientation,
292                        *model,
293                        *magnification,
294                        *error_correction,
295                        *mask,
296                        &replace_vars(data, variables),
297                        *reverse_print,
298                    )?;
299                }
300                common::ZplInstruction::Code39 {
301                    condition: _,
302                    x,
303                    y,
304                    orientation,
305                    check_digit,
306                    height,
307                    module_width,
308                    interpretation_line,
309                    interpretation_line_above,
310                    data,
311                    reverse_print,
312                } => {
313                    backend.draw_code39(
314                        *x,
315                        *y,
316                        *orientation,
317                        *check_digit,
318                        *height,
319                        *module_width,
320                        *interpretation_line,
321                        *interpretation_line_above,
322                        &replace_vars(data, variables),
323                        *reverse_print,
324                    )?;
325                }
326                common::ZplInstruction::CustomImage {
327                    condition: _,
328                    x,
329                    y,
330                    width,
331                    height,
332                    data,
333                } => {
334                    backend.draw_graphic_image_custom(*x, *y, *width, *height, data)?;
335                }
336            }
337        }
338
339        let result = backend.finalize()?;
340
341        Ok(result)
342    }
343}