zpl-forge 0.2.1

A fast, memory-safe ZPL (Zebra Programming Language) parser and renderer.
Documentation
use std::collections::HashMap;
use std::sync::Arc;

use crate::{
    FontManager, ZplError, ZplResult,
    ast::parse_zpl,
    engine::{backend, common, font, intr},
};

/// The main entry point for processing and rendering ZPL labels.
///
/// `ZplEngine` holds the parsed instructions, label dimensions, and configuration
/// required to render a label using a specific backend.
#[derive(Debug)]
pub struct ZplEngine {
    instructions: Vec<common::ZplInstruction>,
    width: common::Unit,
    height: common::Unit,
    resolution: common::Resolution,
    fonts: Option<Arc<font::FontManager>>,
}

impl ZplEngine {
    /// Creates a new `ZplEngine` instance by parsing a ZPL string.
    ///
    /// # Arguments
    /// * `zpl` - The raw ZPL string to parse.
    /// * `width` - The physical width of the label.
    /// * `height` - The physical height of the label.
    /// * `resolution` - The printing resolution (DPI).
    ///
    /// # Errors
    /// Returns an error if the ZPL is invalid or if the instruction building fails.
    pub fn new(
        zpl: &str,
        width: common::Unit,
        height: common::Unit,
        resolution: common::Resolution,
    ) -> ZplResult<Self> {
        let commands = parse_zpl(zpl)?;
        if commands.is_empty() {
            return Err(ZplError::EmptyInput);
        }

        let instructions = intr::ZplInstructionBuilder::new(commands);
        let instructions = instructions.build()?;

        Ok(Self {
            instructions,
            width,
            height,
            resolution,
            fonts: None,
        })
    }

    /// Sets the font manager to be used during rendering.
    ///
    /// If no font manager is provided, a default one will be used.
    pub fn set_fonts(&mut self, fonts: Arc<font::FontManager>) {
        self.fonts = Some(fonts);
    }

    /// Renders the parsed instructions using the provided backend.
    ///
    /// # Arguments
    /// * `backend` - An implementation of `ZplForgeBackend` (e.g., PNG, PDF).
    /// * `variables` - A map of template variables to replace in text fields (format: `{{key}}`).
    ///
    /// # Errors
    /// Returns an error if rendering fails at the backend level.
    pub fn render<B: backend::ZplForgeBackend>(
        &self,
        mut backend: B,
        variables: &HashMap<String, String>,
    ) -> ZplResult<Vec<u8>> {
        fn replace_vars<'a>(
            s: &'a str,
            variables: &HashMap<String, String>,
        ) -> std::borrow::Cow<'a, str> {
            if variables.is_empty() || !s.contains("{{") {
                return std::borrow::Cow::Borrowed(s);
            }

            let mut result = String::new();
            let mut last_pos = 0;
            let mut found = false;
            let mut cursor = 0;

            while let Some(start_offset) = s[cursor..].find("{{") {
                let start = cursor + start_offset;
                if let Some(end_offset) = s[start + 2..].find("}}") {
                    let end = start + 2 + end_offset;
                    let key = &s[start + 2..end];
                    if let Some(value) = variables.get(key) {
                        if !found {
                            result.reserve(s.len());
                            found = true;
                        }
                        result.push_str(&s[last_pos..start]);
                        result.push_str(value);
                        last_pos = end + 2;
                        cursor = last_pos;
                        continue;
                    }
                }
                cursor = start + 2;
            }

