Skip to main content

smart_package_tracker/
barcode.rs

1//! The high-level barcode facade.
2
3use crate::error::Result;
4use crate::render::{RenderOptions, Renderer};
5use crate::symbology::{Symbol, Symbology, SymbologyKind};
6
7/// An encoded barcode, ready to render.
8///
9/// This is the type most callers work with. It pairs an encoded [`Symbol`]
10/// with convenience methods for the built-in renderers, while
11/// [`Barcode::render`] stays open to any [`Renderer`] implementation.
12///
13/// # Examples
14///
15/// ```no_run
16/// # #[cfg(all(feature = "os-rng", feature = "code128", feature = "png", feature = "svg"))]
17/// # fn main() -> Result<(), smart_package_tracker::Error> {
18/// use smart_package_tracker::{Barcode, RenderOptions, TrackingId};
19///
20/// let id = TrackingId::generate()?;
21/// let barcode = Barcode::code128(&id)?;
22/// let options = RenderOptions::default();
23///
24/// barcode.to_png_file("label.png", &options)?;
25/// barcode.to_svg_file("label.svg", &options)?;
26/// # Ok(())
27/// # }
28/// # #[cfg(not(all(feature = "os-rng", feature = "code128", feature = "png", feature = "svg")))]
29/// # fn main() {}
30/// ```
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct Barcode {
33    symbol: Symbol,
34}
35
36impl Barcode {
37    /// Encode `data` as Code 128.
38    ///
39    /// Accepts anything that borrows as a string, including [`TrackingId`].
40    ///
41    /// [`TrackingId`]: crate::TrackingId
42    ///
43    /// # Errors
44    ///
45    /// Returns [`Error::EmptyPayload`](crate::Error::EmptyPayload) or
46    /// [`Error::Unencodable`](crate::Error::Unencodable).
47    #[cfg(feature = "code128")]
48    pub fn code128(data: impl AsRef<str>) -> Result<Self> {
49        Self::encode_with(&crate::symbology::Code128, data.as_ref())
50    }
51
52    /// Encode `data` as a QR Code with default settings: medium error
53    /// correction, and the smallest version that fits.
54    ///
55    /// Use [`Barcode::qr_with`] to choose the error correction level or pin the
56    /// version.
57    ///
58    /// # Errors
59    ///
60    /// Returns [`Error::EmptyPayload`](crate::Error::EmptyPayload) or
61    /// [`Error::Unencodable`](crate::Error::Unencodable).
62    #[cfg(feature = "qr")]
63    pub fn qr(data: impl AsRef<str>) -> Result<Self> {
64        Self::encode_with(&crate::symbology::Qr::new(), data.as_ref())
65    }
66
67    /// Encode `data` as a QR Code with an explicitly configured encoder.
68    ///
69    /// # Examples
70    ///
71    /// ```
72    /// use smart_package_tracker::{Barcode, symbology::{Ecc, Qr, QrVersion}};
73    ///
74    /// let barcode = Barcode::qr_with(
75    ///     Qr::new().ecc(Ecc::High).version(QrVersion::Fixed(6)),
76    ///     "PKG-9ED9285C",
77    /// )?;
78    /// assert_eq!(barcode.symbol().modules().width(), 41);
79    /// # Ok::<(), smart_package_tracker::Error>(())
80    /// ```
81    ///
82    /// # Errors
83    ///
84    /// Returns [`Error::EmptyPayload`](crate::Error::EmptyPayload) or
85    /// [`Error::Unencodable`](crate::Error::Unencodable).
86    #[cfg(feature = "qr")]
87    pub fn qr_with(encoder: crate::symbology::Qr, data: impl AsRef<str>) -> Result<Self> {
88        Self::encode_with(&encoder, data.as_ref())
89    }
90
91    /// Encode `data` with any symbology.
92    ///
93    /// # Errors
94    ///
95    /// Propagates whatever the symbology reports.
96    pub fn encode_with<S: Symbology + ?Sized>(symbology: &S, data: &str) -> Result<Self> {
97        Ok(Self {
98            symbol: symbology.encode(data)?,
99        })
100    }
101
102    /// Wrap an already-encoded symbol.
103    pub fn from_symbol(symbol: Symbol) -> Self {
104        Self { symbol }
105    }
106
107    /// The underlying symbol.
108    pub fn symbol(&self) -> &Symbol {
109        &self.symbol
110    }
111
112    /// The payload this barcode encodes.
113    pub fn payload(&self) -> &str {
114        self.symbol.payload()
115    }
116
117    /// Which symbology encoded it.
118    pub fn kind(&self) -> SymbologyKind {
119        self.symbol.kind()
120    }
121
122    /// Render with an explicit renderer.
123    ///
124    /// # Errors
125    ///
126    /// Propagates renderer failures.
127    pub fn render<R: Renderer>(&self, renderer: &R, options: &RenderOptions) -> Result<R::Output> {
128        renderer.render(&self.symbol, options)
129    }
130
131    /// Render to PNG bytes.
132    ///
133    /// # Errors
134    ///
135    /// Propagates renderer failures.
136    #[cfg(feature = "png")]
137    pub fn to_png(&self, options: &RenderOptions) -> Result<alloc::vec::Vec<u8>> {
138        self.render(&crate::render::Png, options)
139    }
140
141    /// Render to an SVG document.
142    ///
143    /// # Errors
144    ///
145    /// Propagates renderer failures.
146    #[cfg(feature = "svg")]
147    pub fn to_svg(&self, options: &RenderOptions) -> Result<alloc::string::String> {
148        self.render(&crate::render::Svg, options)
149    }
150
151    /// Render to PNG and write it to `path`.
152    ///
153    /// # Errors
154    ///
155    /// Propagates renderer failures and [`Error::Io`](crate::Error::Io).
156    #[cfg(all(feature = "png", feature = "std"))]
157    pub fn to_png_file(
158        &self,
159        path: impl AsRef<std::path::Path>,
160        options: &RenderOptions,
161    ) -> Result<()> {
162        std::fs::write(path, self.to_png(options)?)?;
163        Ok(())
164    }
165
166    /// Render to SVG and write it to `path`.
167    ///
168    /// # Errors
169    ///
170    /// Propagates renderer failures and [`Error::Io`](crate::Error::Io).
171    #[cfg(all(feature = "svg", feature = "std"))]
172    pub fn to_svg_file(
173        &self,
174        path: impl AsRef<std::path::Path>,
175        options: &RenderOptions,
176    ) -> Result<()> {
177        std::fs::write(path, self.to_svg(options)?)?;
178        Ok(())
179    }
180
181    /// Decode the barcode back into its payload from the module grid.
182    ///
183    /// This does not simply return the stored payload: it reconstructs bar and
184    /// space runs from the rendered module pattern and decodes those. A
185    /// successful round trip is therefore evidence that the encoded geometry
186    /// is correct.
187    ///
188    /// # Errors
189    ///
190    /// Returns [`Error::Decode`](crate::Error::Decode) if this is not a
191    /// Code 128 barcode, or if the pattern is not valid Code 128. There is no
192    /// QR decoder in this crate; see the `scan` roadmap item.
193    #[cfg(feature = "code128")]
194    pub fn decode(&self) -> Result<alloc::string::String> {
195        crate::symbology::code128::decode(&self.symbol)
196    }
197}
198
199#[cfg(all(test, feature = "code128"))]
200mod tests {
201    use super::*;
202    use crate::TrackingId;
203
204    #[test]
205    fn encodes_a_tracking_id_by_reference_or_value() {
206        let id = TrackingId::parse("PKG-9ED9285C").unwrap();
207        let a = Barcode::code128(&id).unwrap();
208        let b = Barcode::code128("PKG-9ED9285C").unwrap();
209        let c = Barcode::code128(id.as_str()).unwrap();
210        assert_eq!(a, b);
211        assert_eq!(b, c);
212        assert_eq!(a.payload(), "PKG-9ED9285C");
213        assert_eq!(a.kind(), SymbologyKind::Code128);
214    }
215
216    #[test]
217    fn round_trips_through_the_module_grid() {
218        let barcode = Barcode::code128("PKG-9ED9285C").unwrap();
219        assert_eq!(barcode.decode().unwrap(), "PKG-9ED9285C");
220    }
221
222    #[test]
223    #[cfg(all(feature = "png", feature = "svg"))]
224    fn renders_both_formats_from_one_encode() {
225        let barcode = Barcode::code128("PKG-9ED9285C").unwrap();
226        let options = RenderOptions::default();
227        assert!(!barcode.to_png(&options).unwrap().is_empty());
228        assert!(barcode.to_svg(&options).unwrap().contains("<svg"));
229    }
230
231    #[test]
232    fn from_symbol_preserves_the_symbol() {
233        let symbol = crate::symbology::Code128.encode("PKG-9ED9285C").unwrap();
234        let barcode = Barcode::from_symbol(symbol.clone());
235        assert_eq!(barcode.symbol(), &symbol);
236    }
237
238    #[test]
239    fn rejects_an_empty_payload() {
240        assert!(Barcode::code128("").is_err());
241    }
242}