Skip to main content

bc_mur/
render.rs

1use image::{
2    ImageEncoder,
3    codecs::{jpeg::JpegEncoder, png::PngEncoder},
4};
5
6use crate::{Color, CorrectionLevel, Error, Logo, Result, qr_matrix::QrMatrix};
7
8/// An RGBA pixel buffer with encoding methods.
9pub struct RenderedImage {
10    /// RGBA pixels, row-major, 4 bytes per pixel.
11    pub pixels: Vec<u8>,
12    pub width: u32,
13    pub height: u32,
14}
15
16impl RenderedImage {
17    /// Encode as PNG.
18    pub fn to_png(&self) -> Result<Vec<u8>> {
19        let mut buf = Vec::new();
20        PngEncoder::new(&mut buf)
21            .write_image(
22                &self.pixels,
23                self.width,
24                self.height,
25                image::ExtendedColorType::Rgba8,
26            )
27            .map_err(|e| Error::ImageEncode(e.to_string()))?;
28        Ok(buf)
29    }
30
31    /// Encode as JPEG at the given quality (1–100).
32    pub fn to_jpeg(&self, quality: u8) -> Result<Vec<u8>> {
33        // JPEG doesn't support alpha — convert to RGB
34        let rgb: Vec<u8> = self
35            .pixels
36            .chunks_exact(4)
37            .flat_map(|px| [px[0], px[1], px[2]])
38            .collect();
39        let mut buf = Vec::new();
40        JpegEncoder::new_with_quality(&mut buf, quality)
41            .write_image(
42                &rgb,
43                self.width,
44                self.height,
45                image::ExtendedColorType::Rgb8,
46            )
47            .map_err(|e| Error::ImageEncode(e.to_string()))?;
48        Ok(buf)
49    }
50}
51
52/// Render a single-frame QR code from raw bytes.
53///
54/// - `message`: bytes to encode in the QR code
55/// - `correction`: error correction level
56/// - `size`: target image size in pixels (square)
57/// - `fg` / `bg`: foreground and background colors
58/// - `quiet_zone`: number of background-colored modules around the QR code
59///   (default 1)
60/// - `logo`: optional logo overlay
61pub fn render_qr(
62    message: &[u8],
63    correction: CorrectionLevel,
64    size: u32,
65    fg: Color,
66    bg: Color,
67    quiet_zone: u32,
68    logo: Option<&Logo>,
69) -> Result<RenderedImage> {
70    let matrix = QrMatrix::encode(message, correction)?;
71    render_from_matrix(&matrix, size, fg, bg, quiet_zone, logo)
72}
73
74/// Render a single-frame QR code from a UR string.
75///
76/// The UR string is automatically uppercased for QR
77/// alphanumeric mode efficiency.
78pub fn render_ur_qr(
79    ur_string: &str,
80    correction: CorrectionLevel,
81    size: u32,
82    fg: Color,
83    bg: Color,
84    quiet_zone: u32,
85    logo: Option<&Logo>,
86) -> Result<RenderedImage> {
87    let upper = ur_string.to_ascii_uppercase();
88    render_qr(upper.as_bytes(), correction, size, fg, bg, quiet_zone, logo)
89}
90
91/// Paint the QR matrix into a pixel buffer with module-aligned
92/// rendering, then composite the logo if present.
93pub(crate) fn render_from_matrix(
94    matrix: &QrMatrix,
95    size: u32,
96    fg: Color,
97    bg: Color,
98    quiet_zone: u32,
99    logo: Option<&Logo>,
100) -> Result<RenderedImage> {
101    let qr_modules = matrix.width();
102    let total_modules = qr_modules + 2 * quiet_zone as usize;
103    let pixels_per_module = (size as usize / total_modules).max(1);
104    let compositing_size = total_modules * pixels_per_module;
105    let qz_px = quiet_zone as usize * pixels_per_module;
106
107    // Allocate RGBA buffer, fill with background
108    let mut pixels = vec![0u8; compositing_size * compositing_size * 4];
109    // Fill entire buffer with background color
110    for px in pixels.chunks_exact_mut(4) {
111        px[0] = bg.r;
112        px[1] = bg.g;
113        px[2] = bg.b;
114        px[3] = bg.a;
115    }
116
117    // Paint QR modules offset by quiet zone
118    for row in 0..qr_modules {
119        for col in 0..qr_modules {
120            let color = if matrix.is_dark(col, row) { fg } else { bg };
121            let px = qz_px + col * pixels_per_module;
122            let py = qz_px + row * pixels_per_module;
123            fill_rect(
124                &mut pixels,
125                compositing_size,
126                px,
127                py,
128                pixels_per_module,
129                pixels_per_module,
130                color,
131            );
132        }
133    }
134
135    // Logo overlay (centered within the QR modules area)
136    if let Some(logo) = logo {
137        composite_logo(
138            &mut pixels,
139            compositing_size,
140            qr_modules,
141            pixels_per_module,
142            qz_px,
143            bg,
144            logo,
145        );
146    }
147
148    // Scale to final requested size if different
149    let pixels = if compositing_size as u32 != size {
150        nearest_neighbor_scale(
151            &pixels,
152            compositing_size as u32,
153            compositing_size as u32,
154            size,
155            size,
156        )
157    } else {
158        pixels
159    };
160
161    Ok(RenderedImage { pixels, width: size, height: size })
162}
163
164/// Fill a rectangle in the RGBA buffer.
165fn fill_rect(
166    pixels: &mut [u8],
167    stride: usize,
168    x: usize,
169    y: usize,
170    w: usize,
171    h: usize,
172    color: Color,
173) {
174    for row in y..y + h {
175        for col in x..x + w {
176            let offset = (row * stride + col) * 4;
177            pixels[offset] = color.r;
178            pixels[offset + 1] = color.g;
179            pixels[offset + 2] = color.b;
180            pixels[offset + 3] = color.a;
181        }
182    }
183}
184
185/// Composite the logo into the center of the QR code.
186///
187/// `qz_px` is the quiet-zone offset in pixels so the logo
188/// centers on the QR data area, not the entire image.
189fn composite_logo(
190    pixels: &mut [u8],
191    compositing_size: usize,
192    module_count: usize,
193    pixels_per_module: usize,
194    qz_px: usize,
195    bg: Color,
196    logo: &Logo,
197) {
198    let layout =
199        LogoLayout::new(module_count, logo.fraction, logo.clear_border);
200
201    if layout.logo_modules == 0 {
202        return;
203    }
204
205    // Clear color: use white if background is transparent
206    let clear_color = if bg.is_transparent() {
207        Color::WHITE
208    } else {
209        bg
210    };
211
212    let center_module = module_count as f64 / 2.0;
213    let qr_px = module_count * pixels_per_module;
214
215    // Clear the center area (offset by quiet zone)
216    let start_module = (module_count - layout.cleared_modules) / 2;
217    match logo.clear_shape {
218        crate::LogoClearShape::Square => {
219            let clear_pixels = layout.cleared_modules * pixels_per_module;
220            let clear_origin = qz_px + (qr_px - clear_pixels) / 2;
221            fill_rect(
222                pixels,
223                compositing_size,
224                clear_origin,
225                clear_origin,
226                clear_pixels,
227                clear_pixels,
228                clear_color,
229            );
230        }
231        crate::LogoClearShape::Circle => {
232            let radius = layout.cleared_modules as f64 / 2.0;
233            for row in 0..layout.cleared_modules {
234                for col in 0..layout.cleared_modules {
235                    let mx = (start_module + col) as f64 + 0.5;
236                    let my = (start_module + row) as f64 + 0.5;
237                    let dx = mx - center_module;
238                    let dy = my - center_module;
239                    if dx * dx + dy * dy <= radius * radius {
240                        let px =
241                            qz_px + (start_module + col) * pixels_per_module;
242                        let py =
243                            qz_px + (start_module + row) * pixels_per_module;
244                        fill_rect(
245                            pixels,
246                            compositing_size,
247                            px,
248                            py,
249                            pixels_per_module,
250                            pixels_per_module,
251                            clear_color,
252                        );
253                    }
254                }
255            }
256        }
257    }
258
259    // Draw the logo centered within the QR data area
260    let logo_pixels = layout.logo_modules * pixels_per_module;
261    let logo_origin = qz_px + (qr_px - logo_pixels) / 2;
262
263    // Scale logo to fit the logo area using bilinear
264    // interpolation
265    let scaled = bilinear_scale(
266        &logo.pixels,
267        logo.width,
268        logo.height,
269        logo_pixels as u32,
270        logo_pixels as u32,
271    );
272
273    // Alpha-composite the scaled logo onto the QR
274    for row in 0..logo_pixels {
275        for col in 0..logo_pixels {
276            let src_offset = (row * logo_pixels + col) * 4;
277            let dst_x = logo_origin + col;
278            let dst_y = logo_origin + row;
279            let dst_offset = (dst_y * compositing_size + dst_x) * 4;
280
281            let sa = scaled[src_offset + 3] as u32;
282            if sa == 0 {
283                continue;
284            }
285            if sa == 255 {
286                pixels[dst_offset] = scaled[src_offset];
287                pixels[dst_offset + 1] = scaled[src_offset + 1];
288                pixels[dst_offset + 2] = scaled[src_offset + 2];
289                pixels[dst_offset + 3] = 255;
290            } else {
291                let da = pixels[dst_offset + 3] as u32;
292                let inv_sa = 255 - sa;
293                let out_a = sa + da * inv_sa / 255;
294                if out_a > 0 {
295                    for c in 0..3 {
296                        let sc = scaled[src_offset + c] as u32;
297                        let dc = pixels[dst_offset + c] as u32;
298                        pixels[dst_offset + c] =
299                            ((sc * sa + dc * da * inv_sa / 255) / out_a) as u8;
300                    }
301                    pixels[dst_offset + 3] = out_a.min(255) as u8;
302                }
303            }
304        }
305    }
306}
307
308/// Logo layout calculation — mirrors Swift `LogoLayout` /
309/// Kotlin `LogoLayout`.
310struct LogoLayout {
311    logo_modules: usize,
312    cleared_modules: usize,
313}
314
315impl LogoLayout {
316    fn new(module_count: usize, fraction: f64, clear_border: usize) -> Self {
317        let mut logo = (module_count as f64 * fraction).round() as usize;
318        // Force odd for symmetry
319        if logo.is_multiple_of(2) {
320            logo += 1;
321        }
322        let mut cleared = logo + 2 * clear_border;
323        // Cap cleared area at 40% of module count
324        let max_cleared = (module_count as f64 * 0.40).floor() as usize;
325        if cleared > max_cleared {
326            cleared = max_cleared;
327            logo = cleared.saturating_sub(2 * clear_border);
328        }
329        // Re-ensure odd after capping
330        if logo.is_multiple_of(2) && logo > 0 {
331            logo -= 1;
332        }
333        Self { logo_modules: logo, cleared_modules: cleared }
334    }
335}
336
337/// Nearest-neighbor scale for crisp QR modules.
338fn nearest_neighbor_scale(
339    src: &[u8],
340    src_w: u32,
341    src_h: u32,
342    dst_w: u32,
343    dst_h: u32,
344) -> Vec<u8> {
345    let mut dst = vec![0u8; (dst_w * dst_h * 4) as usize];
346    for y in 0..dst_h {
347        let sy = (y * src_h / dst_h).min(src_h - 1);
348        for x in 0..dst_w {
349            let sx = (x * src_w / dst_w).min(src_w - 1);
350            let si = (sy * src_w + sx) as usize * 4;
351            let di = (y * dst_w + x) as usize * 4;
352            dst[di..di + 4].copy_from_slice(&src[si..si + 4]);
353        }
354    }
355    dst
356}
357
358/// Bilinear scale for smooth logo rendering.
359fn bilinear_scale(
360    src: &[u8],
361    src_w: u32,
362    src_h: u32,
363    dst_w: u32,
364    dst_h: u32,
365) -> Vec<u8> {
366    let mut dst = vec![0u8; (dst_w * dst_h * 4) as usize];
367    for y in 0..dst_h {
368        let fy = y as f64 * (src_h - 1) as f64 / (dst_h - 1).max(1) as f64;
369        let y0 = fy.floor() as u32;
370        let y1 = (y0 + 1).min(src_h - 1);
371        let wy = fy - y0 as f64;
372
373        for x in 0..dst_w {
374            let fx = x as f64 * (src_w - 1) as f64 / (dst_w - 1).max(1) as f64;
375            let x0 = fx.floor() as u32;
376            let x1 = (x0 + 1).min(src_w - 1);
377            let wx = fx - x0 as f64;
378
379            let i00 = (y0 * src_w + x0) as usize * 4;
380            let i10 = (y0 * src_w + x1) as usize * 4;
381            let i01 = (y1 * src_w + x0) as usize * 4;
382            let i11 = (y1 * src_w + x1) as usize * 4;
383
384            let di = (y * dst_w + x) as usize * 4;
385            for c in 0..4 {
386                let v = src[i00 + c] as f64 * (1.0 - wx) * (1.0 - wy)
387                    + src[i10 + c] as f64 * wx * (1.0 - wy)
388                    + src[i01 + c] as f64 * (1.0 - wx) * wy
389                    + src[i11 + c] as f64 * wx * wy;
390                dst[di + c] = v.round() as u8;
391            }
392        }
393    }
394    dst
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn logo_layout_basic() {
403        let l = LogoLayout::new(25, 0.25, 1);
404        // 25 * 0.25 = 6.25 → round to 6 → force odd → 7
405        assert_eq!(l.logo_modules, 7);
406        // 7 + 2*1 = 9
407        assert_eq!(l.cleared_modules, 9);
408    }
409
410    #[test]
411    fn logo_layout_cap_at_40_pct() {
412        // 21 * 0.40 = 8.4 → floor = 8
413        let l = LogoLayout::new(21, 0.5, 2);
414        // 21 * 0.5 = 10.5 → 11 (odd), cleared = 11+4=15 >
415        // 8 → capped
416        assert!(l.cleared_modules <= 8);
417    }
418
419    #[test]
420    fn render_basic_qr() {
421        let img = render_qr(
422            b"HELLO",
423            CorrectionLevel::Low,
424            256,
425            Color::BLACK,
426            Color::WHITE,
427            1,
428            None,
429        )
430        .unwrap();
431        assert_eq!(img.width, 256);
432        assert_eq!(img.height, 256);
433        assert_eq!(img.pixels.len(), 256 * 256 * 4);
434    }
435
436    #[test]
437    fn render_to_png() {
438        let img = render_qr(
439            b"TEST",
440            CorrectionLevel::Medium,
441            128,
442            Color::BLACK,
443            Color::WHITE,
444            1,
445            None,
446        )
447        .unwrap();
448        let png = img.to_png().unwrap();
449        // PNG magic bytes
450        assert_eq!(&png[..4], &[137, 80, 78, 71]);
451    }
452
453    #[test]
454    fn render_ur_qr_uppercases() {
455        let img = render_ur_qr(
456            "ur:bytes/hdcxdwinvezm",
457            CorrectionLevel::Low,
458            256,
459            Color::BLACK,
460            Color::WHITE,
461            1,
462            None,
463        )
464        .unwrap();
465        assert_eq!(img.width, 256);
466    }
467}