Skip to main content

zpl_forge/forge/
mod.rs

1//! # Forge Layer
2//!
3//! The forge layer provides concrete implementations of rendering backends.
4//! It translates the intermediate representation (`ZplInstruction`) into
5//! specific output formats like images or documents.
6
7#[cfg(feature = "pdf")]
8pub mod pdf_native;
9#[cfg(feature = "png")]
10pub mod png;
11
12/// Maps a generic 1-D symbology to its `rxing` barcode format.
13#[cfg(any(feature = "png", feature = "pdf"))]
14pub(crate) fn barcode_1d_format(kind: crate::engine::Barcode1DKind) -> rxing::BarcodeFormat {
15    match kind {
16        crate::engine::Barcode1DKind::Ean13 => rxing::BarcodeFormat::EAN_13,
17        crate::engine::Barcode1DKind::UpcA => rxing::BarcodeFormat::UPC_A,
18        crate::engine::Barcode1DKind::Interleaved2of5 => rxing::BarcodeFormat::ITF,
19        crate::engine::Barcode1DKind::Code93 => rxing::BarcodeFormat::CODE_93,
20    }
21}
22
23/// Process-wide, bounded cache of encoded barcode bit matrices.
24///
25/// Encoding is pure (same format + data + hints → same matrix), so results
26/// are shared across renders. This is the hot path when one template is
27/// rendered thousands of times with different variables but static barcodes.
28#[cfg(any(feature = "png", feature = "pdf"))]
29pub(crate) mod barcode_cache {
30    use std::collections::HashMap;
31    use std::sync::{Arc, Mutex, OnceLock};
32
33    use rxing::common::BitMatrix;
34    use rxing::{BarcodeFormat, EncodeHints, MultiFormatWriter, Writer};
35
36    use crate::{ZplError, ZplResult};
37
38    /// Bound after which the cache is flushed wholesale. Each entry is a few
39    /// KB at most, so the worst case stays in the low single-digit MB.
40    const MAX_ENTRIES: usize = 512;
41
42    type Key = (&'static str, String, String);
43
44    fn cache() -> &'static Mutex<HashMap<Key, Arc<BitMatrix>>> {
45        static CACHE: OnceLock<Mutex<HashMap<Key, Arc<BitMatrix>>>> = OnceLock::new();
46        CACHE.get_or_init(|| Mutex::new(HashMap::new()))
47    }
48
49    fn format_key(format: &BarcodeFormat) -> &'static str {
50        match format {
51            BarcodeFormat::CODE_128 => "c128",
52            BarcodeFormat::CODE_39 => "c39",
53            BarcodeFormat::CODE_93 => "c93",
54            BarcodeFormat::QR_CODE => "qr",
55            BarcodeFormat::DATA_MATRIX => "dm",
56            BarcodeFormat::PDF_417 => "p417",
57            BarcodeFormat::EAN_13 => "e13",
58            BarcodeFormat::UPC_A => "upca",
59            BarcodeFormat::ITF => "itf",
60            _ => "other",
61        }
62    }
63
64    /// Encodes `data` in the given format, reusing a cached matrix when the
65    /// same (format, data, hints) triple was encoded before. `hints_key` must
66    /// uniquely fingerprint the contents of `hints`.
67    #[allow(clippy::collapsible_if)]
68    pub fn encode_cached(
69        format: BarcodeFormat,
70        data: &str,
71        hints_key: &str,
72        hints: Option<&EncodeHints>,
73    ) -> ZplResult<Arc<BitMatrix>> {
74        let key: Key = (format_key(&format), data.to_string(), hints_key.to_string());
75
76        if let Ok(guard) = cache().lock() {
77            if let Some(hit) = guard.get(&key) {
78                return Ok(hit.clone());
79            }
80        }
81
82        let writer = MultiFormatWriter;
83        let matrix = match hints {
84            Some(h) => writer.encode_with_hints(data, &format, 0, 0, h),
85            None => writer.encode(data, &format, 0, 0),
86        }
87        .map_err(|e| ZplError::BackendError(format!("Barcode Generation Error: {}", e)))?;
88
89        let matrix = Arc::new(matrix);
90        if let Ok(mut guard) = cache().lock() {
91            if guard.len() >= MAX_ENTRIES {
92                guard.clear();
93            }
94            guard.insert(key, matrix.clone());
95        }
96        Ok(matrix)
97    }
98}