pub mod error;
use std::io::Cursor;
use error::Error;
use fast_qr::convert::{image::ImageBuilder, Builder, Shape};
use image::{imageops, ImageFormat};
use image::{io::Reader as ImageReader, GenericImageView};
use image::{DynamicImage, Rgba};
pub const DEFAULT_SIZE: u32 = 600;
pub const SIZE_MIN: u32 = 200;
pub const SIZE_MAX: u32 = 1000;
const BLACK: [u8; 4] = [0, 0, 0, 255];
const WHITE: [u8; 4] = [255, 255, 255, 255];
const TRANSPARENT: [u8; 4] = [255, 255, 255, 0];
#[derive(Debug, Clone, Copy)]
pub struct Rgb(pub [u8; 3]);
impl From<Rgb> for Rgba<u8> {
fn from(val: Rgb) -> Self {
let tmp = val.0;
let rgba = [tmp[0], tmp[1], tmp[2], 255];
Self(rgba)
}
}
#[derive(Debug)]
pub struct QrCodeBuilder<'a, 'b> {
content: &'a str,
size: Option<u32>,
bg_color: Option<Rgb>,
logo: &'b [u8],
logo_bg_color: Option<Rgb>,
}
impl<'a, 'b> QrCodeBuilder<'a, 'b> {
pub const fn new(content: &'a str, logo: &'b [u8]) -> QrCodeBuilder<'a, 'b> {
Self {
content,
size: None,
bg_color: None,
logo,
logo_bg_color: None,
}
}
pub fn with_size(&mut self, size: u32) -> &mut Self {
self.size = Some(size);
self
}
pub fn with_bg_color(&mut self, bg_color: Rgb) -> &mut Self {
self.bg_color = Some(bg_color);
self
}
pub fn with_logo_bg_color(&mut self, logo_bg_color: Rgb) -> &mut Self {
self.logo_bg_color = Some(logo_bg_color);
self
}
pub fn build(&self) -> Result<Vec<u8>, Error> {
let content = self.content;
let size = self.size.unwrap_or(DEFAULT_SIZE);
let bg_color = self.bg_color.map_or(Rgba(WHITE), Rgba::from);
let logo = self.logo;
let logo_bg_color = self.logo_bg_color.map_or(Rgba(WHITE), Rgba::from);
generate_qr_code(content, size, bg_color, logo, logo_bg_color)
}
}
fn generate_qr_code(
content: &str,
size: u32,
bg_color: Rgba<u8>,
logo: &[u8],
logo_bg_color: Rgba<u8>,
) -> Result<Vec<u8>, Error> {
if !(SIZE_MIN..=SIZE_MAX).contains(&size) {
return Err(Error::InputError(format!(
"Size should be between {SIZE_MIN} and {SIZE_MAX}."
)));
}
let mut qrcode = fast_qr::QRBuilder::new(content.to_owned());
let qrcode = match content.len() {
1..=35 => qrcode.ecl(fast_qr::ECL::H),
36.. => qrcode.ecl(fast_qr::ECL::Q),
_ => {
return Err(Error::InputError(format!(
"Invalid content length {}",
content.len()
)))
}
};
let qrcode = qrcode.build()?;
let img = ImageBuilder::default()
.shape(Shape::Square)
.fit_width(size)
.to_pixmap(&qrcode)
.encode_png()?;
let logo = ImageReader::new(Cursor::new(logo))
.with_guessed_format()
.map_err(|_e| Error::InputError("Image should be either PNG or JPEG".to_owned()))?
.decode()?;
let mut img = ImageReader::new(std::io::Cursor::new(&img));
img.set_format(ImageFormat::Png);
let mut img = img.decode()?;
if let Some(tmp) = img.as_mut_rgba8() {
tmp.enumerate_pixels_mut().for_each(|(_x, _y, p)| {
if p.0 > BLACK {
*p = Rgba(WHITE);
}
if p.0 == WHITE {
*p = bg_color;
}
});
}
add_logo(&mut img, &logo, logo_bg_color);
let mut bytes: Vec<u8> = Vec::new();
img.write_to(
&mut std::io::Cursor::new(&mut bytes),
image::ImageOutputFormat::Png,
)?;
Ok(bytes)
}
fn add_logo(img: &mut DynamicImage, logo: &DynamicImage, logo_bg_color: Rgba<u8>) {
let logo = logo.resize(
img.width() / 4,
img.width() / 4,
imageops::FilterType::Nearest,
);
let img_center = img.width() / 2;
let logo_center = logo.width() / 2;
let logo = image::ImageBuffer::from_fn(logo.width(), logo.height(), |x, y| {
if distance(logo_center, x, y) < (f64::from(logo_center)) {
logo.get_pixel(x, y)
} else {
Rgba(TRANSPARENT)
}
});
let logo_bg = image::ImageBuffer::from_fn(img.width(), img.width(), |x, y| {
if distance(img_center, x, y) < (f64::from(img_center) / 3.7) {
logo_bg_color
} else {
Rgba(TRANSPARENT)
}
});
let x = img_center - (logo.width() / 2);
let y = img_center - (logo.height() / 2);
imageops::overlay(img, &logo_bg, 0, 0);
imageops::overlay(img, &logo, x.into(), y.into());
}
fn distance(c: u32, x: u32, y: u32) -> f64 {
#[allow(clippy::cast_possible_wrap)]
f64::from((c as i32 - x as i32).pow(2) + (c as i32 - y as i32).pow(2)).sqrt()
}