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/// Measures the advance width of `text` in dots for the given ZPL font spec.
11fn measure_text_dots(
12    fm: &font::FontManager,
13    font_char: char,
14    height: Option<u32>,
15    width: Option<u32>,
16    text: &str,
17) -> u32 {
18    fm.measure_text(font_char, height, width, text)
19}
20
21/// Greedy word-wrap for `^FB`: fits words into `max_width` dots, hard-breaking
22/// words that are longer than a full line. `\&` acts as an explicit line break.
23fn wrap_text_block<F: Fn(&str) -> u32>(text: &str, max_width: u32, measure: F) -> Vec<String> {
24    let mut lines: Vec<String> = Vec::new();
25
26    for segment in text.split("\\&") {
27        if max_width == 0 {
28            lines.push(segment.trim().to_string());
29            continue;
30        }
31
32        let mut current = String::new();
33        for word in segment.split_whitespace() {
34            let candidate = if current.is_empty() {
35                word.to_string()
36            } else {
37                format!("{} {}", current, word)
38            };
39
40            if measure(&candidate) <= max_width {
41                current = candidate;
42                continue;
43            }
44
45            if !current.is_empty() {
46                lines.push(std::mem::take(&mut current));
47            }
48
49            // The word alone may still overflow: hard-break it by characters.
50            if measure(word) > max_width {
51                let mut piece = String::new();
52                for ch in word.chars() {
53                    piece.push(ch);
54                    if measure(&piece) > max_width && piece.chars().count() > 1 {
55                        piece.pop();
56                        lines.push(std::mem::take(&mut piece));
57                        piece.push(ch);
58                    }
59                }
60                current = piece;
61            } else {
62                current = word.to_string();
63            }
64        }
65        lines.push(current);
66    }
67
68    lines
69}
70
71/// The main entry point for processing and rendering ZPL labels.
72///
73/// `ZplEngine` holds the parsed instructions, label dimensions, and configuration
74/// required to render a label using a specific backend.
75#[derive(Debug)]
76pub struct ZplEngine {
77    instructions: Vec<common::ZplInstruction>,
78    width: common::Unit,
79    height: common::Unit,
80    resolution: common::Resolution,
81    fonts: Option<Arc<font::FontManager>>,
82}
83
84impl ZplEngine {
85    /// Creates a new `ZplEngine` instance by parsing a ZPL string.
86    ///
87    /// # Arguments
88    /// * `zpl` - The raw ZPL string to parse.
89    /// * `width` - The physical width of the label.
90    /// * `height` - The physical height of the label.
91    /// * `resolution` - The printing resolution (DPI).
92    ///
93    /// # Errors
94    /// Returns an error if the ZPL is invalid or if the instruction building fails.
95    pub fn new(
96        zpl: &str,
97        width: common::Unit,
98        height: common::Unit,
99        resolution: common::Resolution,
100    ) -> ZplResult<Self> {
101        let commands = parse_zpl(zpl)?;
102        if commands.is_empty() {
103            return Err(ZplError::EmptyInput);
104        }
105
106        let instructions = intr::ZplInstructionBuilder::new(commands);
107        let instructions = instructions.build()?;
108
109        Ok(Self {
110            instructions,
111            width,
112            height,
113            resolution,
114            fonts: None,
115        })
116    }
117
118    /// Sets the font manager to be used during rendering.
119    ///
120    /// If no font manager is provided, a default one will be used.
121    pub fn set_fonts(&mut self, fonts: Arc<font::FontManager>) {
122        self.fonts = Some(fonts);
123    }
124
125    /// Renders the parsed instructions using the provided backend.
126    ///
127    /// # Arguments
128    /// * `backend` - An implementation of `ZplForgeBackend` (e.g., PNG, PDF).
129    /// * `variables` - A map of template variables to replace in text fields (format: `{{key}}`).
130    ///
131    /// # Errors
132    /// Returns an error if rendering fails at the backend level.
133    pub fn render<B: backend::ZplForgeBackend>(
134        &self,
135        mut backend: B,
136        variables: &HashMap<String, String>,
137    ) -> ZplResult<Vec<u8>> {
138        let w_dots = self.width.clone().to_dots(self.resolution);
139        let h_dots = self.height.clone().to_dots(self.resolution);
140        let font_manager = if let Some(fonts) = &self.fonts {
141            fonts.clone()
142        } else {
143            Arc::new(FontManager::default())
144        };
145
146        backend.setup_page(w_dots as f64, h_dots as f64, self.resolution.dpi());
147        backend.setup_font_manager(&font_manager);
148
149        self.render_instructions(&mut backend, variables, &font_manager)?;
150
151        let result = backend.finalize()?;
152
153        Ok(result)
154    }
155
156    /// Renders the same parsed template multiple times into a single multi-page document,
157    /// using a different set of variables for each page.
158    ///
159    /// # Arguments
160    /// * `backend` - An implementation of `ZplForgeBackend` that supports multi-page (e.g., `PdfNativeBackend`).
161    /// * `pages_variables` - A slice of maps, where each map corresponds to a single page's variable assignments.
162    ///
163    /// # Errors
164    /// Returns an error if rendering fails at the backend level.
165    pub fn render_pages<B: backend::ZplForgeBackend>(
166        &self,
167        mut backend: B,
168        pages_variables: &[HashMap<String, String>],
169    ) -> ZplResult<Vec<u8>> {
170        if pages_variables.is_empty() {
171            return Ok(Vec::new());
172        }
173
174        let w_dots = self.width.clone().to_dots(self.resolution);
175        let h_dots = self.height.clone().to_dots(self.resolution);
176        let font_manager = if let Some(fonts) = &self.fonts {
177            fonts.clone()
178        } else {
179            Arc::new(FontManager::default())
180        };
181
182        backend.setup_page(w_dots as f64, h_dots as f64, self.resolution.dpi());
183        backend.setup_font_manager(&font_manager);
184
185        for (page_idx, variables) in pages_variables.iter().enumerate() {
186            if page_idx > 0 {
187                backend.new_page()?;
188            }
189            self.render_instructions(&mut backend, variables, &font_manager)?;
190        }
191
192        let result = backend.finalize()?;
193
194        Ok(result)
195    }
196
197    /// Helper method to execute the parsed instructions on the provided backend.
198    fn render_instructions<B: backend::ZplForgeBackend>(
199        &self,
200        backend: &mut B,
201        variables: &HashMap<String, String>,
202        font_manager: &FontManager,
203    ) -> ZplResult<()> {
204        fn replace_vars<'a>(
205            s: &'a str,
206            variables: &HashMap<String, String>,
207        ) -> std::borrow::Cow<'a, str> {
208            if variables.is_empty() || !s.contains("{{") {
209                return std::borrow::Cow::Borrowed(s);
210            }
211
212            let mut result = String::new();
213            let mut last_pos = 0;
214            let mut found = false;
215            let mut cursor = 0;
216
217            while let Some(start_offset) = s[cursor..].find("{{") {
218                let start = cursor + start_offset;
219                if let Some(end_offset) = s[start + 2..].find("}}") {
220                    let end = start + 2 + end_offset;
221                    let key = &s[start + 2..end];
222                    if let Some(value) = variables.get(key) {
223                        if !found {
224                            result.reserve(s.len());
225                            found = true;
226                        }
227                        result.push_str(&s[last_pos..start]);
228                        result.push_str(value);
229                        last_pos = end + 2;
230                        cursor = last_pos;
231                        continue;
232                    }
233                }
234                cursor = start + 2;
235            }
236
237            if found {
238                result.push_str(&s[last_pos..]);
239                std::borrow::Cow::Owned(result)
240            } else {
241                std::borrow::Cow::Borrowed(s)
242            }
243        }
244
245        for instruction in &self.instructions {
246            if let common::ZplInstruction::PageBreak = instruction {
247                backend.new_page()?;
248                continue;
249            }
250
251            let condition = match instruction {
252                common::ZplInstruction::PageBreak => continue,
253                common::ZplInstruction::Text { condition, .. } => condition,
254                common::ZplInstruction::GraphicBox { condition, .. } => condition,
255                common::ZplInstruction::GraphicCircle { condition, .. } => condition,
256                common::ZplInstruction::GraphicEllipse { condition, .. } => condition,
257                common::ZplInstruction::GraphicField { condition, .. } => condition,
258                common::ZplInstruction::CustomImage { condition, .. } => condition,
259                common::ZplInstruction::Code128 { condition, .. } => condition,
260                common::ZplInstruction::QRCode { condition, .. } => condition,
261                common::ZplInstruction::Code39 { condition, .. } => condition,
262                common::ZplInstruction::DataMatrix { condition, .. } => condition,
263                common::ZplInstruction::Pdf417 { condition, .. } => condition,
264                common::ZplInstruction::Barcode1D { condition, .. } => condition,
265                common::ZplInstruction::GraphicDiagonal { condition, .. } => condition,
266            };
267
268            if let Some((var, expected)) = condition
269                && variables.get(var) != Some(expected)
270            {
271                continue;
272            }
273
274            match instruction {
275                common::ZplInstruction::PageBreak => {}
276                common::ZplInstruction::Text {
277                    condition: _,
278                    x,
279                    y,
280                    font,
281                    height,
282                    width,
283                    orientation,
284                    text,
285                    reverse_print,
286                    color,
287                    block,
288                } => {
289                    let resolved = replace_vars(text, variables);
290
291                    let Some(b) = block else {
292                        backend.draw_text(
293                            *x,
294                            *y,
295                            *font,
296                            *height,
297                            *width,
298                            *orientation,
299                            &resolved,
300                            *reverse_print,
301                            color.clone(),
302                        )?;
303                        continue;
304                    };
305
306                    // ^FB: wrap into lines, justify, and place each line
307                    // according to the field orientation.
308                    let measure =
309                        |s: &str| measure_text_dots(font_manager, *font, *height, *width, s);
310                    let lines = wrap_text_block(&resolved, b.width, measure);
311                    let n_lines = lines.len().min(b.max_lines.max(1) as usize);
312
313                    let font_h = height.unwrap_or(9) as i32;
314                    let line_advance = (font_h + b.line_spacing).max(1);
315                    let block_span = (n_lines as i32 - 1) * line_advance;
316
317                    for (i, line) in lines.iter().take(n_lines).enumerate() {
318                        if line.is_empty() {
319                            continue;
320                        }
321                        let lw = measure(line) as i32;
322                        let indent = if i > 0 { b.indent as i32 } else { 0 };
323                        let avail = (b.width as i32 - indent).max(0);
324                        let jx = indent
325                            + match b.justification {
326                                'C' => (avail - lw).max(0) / 2,
327                                'R' => (avail - lw).max(0),
328                                _ => 0,
329                            };
330                        let ly = i as i32 * line_advance;
331
332                        // Cell top-left offset, rotated with the field.
333                        let (dx, dy) = match orientation {
334                            'R' => (block_span - ly, jx),
335                            'I' => (b.width as i32 - jx - lw, block_span - ly),
336                            'B' => (ly, b.width as i32 - jx - lw),
337                            _ => (jx, ly),
338                        };
339
340                        let fx = (*x as i32 + dx).max(0) as u32;
341                        let fy = (*y as i32 + dy).max(0) as u32;
342                        backend.draw_text(
343                            fx,
344                            fy,
345                            *font,
346                            *height,
347                            *width,
348                            *orientation,
349                            line,
350                            *reverse_print,
351                            color.clone(),
352                        )?;
353                    }
354                }
355                common::ZplInstruction::GraphicBox {
356                    condition: _,
357                    x,
358                    y,
359                    width,
360                    height,
361                    thickness,
362                    color,
363                    custom_color,
364                    rounding,
365                    reverse_print,
366                } => {
367                    backend.draw_graphic_box(
368                        *x,
369                        *y,
370                        *width,
371                        *height,
372                        *thickness,
373                        *color,
374                        custom_color.clone(),
375                        *rounding,
376                        *reverse_print,
377                    )?;
378                }
379                common::ZplInstruction::GraphicCircle {
380                    condition: _,
381                    x,
382                    y,
383                    radius,
384                    thickness,
385                    color,
386                    custom_color,
387                    reverse_print,
388                } => {
389                    backend.draw_graphic_circle(
390                        *x,
391                        *y,
392                        *radius,
393                        *thickness,
394                        *color,
395                        custom_color.clone(),
396                        *reverse_print,
397                    )?;
398                }
399                common::ZplInstruction::GraphicEllipse {
400                    condition: _,
401                    x,
402                    y,
403                    width,
404                    height,
405                    thickness,
406                    color,
407                    custom_color,
408                    reverse_print,
409                } => {
410                    backend.draw_graphic_ellipse(
411                        *x,
412                        *y,
413                        *width,
414                        *height,
415                        *thickness,
416                        *color,
417                        custom_color.clone(),
418                        *reverse_print,
419                    )?;
420                }
421                common::ZplInstruction::GraphicField {
422                    condition: _,
423                    x,
424                    y,
425                    width,
426                    height,
427                    data,
428                    reverse_print,
429                } => {
430                    backend.draw_graphic_field(*x, *y, *width, *height, data, *reverse_print)?;
431                }
432                common::ZplInstruction::Code128 {
433                    condition: _,
434                    x,
435                    y,
436                    orientation,
437                    height,
438                    module_width,
439                    interpretation_line,
440                    interpretation_line_above,
441                    check_digit,
442                    mode,
443                    data,
444                    reverse_print,
445                } => {
446                    backend.draw_code128(
447                        *x,
448                        *y,
449                        *orientation,
450                        *height,
451                        *module_width,
452                        *interpretation_line,
453                        *interpretation_line_above,
454                        *check_digit,
455                        *mode,
456                        &replace_vars(data, variables),
457                        *reverse_print,
458                    )?;
459                }
460                common::ZplInstruction::QRCode {
461                    condition: _,
462                    x,
463                    y,
464                    orientation,
465                    model,
466                    magnification,
467                    error_correction,
468                    mask,
469                    data,
470                    reverse_print,
471                } => {
472                    backend.draw_qr_code(
473                        *x,
474                        *y,
475                        *orientation,
476                        *model,
477                        *magnification,
478                        *error_correction,
479                        *mask,
480                        &replace_vars(data, variables),
481                        *reverse_print,
482                    )?;
483                }
484                common::ZplInstruction::Barcode1D {
485                    condition: _,
486                    kind,
487                    x,
488                    y,
489                    orientation,
490                    height,
491                    module_width,
492                    interpretation_line,
493                    interpretation_line_above,
494                    data,
495                    reverse_print,
496                } => {
497                    backend.draw_barcode_1d(
498                        *kind,
499                        *x,
500                        *y,
501                        *orientation,
502                        *height,
503                        *module_width,
504                        *interpretation_line,
505                        *interpretation_line_above,
506                        &replace_vars(data, variables),
507                        *reverse_print,
508                    )?;
509                }
510                common::ZplInstruction::GraphicDiagonal {
511                    condition: _,
512                    x,
513                    y,
514                    width,
515                    height,
516                    thickness,
517                    color,
518                    custom_color,
519                    diagonal_orientation,
520                    reverse_print,
521                } => {
522                    backend.draw_graphic_diagonal(
523                        *x,
524                        *y,
525                        *width,
526                        *height,
527                        *thickness,
528                        *color,
529                        custom_color.clone(),
530                        *diagonal_orientation,
531                        *reverse_print,
532                    )?;
533                }
534                common::ZplInstruction::DataMatrix {
535                    condition: _,
536                    x,
537                    y,
538                    orientation,
539                    module_size,
540                    data,
541                    reverse_print,
542                } => {
543                    backend.draw_datamatrix(
544                        *x,
545                        *y,
546                        *orientation,
547                        *module_size,
548                        &replace_vars(data, variables),
549                        *reverse_print,
550                    )?;
551                }
552                common::ZplInstruction::Pdf417 {
553                    condition: _,
554                    x,
555                    y,
556                    orientation,
557                    row_height,
558                    module_width,
559                    security_level,
560                    data,
561                    reverse_print,
562                } => {
563                    backend.draw_pdf417(
564                        *x,
565                        *y,
566                        *orientation,
567                        *row_height,
568                        *module_width,
569                        *security_level,
570                        &replace_vars(data, variables),
571                        *reverse_print,
572                    )?;
573                }
574                common::ZplInstruction::Code39 {
575                    condition: _,
576                    x,
577                    y,
578                    orientation,
579                    check_digit,
580                    height,
581                    module_width,
582                    interpretation_line,
583                    interpretation_line_above,
584                    data,
585                    reverse_print,
586                } => {
587                    backend.draw_code39(
588                        *x,
589                        *y,
590                        *orientation,
591                        *check_digit,
592                        *height,
593                        *module_width,
594                        *interpretation_line,
595                        *interpretation_line_above,
596                        &replace_vars(data, variables),
597                        *reverse_print,
598                    )?;
599                }
600                common::ZplInstruction::CustomImage {
601                    condition: _,
602                    x,
603                    y,
604                    width,
605                    height,
606                    data,
607                } => {
608                    backend.draw_graphic_image_custom(*x, *y, *width, *height, data)?;
609                }
610            }
611        }
612
613        Ok(())
614    }
615}