Skip to main content

zpl_builder/
types.rs

1use std::fmt::Display;
2use thiserror::Error;
3
4/// Errors that can occur when building a ZPL label.
5///
6/// Every method on the builder validates its arguments before adding an
7/// element to the label. When a value falls outside the range accepted by the
8/// ZPL specification, the method returns the corresponding variant of this
9/// error instead of silently producing an invalid command string.
10///
11/// # Example
12///
13/// ```rust
14/// use zpl_builder::{LabelBuilder, Orientation, ZplError};
15///
16/// let result = LabelBuilder::new()
17///     .add_text("Hello", 0, 0, '0', 15, Orientation::Normal); // '0' is not a valid font
18///
19/// assert_eq!(result, Err(ZplError::InvalidFont('0')));
20/// ```
21#[derive(Error, Debug, PartialEq, Copy, Clone)]
22pub enum ZplError {
23    /// A position value (x or y axis) is outside the range `0–32000`.
24    ///
25    /// ZPL positions are expressed in dots. The maximum addressable coordinate
26    /// on either axis is 32000 dots.
27    ///
28    /// The inner value is the rejected position.
29    ///
30    /// # Example
31    ///
32    /// ```rust
33    /// use zpl_builder::{LabelBuilder, ZplError};
34    ///
35    /// assert_eq!(
36    ///     LabelBuilder::new().set_home_position(32001, 0),
37    ///     Err(ZplError::InvalidPosition(32001))
38    /// );
39    /// ```
40    #[error("Position {0} out of range (0-32000)")]
41    InvalidPosition(u16),
42
43    /// A font size is outside the range `10–32000`.
44    ///
45    /// Font sizes are expressed in dots. Values below 10 are not accepted by
46    /// the ZPL specification.
47    ///
48    /// The inner value is the rejected font size.
49    ///
50    /// # Example
51    ///
52    /// ```rust
53    /// use zpl_builder::{LabelBuilder, Orientation, ZplError};
54    ///
55    /// assert_eq!(
56    ///     LabelBuilder::new().add_text("x", 0, 0, 'A', 9, Orientation::Normal),
57    ///     Err(ZplError::InvalidFontSize(9))
58    /// );
59    /// ```
60    #[error("Font size {0} out of range (10-32000)")]
61    InvalidFontSize(u16),
62
63    /// A barcode bar width is outside the range `1–10`.
64    ///
65    /// The bar width controls the width of the narrowest bar in the barcode,
66    /// expressed in dots.
67    ///
68    /// The inner value is the rejected width.
69    ///
70    /// # Example
71    ///
72    /// ```rust
73    /// use zpl_builder::{BarcodeType, LabelBuilder, Orientation, ZplError};
74    ///
75    /// assert_eq!(
76    ///     LabelBuilder::new().add_barcode(BarcodeType::Code128, "x", 0, 0, 11, 2.5, 10, Orientation::Normal),
77    ///     Err(ZplError::InvalidBarcodeWidth(11))
78    /// );
79    /// ```
80    #[error("Barcode width {0} out of range (1-10)")]
81    InvalidBarcodeWidth(u8),
82
83    /// A barcode wide-to-narrow bar width ratio is outside the range `2.0–3.0`.
84    ///
85    /// The ratio must be a multiple of `0.1`. It has no effect on fixed-ratio
86    /// barcode types.
87    ///
88    /// The inner value is the rejected ratio as originally supplied by the caller.
89    ///
90    /// # Example
91    ///
92    /// ```rust
93    /// use zpl_builder::{BarcodeType, LabelBuilder, Orientation, ZplError};
94    ///
95    /// assert_eq!(
96    ///     LabelBuilder::new().add_barcode(BarcodeType::Code39, "x", 0, 0, 2, 1.5, 10, Orientation::Normal),
97    ///     Err(ZplError::InvalidWidthRatio(1.5))
98    /// );
99    /// ```
100    #[error("Width ratio {0} out of range (2.0-3.0)")]
101    InvalidWidthRatio(f32),
102
103    /// A barcode height is outside the range `1–32000`.
104    ///
105    /// The height is expressed in dots.
106    ///
107    /// The inner value is the rejected height.
108    ///
109    /// # Example
110    ///
111    /// ```rust
112    /// use zpl_builder::{BarcodeType, LabelBuilder, Orientation, ZplError};
113    ///
114    /// assert_eq!(
115    ///     LabelBuilder::new().add_barcode(BarcodeType::Code128, "x", 0, 0, 2, 2.5, 0, Orientation::Normal),
116    ///     Err(ZplError::InvalidBarcodeHeight(0))
117    /// );
118    /// ```
119    #[error("Barcode height {0} out of range (1-32000)")]
120    InvalidBarcodeHeight(u16),
121
122    /// A line thickness is outside the range `1–32000`.
123    ///
124    /// The thickness is expressed in dots and applies to boxes, circles and
125    /// ellipses.
126    ///
127    /// The inner value is the rejected thickness.
128    ///
129    /// # Example
130    ///
131    /// ```rust
132    /// use zpl_builder::{Color, LabelBuilder, ZplError};
133    ///
134    /// assert_eq!(
135    ///     LabelBuilder::new().add_graphical_box(0, 0, 100, 100, 0, Color::Black, 0),
136    ///     Err(ZplError::InvalidThickness(0))
137    /// );
138    /// ```
139    #[error("Thickness {0} out of range (1-32000)")]
140    InvalidThickness(u16),
141
142    /// A graphical dimension (diameter, width or height of a circle or ellipse)
143    /// is outside the range `3–4095`.
144    ///
145    /// The dimension is expressed in dots.
146    ///
147    /// The inner value is the rejected dimension.
148    ///
149    /// # Example
150    ///
151    /// ```rust
152    /// use zpl_builder::{Color, LabelBuilder, ZplError};
153    ///
154    /// assert_eq!(
155    ///     LabelBuilder::new().add_graphical_circle(0, 0, 2, 1, Color::Black),
156    ///     Err(ZplError::InvalidGraphicDimension(2))
157    /// );
158    /// ```
159    #[error("Graphic dimension {0} out of range (3-4095)")]
160    InvalidGraphicDimension(u16),
161
162    /// A corner rounding value is outside the range `0–8`.
163    ///
164    /// `0` produces sharp corners. `8` produces the most rounded corners.
165    ///
166    /// The inner value is the rejected rounding.
167    ///
168    /// # Example
169    ///
170    /// ```rust
171    /// use zpl_builder::{Color, LabelBuilder, ZplError};
172    ///
173    /// assert_eq!(
174    ///     LabelBuilder::new().add_graphical_box(0, 0, 100, 100, 1, Color::Black, 9),
175    ///     Err(ZplError::InvalidRounding(9))
176    /// );
177    /// ```
178    #[error("Rounding {0} out of range (0-8)")]
179    InvalidRounding(u8),
180
181    /// A font identifier is not a valid ZPL font name.
182    ///
183    /// Valid font names are a single ASCII uppercase letter (`A`–`Z`) or a
184    /// digit from `1` to `9`. The digit `0` (zero) is not a valid font name.
185    ///
186    /// The inner value is the rejected character.
187    ///
188    /// # Example
189    ///
190    /// ```rust
191    /// use zpl_builder::{LabelBuilder, Orientation, ZplError};
192    ///
193    /// assert_eq!(
194    ///     LabelBuilder::new().add_text("x", 0, 0, '0', 10, Orientation::Normal),
195    ///     Err(ZplError::InvalidFont('0'))
196    /// );
197    /// ```
198    #[error("Invalid font '{0}' (expected A-Z or 1-9)")]
199    InvalidFont(char),
200
201    /// A box width or height is smaller than the box thickness.
202    ///
203    /// ZPL requires that the width and height of a graphical box each be at
204    /// least equal to its line thickness, otherwise the box cannot be rendered.
205    ///
206    /// # Fields
207    ///
208    /// * `value` — the rejected width or height, in dots.
209    /// * `thickness` — the thickness the dimension was compared against, in dots.
210    ///
211    /// # Example
212    ///
213    /// ```rust
214    /// use zpl_builder::{Color, LabelBuilder, ZplError};
215    ///
216    /// // thickness = 5, width = 2 → width < thickness
217    /// assert_eq!(
218    ///     LabelBuilder::new().add_graphical_box(0, 0, 2, 100, 5, Color::Black, 0),
219    ///     Err(ZplError::InvalidBoxDimension { value: 2, thickness: 5 })
220    /// );
221    /// ```
222    #[error(
223        "Box dimension {value} is smaller than thickness {thickness} (must be {thickness}-32000)"
224    )]
225    InvalidBoxDimension {
226        /// The rejected width or height, in dots.
227        value: u16,
228        /// The line thickness the dimension was compared against, in dots.
229        thickness: u16,
230    },
231}
232
233// ---- Macro to construct types ----
234macro_rules! bounded_u16 {
235    ($name: ident, $min:expr, $max:expr, $err:ident) => {
236        #[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
237        pub(crate) struct $name(pub(crate) u16);
238
239        impl TryFrom<u16> for $name {
240            type Error = ZplError;
241            fn try_from(v: u16) -> Result<Self, Self::Error> {
242                if ($min..=$max).contains(&v) {
243                    Ok(Self(v))
244                } else {
245                    Err(ZplError::$err(v))
246                }
247            }
248        }
249
250        impl Display for $name {
251            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252                write!(f, "{}", self.0)
253            }
254        }
255    };
256}
257
258macro_rules! bounded_u8 {
259    ($name: ident, $min:expr, $max:expr, $err:ident) => {
260        #[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
261        pub(crate) struct $name(pub(crate) u8);
262
263        impl TryFrom<u8> for $name {
264            type Error = ZplError;
265            fn try_from(v: u8) -> Result<Self, Self::Error> {
266                if ($min..=$max).contains(&v) {
267                    Ok(Self(v))
268                } else {
269                    Err(ZplError::$err(v))
270                }
271            }
272        }
273
274        impl Display for $name {
275            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276                write!(f, "{}", self.0)
277            }
278        }
279    };
280}
281
282// ---- Types ----
283
284bounded_u16!(AxisPosition, 0, 32000, InvalidPosition);
285bounded_u16!(FontSize, 10, 32000, InvalidFontSize);
286bounded_u16!(BarcodeHeight, 1, 32000, InvalidBarcodeHeight);
287bounded_u16!(Thickness, 1, 32000, InvalidThickness);
288bounded_u16!(GraphicDimension, 3, 4095, InvalidGraphicDimension);
289
290bounded_u8!(BarcodeWidth, 1, 10, InvalidBarcodeWidth);
291bounded_u8!(Rounding, 0, 8, InvalidRounding);
292
293#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
294pub(crate) struct WidthRatio(pub(crate) u8);
295
296impl TryFrom<f32> for WidthRatio {
297    type Error = ZplError;
298
299    fn try_from(value: f32) -> Result<Self, Self::Error> {
300        let tenths = (value * 10.0).round() as i32;
301        if (20..=30).contains(&tenths) {
302            Ok(Self(tenths as u8))
303        } else {
304            Err(ZplError::InvalidWidthRatio(value))
305        }
306    }
307}
308
309impl Display for WidthRatio {
310    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311        write!(f, "{:.1}", self.0 as f32 / 10.0)
312    }
313}
314
315#[derive(Debug, PartialEq, Clone)]
316pub(crate) struct Font(pub(crate) char);
317
318impl TryFrom<char> for Font {
319    type Error = ZplError;
320
321    fn try_from(c: char) -> Result<Self, Self::Error> {
322        if c.is_ascii_uppercase() || ('1'..='9').contains(&c) {
323            Ok(Self(c))
324        } else {
325            Err(ZplError::InvalidFont(c))
326        }
327    }
328}
329
330impl Display for Font {
331    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332        write!(f, "{}", self.0)
333    }
334}
335
336#[derive(Debug, PartialEq, Clone, Copy)]
337pub(crate) struct BoxDimension(pub(crate) u16);
338
339impl BoxDimension {
340    pub(crate) fn try_new(value: u16, thickness: Thickness) -> Result<Self, ZplError> {
341        if (thickness.0..=32000).contains(&value) {
342            Ok(Self(value))
343        } else {
344            Err(ZplError::InvalidBoxDimension {
345                value,
346                thickness: thickness.0,
347            })
348        }
349    }
350}
351
352impl Display for BoxDimension {
353    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
354        write!(f, "{}", self.0)
355    }
356}