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}