Skip to main content

zpl_forge/engine/
engine.rs

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