Skip to main content

zpl_builder/
builder.rs

1//! Builder to build a ZPL label
2
3use crate::elements::{
4    BarcodeElement, GraphicalBoxElement, GraphicalCircleElement, GraphicalDiagonalLineElement,
5    GraphicalEllipseElement, TextElement,
6};
7use crate::types::{
8    AxisPosition, BarcodeHeight, BarcodeWidth, BoxDimension, Font, FontSize, GraphicDimension,
9    Rounding, Thickness, WidthRatio, ZplError,
10};
11use std::fmt::Display;
12
13/// Orientation of a text or barcode field.
14///
15/// Applies to [`LabelBuilder::add_text`] and [`LabelBuilder::add_barcode`].
16/// The rotation is applied clockwise relative to the normal reading direction.
17#[derive(Debug, PartialEq, Copy, Clone)]
18pub enum Orientation {
19    /// No rotation — standard left-to-right reading direction.
20    Normal,
21    /// Rotated 90 degrees clockwise.
22    Rotated,
23    /// Rotated 180 degrees — upside down.
24    Inverted,
25    /// Rotated 270 degrees clockwise (read from bottom to top).
26    Bottom,
27}
28
29impl Display for Orientation {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        match self {
32            Orientation::Normal => write!(f, "N"),
33            Orientation::Rotated => write!(f, "R"),
34            Orientation::Inverted => write!(f, "I"),
35            Orientation::Bottom => write!(f, "B"),
36        }
37    }
38}
39
40/// Orientation of a diagonal line.
41///
42/// Applies to [`LabelBuilder::add_graphical_diagonal_line`].
43/// The orientation describes the direction the line runs within its bounding box.
44///
45/// This is a separate type from [`Orientation`] because the ZPL `^GD` command
46/// uses `R` and `L` rather than the `N`/`R`/`I`/`B` values used by other elements.
47#[derive(Debug, PartialEq, Copy, Clone)]
48pub enum DiagonalOrientation {
49    /// Line runs from the top-left corner to the bottom-right corner ( `\` ).
50    RightLeaning,
51    /// Line runs from the bottom-left corner to the top-right corner ( `/` ).
52    LeftLeaning,
53}
54
55impl Display for DiagonalOrientation {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            DiagonalOrientation::RightLeaning => write!(f, "R"),
59            DiagonalOrientation::LeftLeaning => write!(f, "L"),
60        }
61    }
62}
63
64/// Barcode symbology.
65///
66/// Passed to [`LabelBuilder::add_barcode`] to select the encoding format.
67/// The wide-to-narrow bar ratio set via `width_ratio` has no effect on
68/// fixed-ratio symbologies such as [`BarcodeType::Code128`] or
69/// [`BarcodeType::QrCode`].
70#[derive(Debug, PartialEq, Copy, Clone)]
71#[allow(missing_docs)]
72pub enum BarcodeType {
73    Aztec,
74    Code11,
75    Interleaved,
76    Code39,
77    Code49,
78    PlanetCode,
79    Pdf417,
80    EAN8,
81    UpcE,
82    Code93,
83    CodaBlock,
84    Code128,
85    UPSMaxiCode,
86    EAN13,
87    MicroPDF417,
88    Industrial,
89    Standard,
90    ANSICodeBar,
91    LogMars,
92    MSI,
93    Plessey,
94    QrCode,
95    Rss,
96    UpcEanExtension,
97    Tlc39,
98    UpcA,
99    DataMatrix,
100}
101
102impl Display for BarcodeType {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        match self {
105            BarcodeType::Aztec => write!(f, "^BO"),
106            BarcodeType::Code11 => write!(f, "^B1"),
107            BarcodeType::Interleaved => write!(f, "^B2"),
108            BarcodeType::Code39 => write!(f, "^B3"),
109            BarcodeType::Code49 => write!(f, "^B4"),
110            BarcodeType::PlanetCode => write!(f, "^B5"),
111            BarcodeType::Pdf417 => write!(f, "^B7"),
112            BarcodeType::EAN8 => write!(f, "^B8"),
113            BarcodeType::UpcE => write!(f, "^B9"),
114            BarcodeType::Code93 => write!(f, "^BA"),
115            BarcodeType::CodaBlock => write!(f, "^BB"),
116            BarcodeType::Code128 => write!(f, "^BC"),
117            BarcodeType::UPSMaxiCode => write!(f, "^BD"),
118            BarcodeType::EAN13 => write!(f, "^BE"),
119            BarcodeType::MicroPDF417 => write!(f, "^BF"),
120            BarcodeType::Industrial => write!(f, "^BI"),
121            BarcodeType::Standard => write!(f, "^BJ"),
122            BarcodeType::ANSICodeBar => write!(f, "^BK"),
123            BarcodeType::LogMars => write!(f, "^BL"),
124            BarcodeType::MSI => write!(f, "^BM"),
125            BarcodeType::Plessey => write!(f, "^BP"),
126            BarcodeType::QrCode => write!(f, "^BQ"),
127            BarcodeType::Rss => write!(f, "^BR"),
128            BarcodeType::UpcEanExtension => write!(f, "^BS"),
129            BarcodeType::Tlc39 => write!(f, "^BT"),
130            BarcodeType::UpcA => write!(f, "^BU"),
131            BarcodeType::DataMatrix => write!(f, "^BX"),
132        }
133    }
134}
135
136/// Line and fill color for graphical elements.
137///
138/// Passed to box, circle, ellipse and diagonal line methods.
139/// `White` prints white on a black background, which is useful for
140/// inverting areas of a label.
141#[derive(Debug, PartialEq, Copy, Clone)]
142#[allow(missing_docs)]
143pub enum Color {
144    /// Solid black.
145    Black,
146    /// Solid white (transparent on a white label background).
147    White,
148}
149
150impl Display for Color {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        match self {
153            Color::Black => write!(f, "B"),
154            Color::White => write!(f, "W"),
155        }
156    }
157}
158
159/// Builder for ZPL II label strings.
160///
161/// Elements are added in order; the final ZPL string is produced by calling
162/// [`build`](LabelBuilder::build). Every method that adds an element validates
163/// its arguments and returns a [`ZplError`] if any value is out of range.
164///
165/// # Example
166///
167/// ```rust
168/// use zpl_builder::{LabelBuilder, BarcodeType, Color, Orientation};
169///
170/// let zpl = LabelBuilder::new()
171///     .set_home_position(10, 10).unwrap()
172///     .add_text("Hello, printer!", 50, 50, 'A', 30, Orientation::Normal).unwrap()
173///     .add_graphical_box(10, 10, 500, 300, 3, Color::Black, 0).unwrap()
174///     .add_barcode(BarcodeType::Code128, "ABC-123", 50, 120, 2, 2.5, 80, Orientation::Normal).unwrap()
175///     .build();
176///
177/// assert!(zpl.starts_with("^XA"));
178/// assert!(zpl.ends_with("^XZ"));
179/// ```
180#[derive(Debug, PartialEq)]
181pub struct LabelBuilder {
182    elements: Vec<String>,
183    x_origin: AxisPosition,
184    y_origin: AxisPosition,
185}
186
187fn get_axis_position(x: u16, y: u16) -> Result<(AxisPosition, AxisPosition), ZplError> {
188    let x_position = AxisPosition::try_from(x)?;
189    let y_position = AxisPosition::try_from(y)?;
190    Ok((x_position, y_position))
191}
192
193impl LabelBuilder {
194    /// Create a new, empty label builder.
195    ///
196    /// The home position defaults to `(0, 0)`. Call
197    /// [`set_home_position`](LabelBuilder::set_home_position) to change it.
198    #[must_use]
199    pub fn new() -> Self {
200        Self {
201            elements: Vec::new(),
202            x_origin: AxisPosition(0),
203            y_origin: AxisPosition(0),
204        }
205    }
206
207    /// Set the label home position (`^LH`).
208    ///
209    /// The home position is the axis reference point for the label. Any area
210    /// below and to the right of this point is available for printing. It is
211    /// recommended to call this before adding any elements.
212    ///
213    /// # Arguments
214    ///
215    /// * `x_origin` — x-axis offset in dots. Range: `0–32000`. Default: `0`.
216    /// * `y_origin` — y-axis offset in dots. Range: `0–32000`. Default: `0`.
217    ///
218    /// # Errors
219    ///
220    /// Returns [`ZplError::InvalidPosition`] if either value exceeds `32000`.
221    ///
222    /// # Example
223    ///
224    /// ```rust
225    /// # use zpl_builder::LabelBuilder;
226    /// let zpl = LabelBuilder::new()
227    ///     .set_home_position(50, 10).unwrap()
228    ///     .build();
229    ///
230    /// assert!(zpl.contains("^LH50,10"));
231    /// ```
232    pub fn set_home_position(mut self, x_origin: u16, y_origin: u16) -> Result<Self, ZplError> {
233        let (x_origin, y_origin) = get_axis_position(x_origin, y_origin)?;
234        self.x_origin = x_origin;
235        self.y_origin = y_origin;
236        Ok(self)
237    }
238
239    /// Add a text field (`^A` / `^FD`).
240    ///
241    /// # Arguments
242    ///
243    /// * `text` — the string to print.
244    /// * `x` — x-axis position in dots. Range: `0–32000`.
245    /// * `y` — y-axis position in dots. Range: `0–32000`.
246    /// * `font` — ZPL font name: a single ASCII uppercase letter (`A`–`Z`)
247    ///   or a digit (`1`–`9`). The digit `0` (zero) is not valid.
248    /// * `font_size` — font height in dots. Range: `10–32000`.
249    /// * `orientation` — field rotation. See [`Orientation`].
250    ///
251    /// # Errors
252    ///
253    /// | Error | Condition |
254    /// |---|---|
255    /// | [`ZplError::InvalidPosition`] | `x` or `y` > 32000 |
256    /// | [`ZplError::InvalidFont`] | `font` is not `A`–`Z` or `1`–`9` |
257    /// | [`ZplError::InvalidFontSize`] | `font_size` < 10 or > 32000 |
258    ///
259    /// # Example
260    ///
261    /// ```rust
262    /// # use zpl_builder::{LabelBuilder, Orientation};
263    /// let zpl = LabelBuilder::new()
264    ///     .add_text("Hello", 10, 10, 'A', 30, Orientation::Normal).unwrap()
265    ///     .build();
266    ///
267    /// assert!(zpl.contains("^FDHello^FS"));
268    /// ```
269    pub fn add_text(
270        mut self,
271        text: &str,
272        x: u16,
273        y: u16,
274        font: char,
275        font_size: u16,
276        orientation: Orientation,
277    ) -> Result<Self, ZplError> {
278        let (x, y) = get_axis_position(x, y)?;
279        let font_size = FontSize::try_from(font_size)?;
280        let font = Font::try_from(font)?;
281        let zpl_text = TextElement::new(text, x, y, font, font_size, orientation).to_zpl();
282        self.elements.push(zpl_text);
283        Ok(self)
284    }
285
286    /// Add a barcode field (`^BY` / `^B*`).
287    ///
288    /// # Arguments
289    ///
290    /// * `_type` — barcode symbology. See [`BarcodeType`].
291    /// * `data` — the string to encode.
292    /// * `x` — x-axis position in dots. Range: `0–32000`.
293    /// * `y` — y-axis position in dots. Range: `0–32000`.
294    /// * `width` — width of the narrowest bar in dots. Range: `1–10`.
295    /// * `width_ratio` — wide bar to narrow bar ratio. Range: `2.0–3.0`,
296    ///   step `0.1`. Has no effect on fixed-ratio symbologies.
297    /// * `height` — barcode height in dots. Range: `1–32000`.
298    /// * `orientation` — field rotation. See [`Orientation`].
299    ///
300    /// # Errors
301    ///
302    /// | Error | Condition |
303    /// |---|---|
304    /// | [`ZplError::InvalidPosition`] | `x` or `y` > 32000 |
305    /// | [`ZplError::InvalidBarcodeWidth`] | `width` < 1 or > 10 |
306    /// | [`ZplError::InvalidWidthRatio`] | `width_ratio` outside `2.0–3.0` |
307    /// | [`ZplError::InvalidBarcodeHeight`] | `height` < 1 or > 32000 |
308    ///
309    /// # Example
310    ///
311    /// ```rust
312    /// # use zpl_builder::{BarcodeType, LabelBuilder, Orientation};
313    /// let zpl = LabelBuilder::new()
314    ///     .add_barcode(BarcodeType::Code128, "ABC-123", 10, 10, 2, 2.5, 80, Orientation::Normal).unwrap()
315    ///     .build();
316    ///
317    /// assert!(zpl.contains("^FDABC-123^FS"));
318    /// ```
319    pub fn add_barcode(
320        mut self,
321        _type: BarcodeType,
322        data: &str,
323        x: u16,
324        y: u16,
325        width: u8,
326        width_ratio: f32,
327        height: u16,
328        orientation: Orientation,
329    ) -> Result<Self, ZplError> {
330        let (x, y) = get_axis_position(x, y)?;
331        let width = BarcodeWidth::try_from(width)?;
332        let width_ratio = WidthRatio::try_from(width_ratio)?;
333        let height = BarcodeHeight::try_from(height)?;
334        let zpl_barcode =
335            BarcodeElement::new(_type, data, x, y, width, width_ratio, height, orientation)
336                .to_zpl();
337        self.elements.push(zpl_barcode);
338        Ok(self)
339    }
340
341    /// Add a graphical box (`^GB`).
342    ///
343    /// Draws a rectangle outline. Setting `width` and `height` equal to
344    /// `thickness` produces a filled square. Set `rounding` to `0` for sharp
345    /// corners.
346    ///
347    /// # Arguments
348    ///
349    /// * `x` — x-axis position in dots. Range: `0–32000`.
350    /// * `y` — y-axis position in dots. Range: `0–32000`.
351    /// * `width` — box width in dots. Must be ≥ `thickness`. Range: `thickness–32000`.
352    /// * `height` — box height in dots. Must be ≥ `thickness`. Range: `thickness–32000`.
353    /// * `thickness` — line thickness in dots. Range: `1–32000`.
354    /// * `color` — line color. See [`Color`].
355    /// * `rounding` — corner rounding amount. Range: `0–8` (`0` = sharp, `8` = most rounded).
356    ///
357    /// # Errors
358    ///
359    /// | Error | Condition |
360    /// |---|---|
361    /// | [`ZplError::InvalidPosition`] | `x` or `y` > 32000 |
362    /// | [`ZplError::InvalidThickness`] | `thickness` < 1 or > 32000 |
363    /// | [`ZplError::InvalidBoxDimension`] | `width` or `height` < `thickness` or > 32000 |
364    /// | [`ZplError::InvalidRounding`] | `rounding` > 8 |
365    ///
366    /// # Example
367    ///
368    /// ```rust
369    /// # use zpl_builder::{LabelBuilder, Color};
370    /// let zpl = LabelBuilder::new()
371    ///     .add_graphical_box(10, 10, 200, 100, 3, Color::Black, 0).unwrap()
372    ///     .build();
373    ///
374    /// assert!(zpl.contains("^GB200,100,3,B,0"));
375    /// ```
376    pub fn add_graphical_box(
377        mut self,
378        x: u16,
379        y: u16,
380        width: u16,
381        height: u16,
382        thickness: u16,
383        color: Color,
384        rounding: u8,
385    ) -> Result<Self, ZplError> {
386        let (x, y) = get_axis_position(x, y)?;
387        let rounding = Rounding::try_from(rounding)?;
388        let thickness = Thickness::try_from(thickness)?;
389        let width = BoxDimension::try_new(width, thickness)?;
390        let height = BoxDimension::try_new(height, thickness)?;
391        let zpl_box =
392            GraphicalBoxElement::new(x, y, width, height, thickness, color, rounding).to_zpl();
393        self.elements.push(zpl_box);
394        Ok(self)
395    }
396
397    /// Add a graphical circle (`^GC`).
398    ///
399    /// # Arguments
400    ///
401    /// * `x` — x-axis position in dots. Range: `0–32000`.
402    /// * `y` — y-axis position in dots. Range: `0–32000`.
403    /// * `diameter` — outer diameter in dots. Range: `3–4095`.
404    /// * `thickness` — line thickness in dots. Range: `1–32000`.
405    ///   Set equal to `diameter` to produce a filled circle.
406    /// * `color` — line color. See [`Color`].
407    ///
408    /// # Errors
409    ///
410    /// | Error | Condition |
411    /// |---|---|
412    /// | [`ZplError::InvalidPosition`] | `x` or `y` > 32000 |
413    /// | [`ZplError::InvalidThickness`] | `thickness` < 1 or > 32000 |
414    /// | [`ZplError::InvalidGraphicDimension`] | `diameter` < 3 or > 4095 |
415    ///
416    /// # Example
417    ///
418    /// ```rust
419    /// # use zpl_builder::{LabelBuilder, Color};
420    /// let zpl = LabelBuilder::new()
421    ///     .add_graphical_circle(50, 50, 100, 3, Color::Black).unwrap()
422    ///     .build();
423    ///
424    /// assert!(zpl.contains("^GC100,3,B"));
425    /// ```
426    pub fn add_graphical_circle(
427        mut self,
428        x: u16,
429        y: u16,
430        diameter: u16,
431        thickness: u16,
432        color: Color,
433    ) -> Result<Self, ZplError> {
434        let (x, y) = get_axis_position(x, y)?;
435        let thickness = Thickness::try_from(thickness)?;
436        let diameter = GraphicDimension::try_from(diameter)?;
437        let zpl_circle = GraphicalCircleElement::new(x, y, diameter, thickness, color).to_zpl();
438        self.elements.push(zpl_circle);
439        Ok(self)
440    }
441
442    /// Add a graphical ellipse (`^GE`).
443    ///
444    /// # Arguments
445    ///
446    /// * `x` — x-axis position in dots. Range: `0–32000`.
447    /// * `y` — y-axis position in dots. Range: `0–32000`.
448    /// * `width` — ellipse width in dots. Range: `3–4095`.
449    /// * `height` — ellipse height in dots. Range: `3–4095`.
450    /// * `thickness` — line thickness in dots. Range: `1–32000`.
451    /// * `color` — line color. See [`Color`].
452    ///
453    /// # Errors
454    ///
455    /// | Error | Condition |
456    /// |---|---|
457    /// | [`ZplError::InvalidPosition`] | `x` or `y` > 32000 |
458    /// | [`ZplError::InvalidThickness`] | `thickness` < 1 or > 32000 |
459    /// | [`ZplError::InvalidGraphicDimension`] | `width` or `height` < 3 or > 4095 |
460    ///
461    /// # Example
462    ///
463    /// ```rust
464    /// # use zpl_builder::{LabelBuilder, Color};
465    /// let zpl = LabelBuilder::new()
466    ///     .add_graphical_ellipse(20, 20, 200, 100, 2, Color::Black).unwrap()
467    ///     .build();
468    ///
469    /// assert!(zpl.contains("^GE200,100,2,B"));
470    /// ```
471    pub fn add_graphical_ellipse(
472        mut self,
473        x: u16,
474        y: u16,
475        width: u16,
476        height: u16,
477        thickness: u16,
478        color: Color,
479    ) -> Result<Self, ZplError> {
480        let (x, y) = get_axis_position(x, y)?;
481        let thickness = Thickness::try_from(thickness)?;
482        let width = GraphicDimension::try_from(width)?;
483        let height = GraphicDimension::try_from(height)?;
484        let zpl_ellipse =
485            GraphicalEllipseElement::new(x, y, width, height, thickness, color).to_zpl();
486        self.elements.push(zpl_ellipse);
487        Ok(self)
488    }
489
490    /// Add a diagonal line (`^GD`).
491    ///
492    /// The line is drawn within a bounding box of size `box_width × box_height`.
493    /// A square bounding box (`box_width == box_height`) produces a 45° angle.
494    ///
495    /// # Arguments
496    ///
497    /// * `x` — x-axis position of the bounding box in dots. Range: `0–32000`.
498    /// * `y` — y-axis position of the bounding box in dots. Range: `0–32000`.
499    /// * `box_width` — width of the bounding box in dots. Must be ≥ `thickness`.
500    ///   Range: `thickness–32000`.
501    /// * `box_height` — height of the bounding box in dots. Must be ≥ `thickness`.
502    ///   Range: `thickness–32000`.
503    /// * `thickness` — line thickness in dots. Range: `1–32000`.
504    /// * `color` — line color. See [`Color`].
505    /// * `orientation` — direction of the line. See [`DiagonalOrientation`].
506    ///
507    /// # Errors
508    ///
509    /// | Error | Condition |
510    /// |---|---|
511    /// | [`ZplError::InvalidPosition`] | `x` or `y` > 32000 |
512    /// | [`ZplError::InvalidThickness`] | `thickness` < 1 or > 32000 |
513    /// | [`ZplError::InvalidBoxDimension`] | `box_width` or `box_height` < `thickness` or > 32000 |
514    ///
515    /// # Example
516    ///
517    /// ```rust
518    /// # use zpl_builder::{LabelBuilder, Color, DiagonalOrientation};
519    /// let zpl = LabelBuilder::new()
520    ///     .add_graphical_diagonal_line(10, 10, 100, 100, 2, Color::Black, DiagonalOrientation::RightLeaning).unwrap()
521    ///     .build();
522    ///
523    /// assert!(zpl.contains("^GD100,100,2,B,R"));
524    /// ```
525    pub fn add_graphical_diagonal_line(
526        mut self,
527        x: u16,
528        y: u16,
529        box_width: u16,
530        box_height: u16,
531        thickness: u16,
532        color: Color,
533        orientation: DiagonalOrientation,
534    ) -> Result<Self, ZplError> {
535        let (x, y) = get_axis_position(x, y)?;
536        let thickness = Thickness::try_from(thickness)?;
537        let box_width = BoxDimension::try_new(box_width, thickness)?;
538        let box_height = BoxDimension::try_new(box_height, thickness)?;
539        let zpl_diagonal = GraphicalDiagonalLineElement::new(
540            x,
541            y,
542            box_width,
543            box_height,
544            thickness,
545            color,
546            orientation,
547        )
548        .to_zpl();
549        self.elements.push(zpl_diagonal);
550        Ok(self)
551    }
552
553    /// Assemble and return the complete ZPL string.
554    ///
555    /// The string starts with `^XA` and ends with `^XZ`. It can be sent
556    /// directly to a Zebra printer over TCP, USB, or serial.
557    ///
558    /// # Example
559    ///
560    /// ```rust
561    /// # use zpl_builder::LabelBuilder;
562    /// let zpl = LabelBuilder::new().build();
563    /// assert_eq!(zpl, "^XA\n^LH0,0\n^XZ");
564    /// ```
565    #[must_use]
566    pub fn build(&self) -> String {
567        let mut zpl = String::new();
568        zpl.push_str("^XA\n");
569        zpl.push_str(format!("^LH{},{}\n", self.x_origin, self.y_origin).as_str());
570        for element in &self.elements {
571            zpl.push_str(element);
572        }
573        zpl.push_str("^XZ");
574        zpl
575    }
576}
577
578impl Default for LabelBuilder {
579    fn default() -> Self {
580        Self::new()
581    }
582}