            if found {
                result.push_str(&s[last_pos..]);
                std::borrow::Cow::Owned(result)
            } else {
                std::borrow::Cow::Borrowed(s)
            }
        }

        let w_dots = self.width.clone().to_dots(self.resolution);
        let h_dots = self.height.clone().to_dots(self.resolution);
        let font_manager = if let Some(fonts) = &self.fonts {
            fonts.clone()
        } else {
            Arc::new(FontManager::default())
        };

        backend.setup_page(w_dots as f64, h_dots as f64, self.resolution.dpi());
        backend.setup_font_manager(&font_manager);

        for instruction in &self.instructions {
            let condition = match instruction {
                common::ZplInstruction::Text { condition, .. } => condition,
                common::ZplInstruction::GraphicBox { condition, .. } => condition,
                common::ZplInstruction::GraphicCircle { condition, .. } => condition,
                common::ZplInstruction::GraphicEllipse { condition, .. } => condition,
                common::ZplInstruction::GraphicField { condition, .. } => condition,
                common::ZplInstruction::CustomImage { condition, .. } => condition,
                common::ZplInstruction::Code128 { condition, .. } => condition,
                common::ZplInstruction::QRCode { condition, .. } => condition,
                common::ZplInstruction::Code39 { condition, .. } => condition,
            };

            if let Some((var, expected)) = condition
                && variables.get(var) != Some(expected)
            {
                continue;
            }

            match instruction {
                common::ZplInstruction::Text {
                    condition: _,
                    x,
                    y,
                    font,
                    height,
                    width,
                    text,
                    reverse_print,
                    color,
                } => {
                    backend.draw_text(
                        *x,
                        *y,
                        *font,
                        *height,
                        *width,
                        &replace_vars(text, variables),
                        *reverse_print,
                        color.clone(),
                    )?;
                }
                common::ZplInstruction::GraphicBox {
                    condition: _,
                    x,
                    y,
                    width,
                    height,
                    thickness,
                    color,
                    custom_color,
                    rounding,
                    reverse_print,
                } => {
                    backend.draw_graphic_box(
                        *x,
                        *y,
                        *width,
                        *height,
                        *thickness,
                        *color,
                        custom_color.clone(),
                        *rounding,
                        *reverse_print,
                    )?;
                }
                common::ZplInstruction::GraphicCircle {
                    condition: _,
                    x,
                    y,
                    radius,
                    thickness,
                    color,
                    custom_color,
                    reverse_print,
                } => {
                    backend.draw_graphic_circle(
                        *x,
                        *y,
                        *radius,
                        *thickness,
                        *color,
                        custom_color.clone(),
                        *reverse_print,
                    )?;
                }
                common::ZplInstruction::GraphicEllipse {
                    condition: _,
                    x,
                    y,
                    width,
                    height,
                    thickness,
                    color,
                    custom_color,
                    reverse_print,
                } => {
                    backend.draw_graphic_ellipse(
                        *x,
                        *y,
                        *width,
                        *height,
                        *thickness,
                        *color,
                        custom_color.clone(),
                        *reverse_print,
                    )?;
                }
                common::ZplInstruction::GraphicField {
                    condition: _,
                    x,
                    y,
                    width,
                    height,
                    data,
                    reverse_print,
                } => {
                    backend.draw_graphic_field(*x, *y, *width, *height, data, *reverse_print)?;
                }
                common::ZplInstruction::Code128 {
                    condition: _,
                    x,
                    y,
                    orientation,
                    height,
                    module_width,
                    interpretation_line,
                    interpretation_line_above,
                    check_digit,
                    mode,
                    data,
                    reverse_print,
                } => {
                    backend.draw_code128(
                        *x,
                        *y,
                        *orientation,
                        *height,
                        *module_width,
                        *interpretation_line,
                        *interpretation_line_above,
                        *check_digit,
                        *mode,
                        &replace_vars(data, variables),
                        *reverse_print,
                    )?;
                }
                common::ZplInstruction::QRCode {
                    condition: _,
                    x,
                    y,
                    orientation,
                    model,
                    magnification,
                    error_correction,
                    mask,
                    data,
                    reverse_print,
                } => {
                    backend.draw_qr_code(
                        *x,
                        *y,
                        *orientation,
                        *model,
                        *magnification,
                        *error_correction,
                        *mask,
                        &replace_vars(data, variables),
                        *reverse_print,
                    )?;
                }
                common::ZplInstruction::Code39 {
                    condition: _,
                    x,
                    y,
                    orientation,
                    check_digit,
                    height,
                    module_width,
                    interpretation_line,
                    interpretation_line_above,
                    data,
                    reverse_print,
                } => {
                    backend.draw_code39(
                        *x,
                        *y,
                        *orientation,
                        *check_digit,
                        *height,
                        *module_width,
                        *interpretation_line,
                        *interpretation_line_above,
                        &replace_vars(data, variables),
                        *reverse_print,
                    )?;
                }
                common::ZplInstruction::CustomImage {
                    condition: _,
                    x,
                    y,
                    width,
                    height,
                    data,
                } => {
                    backend.draw_graphic_image_custom(*x, *y, *width, *height, data)?;
                }
            }
        }

        let result = backend.finalize()?;

        Ok(result)
    }
}