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}