Skip to main content

bc_mur/
logo.rs

1use crate::{Error, Result};
2
3/// Shape used to clear the center area behind the logo.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum LogoClearShape {
6    Square,
7    Circle,
8}
9
10impl std::fmt::Display for LogoClearShape {
11    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
12        match self {
13            Self::Square => write!(f, "square"),
14            Self::Circle => write!(f, "circle"),
15        }
16    }
17}
18
19impl std::str::FromStr for LogoClearShape {
20    type Err = String;
21
22    fn from_str(s: &str) -> std::result::Result<Self, String> {
23        match s.to_ascii_lowercase().as_str() {
24            "square" => Ok(Self::Square),
25            "circle" => Ok(Self::Circle),
26            _ => Err(format!(
27                "unknown clear shape: {s} (expected square or circle)"
28            )),
29        }
30    }
31}
32
33/// A pre-rendered logo for compositing onto QR codes.
34#[derive(Clone)]
35pub struct Logo {
36    /// RGBA pixels, row-major.
37    pub pixels: Vec<u8>,
38    pub width: u32,
39    pub height: u32,
40    /// Fraction of QR width to occupy (0.01–0.99, default
41    /// 0.25).
42    pub fraction: f64,
43    /// Number of clear-border modules around the logo
44    /// (0–5, default 1).
45    pub clear_border: usize,
46    /// Shape of the cleared center area.
47    pub clear_shape: LogoClearShape,
48}
49
50impl Logo {
51    /// Create a logo from SVG data, rendered at 512×512 via
52    /// resvg (pure Rust, no system deps).
53    pub fn from_svg(
54        svg_data: &[u8],
55        fraction: f64,
56        clear_border: usize,
57        clear_shape: LogoClearShape,
58    ) -> Result<Self> {
59        let fraction = validate_fraction(fraction)?;
60        let clear_border = validate_clear_border(clear_border)?;
61
62        let tree = resvg::usvg::Tree::from_data(
63            svg_data,
64            &resvg::usvg::Options::default(),
65        )
66        .map_err(|e| Error::SvgRender(format!("SVG parse: {e}")))?;
67
68        let render_size = 512u32;
69        let mut pixmap =
70            resvg::tiny_skia::Pixmap::new(render_size, render_size)
71                .ok_or_else(|| {
72                    Error::SvgRender("failed to allocate pixmap".into())
73                })?;
74
75        let svg_size = tree.size();
76        let sx = render_size as f32 / svg_size.width();
77        let sy = render_size as f32 / svg_size.height();
78        let scale = sx.min(sy);
79        let tx = (render_size as f32 - svg_size.width() * scale) / 2.0;
80        let ty = (render_size as f32 - svg_size.height() * scale) / 2.0;
81        let transform = resvg::tiny_skia::Transform::from_scale(scale, scale)
82            .post_translate(tx, ty);
83
84        resvg::render(&tree, transform, &mut pixmap.as_mut());
85
86        // resvg outputs premultiplied RGBA — demultiply
87        let pixels = demultiply_alpha(pixmap.data());
88
89        Ok(Self {
90            pixels,
91            width: render_size,
92            height: render_size,
93            fraction,
94            clear_border,
95            clear_shape,
96        })
97    }
98
99    /// Create a logo from raw RGBA pixels.
100    pub fn from_rgba(
101        pixels: Vec<u8>,
102        width: u32,
103        height: u32,
104        fraction: f64,
105        clear_border: usize,
106        clear_shape: LogoClearShape,
107    ) -> Result<Self> {
108        let fraction = validate_fraction(fraction)?;
109        let clear_border = validate_clear_border(clear_border)?;
110        if pixels.len() != (width * height * 4) as usize {
111            return Err(Error::InvalidParameter(format!(
112                "pixel buffer size {} doesn't match {}x{}x4",
113                pixels.len(),
114                width,
115                height
116            )));
117        }
118        Ok(Self {
119            pixels,
120            width,
121            height,
122            fraction,
123            clear_border,
124            clear_shape,
125        })
126    }
127
128    /// Create a logo from PNG or JPEG image bytes.
129    pub fn from_image_bytes(
130        data: &[u8],
131        fraction: f64,
132        clear_border: usize,
133        clear_shape: LogoClearShape,
134    ) -> Result<Self> {
135        let fraction = validate_fraction(fraction)?;
136        let clear_border = validate_clear_border(clear_border)?;
137
138        let img = image::load_from_memory(data)
139            .map_err(|e| {
140                Error::ImageEncode(format!("failed to decode image: {e}"))
141            })?
142            .into_rgba8();
143
144        let width = img.width();
145        let height = img.height();
146        let pixels = img.into_raw();
147
148        Ok(Self {
149            pixels,
150            width,
151            height,
152            fraction,
153            clear_border,
154            clear_shape,
155        })
156    }
157}
158
159fn validate_fraction(f: f64) -> Result<f64> {
160    if !(0.01..=0.99).contains(&f) {
161        return Err(Error::InvalidParameter(format!(
162            "logo fraction must be 0.01–0.99, got {f}"
163        )));
164    }
165    Ok(f)
166}
167
168fn validate_clear_border(b: usize) -> Result<usize> {
169    if b > 5 {
170        return Err(Error::InvalidParameter(format!(
171            "clear_border must be 0–5, got {b}"
172        )));
173    }
174    Ok(b)
175}
176
177/// Convert premultiplied RGBA to straight RGBA.
178fn demultiply_alpha(data: &[u8]) -> Vec<u8> {
179    let mut out = vec![0u8; data.len()];
180    for i in (0..data.len()).step_by(4) {
181        let a = data[i + 3] as u16;
182        if a == 0 {
183            // Fully transparent
184            out[i] = 0;
185            out[i + 1] = 0;
186            out[i + 2] = 0;
187            out[i + 3] = 0;
188        } else if a == 255 {
189            out[i..i + 4].copy_from_slice(&data[i..i + 4]);
190        } else {
191            out[i] = ((data[i] as u16 * 255 + a / 2) / a) as u8;
192            out[i + 1] = ((data[i + 1] as u16 * 255 + a / 2) / a) as u8;
193            out[i + 2] = ((data[i + 2] as u16 * 255 + a / 2) / a) as u8;
194            out[i + 3] = a as u8;
195        }
196    }
197    out
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn fraction_validation() {
206        assert!(validate_fraction(0.25).is_ok());
207        assert!(validate_fraction(0.0).is_err());
208        assert!(validate_fraction(1.0).is_err());
209    }
210
211    #[test]
212    fn clear_border_validation() {
213        assert!(validate_clear_border(0).is_ok());
214        assert!(validate_clear_border(5).is_ok());
215        assert!(validate_clear_border(6).is_err());
216    }
217
218    #[test]
219    fn demultiply_identity() {
220        let data = vec![255, 128, 0, 255]; // fully opaque
221        let out = demultiply_alpha(&data);
222        assert_eq!(out, data);
223    }
224
225    #[test]
226    fn demultiply_transparent() {
227        let data = vec![0, 0, 0, 0];
228        let out = demultiply_alpha(&data);
229        assert_eq!(out, vec![0, 0, 0, 0]);
230    }
231}