smart-package-tracker 0.2.0

Generate package tracking IDs and render them as Code 128 barcodes (PNG and SVG)
Documentation
//! The high-level barcode facade.

use crate::error::Result;
use crate::render::{RenderOptions, Renderer};
use crate::symbology::{Symbol, Symbology, SymbologyKind};

/// An encoded barcode, ready to render.
///
/// This is the type most callers work with. It pairs an encoded [`Symbol`]
/// with convenience methods for the built-in renderers, while
/// [`Barcode::render`] stays open to any [`Renderer`] implementation.
///
/// # Examples
///
/// ```no_run
/// # #[cfg(all(feature = "os-rng", feature = "code128", feature = "png", feature = "svg"))]
/// # fn main() -> Result<(), smart_package_tracker::Error> {
/// use smart_package_tracker::{Barcode, RenderOptions, TrackingId};
///
/// let id = TrackingId::generate()?;
/// let barcode = Barcode::code128(&id)?;
/// let options = RenderOptions::default();
///
/// barcode.to_png_file("label.png", &options)?;
/// barcode.to_svg_file("label.svg", &options)?;
/// # Ok(())
/// # }
/// # #[cfg(not(all(feature = "os-rng", feature = "code128", feature = "png", feature = "svg")))]
/// # fn main() {}
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Barcode {
    symbol: Symbol,
}

impl Barcode {
    /// Encode `data` as Code 128.
    ///
    /// Accepts anything that borrows as a string, including [`TrackingId`].
    ///
    /// [`TrackingId`]: crate::TrackingId
    ///
    /// # Errors
    ///
    /// Returns [`Error::EmptyPayload`](crate::Error::EmptyPayload) or
    /// [`Error::Unencodable`](crate::Error::Unencodable).
    #[cfg(feature = "code128")]
    pub fn code128(data: impl AsRef<str>) -> Result<Self> {
        Self::encode_with(&crate::symbology::Code128, data.as_ref())
    }

    /// Encode `data` as a QR Code with default settings: medium error
    /// correction, and the smallest version that fits.
    ///
    /// Use [`Barcode::qr_with`] to choose the error correction level or pin the
    /// version.
    ///
    /// # Errors
    ///
    /// Returns [`Error::EmptyPayload`](crate::Error::EmptyPayload) or
    /// [`Error::Unencodable`](crate::Error::Unencodable).
    #[cfg(feature = "qr")]
    pub fn qr(data: impl AsRef<str>) -> Result<Self> {
        Self::encode_with(&crate::symbology::Qr::new(), data.as_ref())
    }

    /// Encode `data` as a QR Code with an explicitly configured encoder.
    ///
    /// # Examples
    ///
    /// ```
    /// use smart_package_tracker::{Barcode, symbology::{Ecc, Qr, QrVersion}};
    ///
    /// let barcode = Barcode::qr_with(
    ///     Qr::new().ecc(Ecc::High).version(QrVersion::Fixed(6)),
    ///     "PKG-9ED9285C",
    /// )?;
    /// assert_eq!(barcode.symbol().modules().width(), 41);
    /// # Ok::<(), smart_package_tracker::Error>(())
    /// ```
    ///
    /// # Errors
    ///
    /// Returns [`Error::EmptyPayload`](crate::Error::EmptyPayload) or
    /// [`Error::Unencodable`](crate::Error::Unencodable).
    #[cfg(feature = "qr")]
    pub fn qr_with(encoder: crate::symbology::Qr, data: impl AsRef<str>) -> Result<Self> {
        Self::encode_with(&encoder, data.as_ref())
    }

    /// Encode `data` with any symbology.
    ///
    /// # Errors
    ///
    /// Propagates whatever the symbology reports.
    pub fn encode_with<S: Symbology + ?Sized>(symbology: &S, data: &str) -> Result<Self> {
        Ok(Self {
            symbol: symbology.encode(data)?,
        })
    }

    /// Wrap an already-encoded symbol.
    pub fn from_symbol(symbol: Symbol) -> Self {
        Self { symbol }
    }

    /// The underlying symbol.
    pub fn symbol(&self) -> &Symbol {
        &self.symbol
    }

    /// The payload this barcode encodes.
    pub fn payload(&self) -> &str {
        self.symbol.payload()
    }

