use image::{imageops, ImageBuffer, Rgba, RgbaImage};
use qrcode::{Color, QrCode};
const QUIET: u32 = 4;
const BLACK: [u8; 4] = [0, 0, 0, 255];
const WHITE: [u8; 4] = [255, 255, 255, 255];
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BlendOptions {
pub module_size: u32,
pub strength: f32,
pub dot_ratio: f32,
}
impl Default for BlendOptions {
fn default() -> Self {
BlendOptions {
module_size: 12,
strength: 0.75,
dot_ratio: 0.66,
}
}
}
pub(crate) fn control_image(code: &QrCode, size: u32) -> RgbaImage {
let n = code.width() as u32;
let total = n + 2 * QUIET;
let module_px = (size / total).max(1);
let qr_dim = module_px * total;
let dim = size.max(qr_dim);
let offset = (dim - qr_dim) / 2;
let mut img: RgbaImage = ImageBuffer::from_pixel(dim, dim, Rgba(WHITE));
for y in 0..n {
for x in 0..n {
if code[(x as usize, y as usize)] != Color::Dark {
continue;
}
let px0 = offset + (x + QUIET) * module_px;
let py0 = offset + (y + QUIET) * module_px;
for dy in 0..module_px {
for dx in 0..module_px {
img.put_pixel(px0 + dx, py0 + dy, Rgba(BLACK));
}
}
}
}
img
}
pub(crate) fn blend(code: &QrCode, background: &RgbaImage, opts: &BlendOptions) -> RgbaImage {
let n = code.width() as u32;
let total = n + 2 * QUIET;
let m = opts.module_size.max(1);
let dim = total * m;
let bg: RgbaImage = if background.width() == 0 || background.height() == 0 {
ImageBuffer::from_pixel(dim, dim, Rgba(WHITE))
} else {
imageops::resize(background, dim, dim, imageops::FilterType::Lanczos3)
};
let mut out: RgbaImage = ImageBuffer::new(dim, dim);
let center = (m as f32 - 1.0) / 2.0;
let dot_r = m as f32 * opts.dot_ratio.clamp(0.0, 1.0) / 2.0;
for my in 0..total {
for mx in 0..total {
let is_quiet = mx < QUIET || my < QUIET || mx >= QUIET + n || my >= QUIET + n;
let (dx_mod, dy_mod) = (mx.wrapping_sub(QUIET), my.wrapping_sub(QUIET));
let dark_module = !is_quiet && code[(dx_mod as usize, dy_mod as usize)] == Color::Dark;
let finder = !is_quiet && in_finder(dx_mod as usize, dy_mod as usize, n as usize);
let tint = if dark_module { BLACK } else { WHITE };
for dy in 0..m {
for dx in 0..m {
let (px, py) = (mx * m + dx, my * m + dy);
let pixel = if is_quiet || finder {
Rgba(tint)
} else {
let ddx = dx as f32 - center;
let ddy = dy as f32 - center;
if (ddx * ddx + ddy * ddy).sqrt() <= dot_r {
Rgba(tint) } else {
mix(*bg.get_pixel(px, py), tint, opts.strength)
}
};
out.put_pixel(px, py, pixel);
}
}
}
}
out
}
fn in_finder(x: usize, y: usize, size: usize) -> bool {
const F: usize = 7;
let (left, right) = (x < F, x >= size - F);
let (top, bottom) = (y < F, y >= size - F);
(top && (left || right)) || (bottom && left)
}
fn mix(bg: Rgba<u8>, tint: [u8; 4], strength: f32) -> Rgba<u8> {
let s = strength.clamp(0.0, 1.0);
let ch = |b: u8, t: u8| ((1.0 - s) * f32::from(b) + s * f32::from(t)) as u8;
Rgba([
ch(bg[0], tint[0]),
ch(bg[1], tint[1]),
ch(bg[2], tint[2]),
255,
])
}