bwipp/options.rs
1//! Rendering options shared across symbologies.
2//!
3//! Each symbology accepts additional symbology-specific options; those go in
4//! [`Options::extras`] as `(key, value)` string pairs to mirror BWIPP's flag
5//! convention (`includecheck=true`, `eclevel=M`, etc.).
6
7/// Module-level rendering options.
8#[derive(Debug, Clone)]
9pub struct Options {
10 /// Pixel multiplier for raster output, or stroke unit for vector output.
11 /// Default: 4.
12 pub scale: u32,
13 /// Bar height in modules (only meaningful for 1D codes). Default: 50.
14 pub bar_height: u32,
15 /// Quiet zone in modules around the barcode. Default: 4.
16 pub quiet_zone: u32,
17 /// Whether to render human-readable text under 1D symbologies.
18 pub include_text: bool,
19 /// Foreground color as RGB. Default: black.
20 pub foreground: [u8; 3],
21 /// Background color as RGB. Default: white.
22 pub background: [u8; 3],
23 /// Symbology-specific options (e.g. `("eclevel", "M")`, `("type", "29")`).
24 pub extras: Vec<(String, String)>,
25}
26
27impl Default for Options {
28 fn default() -> Self {
29 Self {
30 scale: 4,
31 bar_height: 50,
32 quiet_zone: 4,
33 include_text: false,
34 foreground: [0, 0, 0],
35 background: [255, 255, 255],
36 extras: Vec::new(),
37 }
38 }
39}
40
41impl Options {
42 /// Look up an extra option by key (case-sensitive, like BWIPP).
43 ///
44 /// # Example
45 ///
46 /// ```
47 /// use bwipp::Options;
48 ///
49 /// let opts = Options::default().with("eclevel", "H");
50 /// assert_eq!(opts.get("eclevel"), Some("H"));
51 /// assert_eq!(opts.get("missing"), None);
52 /// ```
53 pub fn get(&self, key: &str) -> Option<&str> {
54 self.extras
55 .iter()
56 .find(|(k, _)| k == key)
57 .map(|(_, v)| v.as_str())
58 }
59
60 /// Add an option `key=value`. Designed to be chained as a builder.
61 ///
62 /// # Example
63 ///
64 /// ```
65 /// use bwipp::{Options, Symbology, render_svg};
66 ///
67 /// let opts = Options::default()
68 /// .with("eclevel", "Q")
69 /// .with("version", "5");
70 /// let svg = render_svg(Symbology::QrCode, "Hello", &opts).unwrap();
71 /// assert!(svg.starts_with("<svg"));
72 /// ```
73 #[must_use]
74 pub fn with(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
75 self.extras.push((key.into(), value.into()));
76 self
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 /// Stage 11.A8c — pin `Options::default()` field values. Kills
85 /// any mutation that changes a default literal (e.g. scale=4 →
86 /// scale=0, quiet_zone=4 → quiet_zone=0).
87 #[test]
88 fn default_field_values() {
89 let opts = Options::default();
90 assert_eq!(opts.scale, 4);
91 assert_eq!(opts.bar_height, 50);
92 assert_eq!(opts.quiet_zone, 4);
93 assert!(!opts.include_text);
94 assert_eq!(opts.foreground, [0, 0, 0]);
95 assert_eq!(opts.background, [255, 255, 255]);
96 assert!(opts.extras.is_empty());
97 }
98
99 /// Stage 11.A8c — pin `Options::get` lookup semantics: returns
100 /// Some for known keys, None for unknown, case-sensitive (BWIPP
101 /// behavior). Kills `find` predicate mutations.
102 #[test]
103 fn get_lookup_semantics() {
104 let opts = Options::default().with("eclevel", "H").with("version", "5");
105 assert_eq!(opts.get("eclevel"), Some("H"));
106 assert_eq!(opts.get("version"), Some("5"));
107 assert_eq!(opts.get("missing"), None);
108 // Case-sensitive.
109 assert_eq!(opts.get("ECLEVEL"), None);
110 assert_eq!(opts.get("Eclevel"), None);
111 // Empty key.
112 assert_eq!(opts.get(""), None);
113 }
114
115 /// Stage 11.A8c — pin `Options::with` builder behavior. Kills
116 /// `push` order or value-swap mutations.
117 #[test]
118 fn with_builder_appends_and_returns() {
119 let opts = Options::default().with("a", "1").with("b", "2");
120 assert_eq!(opts.extras.len(), 2);
121 // Order preserved: first append at index 0, second at index 1.
122 assert_eq!(opts.extras[0], ("a".to_string(), "1".to_string()));
123 assert_eq!(opts.extras[1], ("b".to_string(), "2".to_string()));
124 // Subsequent .with for the same key appends a duplicate (BWIPP
125 // matches by key from front, so later wins for `get`? Let's
126 // verify both behaviors are pinned).
127 let opts = Options::default().with("k", "first").with("k", "second");
128 assert_eq!(opts.extras.len(), 2);
129 // get returns the FIRST match.
130 assert_eq!(opts.get("k"), Some("first"));
131 }
132}