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    /// Renders the label directly to native vector PDF bytes using default backend settings.
198    ///
199    /// # Returns
200    /// A `ZplResult<Vec<u8>>` containing the raw PDF document bytes.
201    #[cfg(feature = "pdf")]
202    pub fn to_pdf(&self) -> ZplResult<Vec<u8>> {
203        self.render(
204            crate::forge::pdf_native::PdfNativeBackend::new(),
205            &HashMap::new(),
206        )
207    }
208
209    /// Renders the label directly to PNG image bytes using default backend settings.
210    ///
211    /// # Returns
212    /// A `ZplResult<Vec<u8>>` containing the raw PNG image bytes.
213    #[cfg(feature = "png")]
214    pub fn to_png(&self) -> ZplResult<Vec<u8>> {
215        self.render(crate::forge::png::PngBackend::new(), &HashMap::new())
216    }
217
218    /// Helper method to execute the parsed instructions on the provided backend.
219    fn render_instructions<B: backend::ZplForgeBackend>(
220        &self,
221        backend: &mut B,
222        variables: &HashMap<String, String>,
223        font_manager: &FontManager,
224    ) -> ZplResult<()> {
225        fn replace_vars<'a>(
226            s: &'a str,
227            variables: &HashMap<String, String>,
228        ) -> std::borrow::Cow<'a, str> {
229            if variables.is_empty() || !s.contains("{{") {
230                return std::borrow::Cow::Borrowed(s);
231            }
232
233            let mut result = String::new();
234            let mut last_pos = 0;
235            let mut found = false;
236            let mut cursor = 0;
237
238            while let Some(start_offset) = s[cursor..].find("{{") {
239                let start = cursor + start_offset;
240                if let Some(end_offset) = s[start + 2..].find("}}") {
241                    let end = start + 2 + end_offset;
242                    let key = &s[start + 2..end];
243                    if let Some(value) = variables.get(key) {
244                        if !found {
245                            result.reserve(s.len());
246                            found = true;
247                        }
248                        result.push_str(&s[last_pos..start]);
249                        result.push_str(value);
250                        last_pos = end + 2;
251                        cursor = last_pos;
252                        continue;
253                    }
254                }
255                cursor = start + 2;
256            }
257
258            if found {
259                result.push_str(&s[last_pos..]);
260                std::borrow::Cow::Owned(result)
261            } else {
262                std::borrow::Cow::Borrowed(s)
263            }
264        }
265
266        for instruction in &self.instructions {
267            if let common::ZplInstruction::PageBreak = instruction {
268                backend.new_page()?;
269                continue;
270            }
271
272            let condition = match instruction {
273                common::ZplInstruction::PageBreak => continue,
274                common::ZplInstruction::Text { condition, .. } => condition,
275                common::ZplInstruction::GraphicBox { condition, .. } => condition,
276                common::ZplInstruction::GraphicCircle { condition, .. } => condition,
277                common::ZplInstruction::GraphicEllipse { condition, .. } => condition,
278                common::ZplInstruction::GraphicField { condition, .. } => condition,
279                common::ZplInstruction::CustomImage { condition, .. } => condition,
280                common::ZplInstruction::Code128 { condition, .. } => condition,
281                common::ZplInstruction::QRCode { condition, .. } => condition,
282                common::ZplInstruction::Code39 { condition, .. } => condition,
283                common::ZplInstruction::DataMatrix { condition, .. } => condition,
284                common::ZplInstruction::Pdf417 { condition, .. } => condition,
285                common::ZplInstruction::Barcode1D { condition, .. } => condition,
286                common::ZplInstruction::GraphicDiagonal { condition, .. } => condition,
287                common::ZplInstruction::MicroPdf417 { condition, .. } => condition,
288                common::ZplInstruction::AztecCode { condition, .. } => condition,
289            };
290
291            if let Some((var, expected)) = condition
292                && variables.get(var) != Some(expected)
293            {
294                continue;
295            }
296
297            match instruction {
298                common::ZplInstruction::PageBreak => {}
299                common::ZplInstruction::Text {
300                    condition: _,
301                    x,
302                    y,
303                    font,
304                    height,
305                    width,
306                    orientation,
307                    text,
308                    reverse_print,
309                    color,
310                    block,
311                } => {
312                    let resolved = replace_vars(text, variables);
313
314                    let Some(b) = block else {
315                        backend.draw_text(
316                            *x,
317                            *y,
318                            *font,
319                            *height,
320                            *width,
321                            *orientation,
322                            &resolved,
323                            *reverse_print,
324                            color.clone(),
325                        )?;
326                        continue;
327                    };
328
329                    // ^FB: wrap into lines, justify, and place each line
330                    // according to the field orientation.
331                    let measure =
332                        |s: &str| measure_text_dots(font_manager, *font, *height, *width, s);
333                    let lines = wrap_text_block(&resolved, b.width, measure);
334                    let n_lines = lines.len().min(b.max_lines.max(1) as usize);
335
336                    let font_h = height.unwrap_or(9) as i32;
337                    let line_advance = (font_h + b.line_spacing).max(1);
338                    let block_span = (n_lines as i32 - 1) * line_advance;
339
340                    for (i, line) in lines.iter().take(n_lines).enumerate() {
341                        if line.is_empty() {
342                            continue;
343                        }
344                        let lw = measure(line) as i32;
345                        let indent = if i > 0 { b.indent as i32 } else { 0 };
346                        let avail = (b.width as i32 - indent).max(0);
347                        let jx = indent
348                            + match b.justification {
349                                'C' => (avail - lw).max(0) / 2,
350                                'R' => (avail - lw).max(0),
351                                _ => 0,
352                            };
353                        let ly = i as i32 * line_advance;
354
355                        // Cell top-left offset, rotated with the field.
356                        let (dx, dy) = match orientation {
357                            'R' => (block_span - ly, jx),
358                            'I' => (b.width as i32 - jx - lw, block_span - ly),
359                            'B' => (ly, b.width as i32 - jx - lw),
360                            _ => (jx, ly),
361                        };
362
363                        let fx = (*x as i32 + dx).max(0) as u32;
364                        let fy = (*y as i32 + dy).max(0) as u32;
365                        backend.draw_text(
366                            fx,
367                            fy,
368                            *font,
369                            *height,
370                            *width,
371                            *orientation,
372                            line,
373                            *reverse_print,
374                            color.clone(),
375                        )?;
376                    }
377                }
378                common::ZplInstruction::GraphicBox {
379                    condition: _,
380                    x,
381                    y,
382                    width,
383                    height,
384                    thickness,
385                    color,
386                    custom_color,
387                    rounding,
388                    reverse_print,
389                } => {
390                    backend.draw_graphic_box(
391                        *x,
392                        *y,
393                        *width,
394                        *height,
395                        *thickness,
396                        *color,
397                        custom_color.clone(),
398                        *rounding,
399                        *reverse_print,
400                    )?;
401                }
402                common::ZplInstruction::GraphicCircle {
403                    condition: _,
404                    x,
405                    y,
406                    radius,
407                    thickness,
408                    color,
409                    custom_color,
410                    reverse_print,
411                } => {
412                    backend.draw_graphic_circle(
413                        *x,
414                        *y,
415                        *radius,
416                        *thickness,
417                        *color,
418                        custom_color.clone(),
419                        *reverse_print,
420                    )?;
421                }
422                common::ZplInstruction::GraphicEllipse {
423                    condition: _,
424                    x,
425                    y,
426                    width,
427                    height,
428                    thickness,
429                    color,
430                    custom_color,
431                    reverse_print,
432                } => {
433                    backend.draw_graphic_ellipse(
434                        *x,
435                        *y,
436                        *width,
437                        *height,
438                        *thickness,
439                        *color,
440                        custom_color.clone(),
441                        *reverse_print,
442                    )?;
443                }
444                common::ZplInstruction::GraphicField {
445                    condition: _,
446                    x,
447                    y,
448                    width,
449                    height,
450                    data,
451                    reverse_print,
452                } => {
453                    backend.draw_graphic_field(*x, *y, *width, *height, data, *reverse_print)?;
454                }
455                common::ZplInstruction::Code128 {
456                    condition: _,
457                    x,
458                    y,
459                    orientation,
460                    height,
461                    module_width,
462                    interpretation_line,
463                    interpretation_line_above,
464                    check_digit,
465                    mode,
466                    data,
467                    reverse_print,
468                } => {
469                    backend.draw_code128(
470                        *x,
471                        *y,
472                        *orientation,
473                        *height,
474                        *module_width,
475                        *interpretation_line,
476                        *interpretation_line_above,
477                        *check_digit,
478                        *mode,
479                        &replace_vars(data, variables),
480                        *reverse_print,
481                    )?;
482                }
483                common::ZplInstruction::QRCode {
484                    condition: _,
485                    x,
486                    y,
487                    orientation,
488                    model,
489                    magnification,
490                    error_correction,
491                    mask,
492                    data,
493                    reverse_print,
494                } => {
495                    backend.draw_qr_code(
496                        *x,
497                        *y,
498                        *orientation,
499                        *model,
500                        *magnification,
501                        *error_correction,
502                        *mask,
503                        &replace_vars(data, variables),
504                        *reverse_print,
505                    )?;
506                }
507                common::ZplInstruction::Barcode1D {
508                    condition: _,
509                    kind,
510                    x,
511                    y,
512                    orientation,
513                    height,
514                    module_width,
515                    ratio,
516                    check_digit,
517                    interpretation_line,
518                    interpretation_line_above,
519                    data,
520                    reverse_print,
521                } => {
522                    backend.draw_barcode_1d(
523                        *kind,
524                        *x,
525                        *y,
526                        *orientation,
527                        *height,
528                        *module_width,
529                        *ratio,
530                        *check_digit,
531                        *interpretation_line,
532                        *interpretation_line_above,
533                        &replace_vars(data, variables),
534                        *reverse_print,
535                    )?;
536                }
537                common::ZplInstruction::GraphicDiagonal {
538                    condition: _,
539                    x,
540                    y,
541                    width,
542                    height,
543                    thickness,
544                    color,
545                    custom_color,
546                    diagonal_orientation,
547                    reverse_print,
548                } => {
549                    backend.draw_graphic_diagonal(
550                        *x,
551                        *y,
552                        *width,
553                        *height,
554                        *thickness,
555                        *color,
556                        custom_color.clone(),
557                        *diagonal_orientation,
558                        *reverse_print,
559                    )?;
560                }
561                common::ZplInstruction::DataMatrix {
562                    condition: _,
563                    x,
564                    y,
565                    orientation,
566                    module_size,
567                    data,
568                    reverse_print,
569                } => {
570                    backend.draw_datamatrix(
571                        *x,
572                        *y,
573                        *orientation,
574                        *module_size,
575                        &replace_vars(data, variables),
576                        *reverse_print,
577                    )?;
578                }
579                common::ZplInstruction::Pdf417 {
580                    condition: _,
581                    x,
582                    y,
583                    orientation,
584                    row_height,
585                    module_width,
586                    security_level,
587                    data,
588                    reverse_print,
589                } => {
590                    backend.draw_pdf417(
591                        *x,
592                        *y,
593                        *orientation,
594                        *row_height,
595                        *module_width,
596                        *security_level,
597                        &replace_vars(data, variables),
598                        *reverse_print,
599                    )?;
600                }
601                common::ZplInstruction::Code39 {
602                    condition: _,
603                    x,
604                    y,
605                    orientation,
606                    check_digit,
607                    height,
608                    module_width,
609                    ratio,
610                    interpretation_line,
611                    interpretation_line_above,
612                    data,
613                    reverse_print,
614                } => {
615                    backend.draw_code39(
616                        *x,
617                        *y,
618                        *orientation,
619                        *check_digit,
620                        *height,
621                        *module_width,
622                        *ratio,
623                        *interpretation_line,
624                        *interpretation_line_above,
625                        &replace_vars(data, variables),
626                        *reverse_print,
627                    )?;
628                }
629                common::ZplInstruction::CustomImage {
630                    condition: _,
631                    x,
632                    y,
633                    width,
634                    height,
635                    data,
636                } => {
637                    backend.draw_graphic_image_custom(*x, *y, *width, *height, data)?;
638                }
639                common::ZplInstruction::MicroPdf417 {
640                    condition: _,
641                    x,
642                    y,
643                    orientation,
644                    height,
645                    mode,
646                    data,
647                    reverse_print: _,
648                } => {
649                    backend.draw_micropdf417(
650                        *x,
651                        *y,
652                        *orientation,
653                        *height,
654                        *mode,
655                        &replace_vars(data, variables),
656                        false,
657                    )?;
658                }
659                common::ZplInstruction::AztecCode {
660                    condition: _,
661                    x,
662                    y,
663                    orientation,
664                    magnification,
665                    data,
666                    reverse_print: _,
667                } => {
668                    backend.draw_aztec_code(
669                        *x,
670                        *y,
671                        *orientation,
672                        *magnification,
673                        &replace_vars(data, variables),
674                        false,
675                    )?;
676                }
677            }
678        }
679
680        Ok(())
681    }
682}