    /// Which symbology encoded it.
    pub fn kind(&self) -> SymbologyKind {
        self.symbol.kind()
    }

    /// Render with an explicit renderer.
    ///
    /// # Errors
    ///
    /// Propagates renderer failures.
    pub fn render<R: Renderer>(&self, renderer: &R, options: &RenderOptions) -> Result<R::Output> {
        renderer.render(&self.symbol, options)
    }

    /// Render to PNG bytes.
    ///
    /// # Errors
    ///
    /// Propagates renderer failures.
    #[cfg(feature = "png")]
    pub fn to_png(&self, options: &RenderOptions) -> Result<alloc::vec::Vec<u8>> {
        self.render(&crate::render::Png, options)
    }

    /// Render to an SVG document.
    ///
    /// # Errors
    ///
    /// Propagates renderer failures.
    #[cfg(feature = "svg")]
    pub fn to_svg(&self, options: &RenderOptions) -> Result<alloc::string::String> {
        self.render(&crate::render::Svg, options)
    }

    /// Render to PNG and write it to `path`.
    ///
    /// # Errors
    ///
    /// Propagates renderer failures and [`Error::Io`](crate::Error::Io).
    #[cfg(all(feature = "png", feature = "std"))]
    pub fn to_png_file(
        &self,
        path: impl AsRef<std::path::Path>,
        options: &RenderOptions,
    ) -> Result<()> {
        std::fs::write(path, self.to_png(options)?)?;
        Ok(())
    }

    /// Render to SVG and write it to `path`.
    ///
    /// # Errors
    ///
    /// Propagates renderer failures and [`Error::Io`](crate::Error::Io).
    #[cfg(all(feature = "svg", feature = "std"))]
    pub fn to_svg_file(
        &self,
        path: impl AsRef<std::path::Path>,
        options: &RenderOptions,
    ) -> Result<()> {
        std::fs::write(path, self.to_svg(options)?)?;
        Ok(())
    }

    /// Decode the barcode back into its payload from the module grid.
    ///
    /// This does not simply return the stored payload: it reconstructs bar and
    /// space runs from the rendered module pattern and decodes those. A
    /// successful round trip is therefore evidence that the encoded geometry
    /// is correct.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Decode`](crate::Error::Decode) if this is not a
    /// Code 128 barcode, or if the pattern is not valid Code 128. There is no
    /// QR decoder in this crate; see the `scan` roadmap item.
    #[cfg(feature = "code128")]
    pub fn decode(&self) -> Result<alloc::string::String> {
        crate::symbology::code128::decode(&self.symbol)
    }
}

#[cfg(all(test, feature = "code128"))]
mod tests {
    use super::*;
    use crate::TrackingId;

    #[test]
    fn encodes_a_tracking_id_by_reference_or_value() {
        let id = TrackingId::parse("PKG-9ED9285C").unwrap();
        let a = Barcode::code128(&id).unwrap();
        let b = Barcode::code128("PKG-9ED9285C").unwrap();
        let c = Barcode::code128(id.as_str()).unwrap();
        assert_eq!(a, b);
        assert_eq!(b, c);
        assert_eq!(a.payload(), "PKG-9ED9285C");
        assert_eq!(a.kind(), SymbologyKind::Code128);
    }

    #[test]
    fn round_trips_through_the_module_grid() {
        let barcode = Barcode::code128("PKG-9ED9285C").unwrap();
        assert_eq!(barcode.decode().unwrap(), "PKG-9ED9285C");
    }

    #[test]
    #[cfg(all(feature = "png", feature = "svg"))]
    fn renders_both_formats_from_one_encode() {
        let barcode = Barcode::code128("PKG-9ED9285C").unwrap();
        let options = RenderOptions::default();
        assert!(!barcode.to_png(&options).unwrap().is_empty());
        assert!(barcode.to_svg(&options).unwrap().contains("<svg"));
    }

    #[test]
    fn from_symbol_preserves_the_symbol() {
        let symbol = crate::symbology::Code128.encode("PKG-9ED9285C").unwrap();
        let barcode = Barcode::from_symbol(symbol.clone());
        assert_eq!(barcode.symbol(), &symbol);
    }

    #[test]
    fn rejects_an_empty_payload() {
        assert!(Barcode::code128("").is_err());
    }
}