#![deny(dead_code)]
#![deny(missing_debug_implementations)]
#![deny(missing_docs)]
#![forbid(unsafe_code)]
#![warn(unreachable_pub)]
#![doc(
html_favicon_url = "https://kura.pro/qrc/favicon.ico",
html_logo_url = "https://cloudcdn.pro/qrc/v1/logos/qrc.svg",
html_root_url = "https://docs.rs/qrc"
)]
#![crate_name = "qrc"]
use image::{DynamicImage, ImageBuffer, ImageFormat, Rgba, RgbaImage};
use miniz_oxide::deflate::compress_to_vec_zlib;
use qrcode::{render::svg, Color, QrCode};
use std::collections::HashMap;
use std::fmt::Write as _;
use std::io::Cursor;
pub use qrcode::types::EcLevel;
pub use qrcode::types::QrError;
pub mod macros;
pub mod payload;
mod art;
pub use art::BlendOptions;
#[cfg(feature = "wasm")]
pub mod wasm;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum ModuleShape {
#[default]
Square,
RoundedSquare,
Circle,
Diamond,
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct QRCode {
pub data: Vec<u8>,
encoding_format: String,
pub ec_level: EcLevel,
pub shape: ModuleShape,
}
impl Default for QRCode {
fn default() -> Self {
Self {
data: Vec::new(),
encoding_format: "utf-8".to_string(),
ec_level: EcLevel::M,
shape: ModuleShape::Square,
}
}
}
impl QRCode {
#[must_use]
pub fn new(data: Vec<u8>) -> Self {
Self {
data,
encoding_format: "utf-8".to_string(),
ec_level: EcLevel::M,
shape: ModuleShape::Square,
}
}
#[must_use]
pub fn from_string(data: String) -> Self {
Self {
data: data.into_bytes(),
encoding_format: "utf-8".to_string(),
ec_level: EcLevel::M,
shape: ModuleShape::Square,
}
}
#[must_use]
pub fn from_bytes(data: Vec<u8>) -> Self {
Self {
data,
encoding_format: "utf-8".to_string(),
ec_level: EcLevel::M,
shape: ModuleShape::Square,
}
}
#[must_use]
pub fn with_ec_level(mut self, ec_level: EcLevel) -> Self {
self.ec_level = ec_level;
self
}
#[must_use]
pub fn with_shape(mut self, shape: ModuleShape) -> Self {
self.shape = shape;
self
}
pub fn try_to_qrcode(&self) -> Result<QrCode, QrError> {
QrCode::with_error_correction_level(&self.data, self.ec_level)
}
#[must_use]
pub fn to_qrcode(&self) -> QrCode {
self.try_to_qrcode().expect("Failed to encode QR code")
}
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
fn render_image(&self, width: u32) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
let qrcode = self.to_qrcode();
let height = width;
let qr_width = qrcode.width() as f64;
let module_size = f64::from(width) / qr_width;
let mut img: RgbaImage = ImageBuffer::from_pixel(width, height, Rgba([255, 255, 255, 255]));
for (x, y, pixel) in img.enumerate_pixels_mut() {
let x_index = (f64::from(x) / f64::from(width) * qr_width) as usize;
let y_index = (f64::from(y) / f64::from(height) * qr_width) as usize;
if qrcode[(x_index, y_index)] == Color::Dark {
let mod_x = f64::from(x) - (x_index as f64) * module_size;
let mod_y = f64::from(y) - (y_index as f64) * module_size;
if self.is_inside_shape(mod_x, mod_y, module_size) {
*pixel = Rgba([0, 0, 0, 255]);
}
}
}
img
}
#[allow(clippy::cast_precision_loss)]
fn is_inside_shape(&self, mod_x: f64, mod_y: f64, module_size: f64) -> bool {
match self.shape {
ModuleShape::Square => true,
ModuleShape::RoundedSquare => {
let radius = module_size * 0.3;
is_inside_rounded_rect(mod_x, mod_y, module_size, module_size, radius)
}
ModuleShape::Circle => {
let half = module_size / 2.0;
let dx = mod_x - half;
let dy = mod_y - half;
dx * dx + dy * dy <= half * half
}
ModuleShape::Diamond => {
let half = module_size / 2.0;
(mod_x - half).abs() + (mod_y - half).abs() <= half
}
}
}
#[must_use]
pub fn to_png(&self, width: u32) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
self.render_image(width)
}
fn encode_bytes(&self, width: u32, format: ImageFormat) -> Result<Vec<u8>, image::ImageError> {
let img = self.render_image(width);
let mut buf = Cursor::new(Vec::new());
DynamicImage::ImageRgba8(img).write_to(&mut buf, format)?;
Ok(buf.into_inner())
}
pub fn to_png_bytes(&self, width: u32) -> Result<Vec<u8>, image::ImageError> {
self.encode_bytes(width, ImageFormat::Png)
}
pub fn to_gif(&self, width: u32) -> Result<Vec<u8>, image::ImageError> {
self.encode_bytes(width, ImageFormat::Gif)
}
pub fn to_jpg(&self, width: u32) -> Result<Vec<u8>, image::ImageError> {
self.to_jpg_with_quality(width, 85)
}
pub fn to_jpg_with_quality(
&self,
width: u32,
quality: u8,
) -> Result<Vec<u8>, image::ImageError> {
let img = self.render_image(width);
let rgb = DynamicImage::ImageRgba8(img).to_rgb8();
let mut buf = Cursor::new(Vec::new());
image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, quality).encode_image(&rgb)?;
Ok(buf.into_inner())
}
#[must_use]
pub fn to_image(&self, width: u32) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
self.render_image(width)
}
#[must_use]
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
pub fn to_svg(&self, width: u32) -> String {
let qrcode = self.to_qrcode();
if self.shape == ModuleShape::Square {
return qrcode
.render::<svg::Color>()
.min_dimensions(width, width)
.dark_color(svg::Color("#000000"))
.light_color(svg::Color("#FFFFFF"))
.build();
}
let qr_dim = qrcode.width();
let module_size = f64::from(width) / qr_dim as f64;
let mut elements = String::new();
for y in 0..qr_dim {
for x in 0..qr_dim {
if qrcode[(x, y)] == Color::Dark {
let px = x as f64 * module_size;
let py = y as f64 * module_size;
match self.shape {
ModuleShape::Circle => {
let cx = px + module_size / 2.0;
let cy = py + module_size / 2.0;
let r = module_size / 2.0;
let _ = write!(
elements,
"<circle cx=\"{cx}\" cy=\"{cy}\" r=\"{r}\" fill=\"#000000\"/>"
);
}
ModuleShape::Diamond => {
let half = module_size / 2.0;
let top_x = px + half;
let top_y = py;
let right_x = px + module_size;
let right_y = py + half;
let bot_x = px + half;
let bot_y = py + module_size;
let left_x = px;
let left_y = py + half;
let _ = write!(elements,
"<polygon points=\"{top_x},{top_y} {right_x},{right_y} {bot_x},{bot_y} {left_x},{left_y}\" fill=\"#000000\"/>"
);
}
_ => {
let r = module_size * 0.3;
let _ = write!(elements,
"<rect x=\"{px}\" y=\"{py}\" width=\"{module_size}\" height=\"{module_size}\" rx=\"{r}\" ry=\"{r}\" fill=\"#000000\"/>"
);
}
}
}
}
}
format!(
"<?xml version=\"1.0\" standalone=\"yes\"?><svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"{width}\" height=\"{width}\"><rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>{elements}</svg>"
)
}
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
pub fn colorize(&self, color: Rgba<u8>) -> RgbaImage {
let qrcode = self.to_qrcode();
let qr_dim = qrcode.width() as u32;
let module_size = 1.0; let mut img: RgbaImage =
ImageBuffer::from_pixel(qr_dim, qr_dim, Rgba([255, 255, 255, 255]));
for (x, y, pixel) in img.enumerate_pixels_mut() {
if qrcode[(x as usize, y as usize)] == Color::Dark {
let mod_x = f64::from(x) - f64::from(x) * module_size / module_size;
let mod_y = f64::from(y) - f64::from(y) * module_size / module_size;
if self.is_inside_shape(mod_x, mod_y, module_size) {
*pixel = color;
}
}
}
img
}
#[must_use]
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
pub fn resize(&self, width: u32, height: u32) -> RgbaImage {
let qrcode = self.to_qrcode();
let qr_width = qrcode.width() as f64;
let module_size_x = f64::from(width) / qr_width;
let module_size_y = f64::from(height) / qr_width;
let mut img: RgbaImage = ImageBuffer::from_pixel(width, height, Rgba([255, 255, 255, 255]));
for y in 0..height {
for x in 0..width {
let x_index = (f64::from(x) / f64::from(width) * qr_width) as usize;
let y_index = (f64::from(y) / f64::from(height) * qr_width) as usize;
if qrcode[(x_index, y_index)] == Color::Dark {
let mod_x = f64::from(x) - (x_index as f64) * module_size_x;
let mod_y = f64::from(y) - (y_index as f64) * module_size_y;
let avg_mod = (module_size_x + module_size_y) / 2.0;
if self.is_inside_shape(mod_x, mod_y, avg_mod) {
img.put_pixel(x, y, Rgba([0, 0, 0, 255]));
}
}
}
}
img
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn add_image_watermark(img: &mut RgbaImage, watermark: &RgbaImage) {
let (width, height) = img.dimensions();
let (watermark_width, watermark_height) = watermark.dimensions();
let x_offset = width - watermark_width;
let y_offset = height - watermark_height;
for (dx, dy, watermark_pixel) in watermark.enumerate_pixels() {
let px = x_offset + dx;
let py = y_offset + dy;
let qr_pixel = img.get_pixel(px, py);
let alpha = f32::from(watermark_pixel[3]) / 255.0;
let new_r = alpha.mul_add(
f32::from(watermark_pixel[0]),
(1.0 - alpha) * f32::from(qr_pixel[0]),
);
let new_g = alpha.mul_add(
f32::from(watermark_pixel[1]),
(1.0 - alpha) * f32::from(qr_pixel[1]),
);
let new_b = alpha.mul_add(
f32::from(watermark_pixel[2]),
(1.0 - alpha) * f32::from(qr_pixel[2]),
);
let new_a = alpha.mul_add(255.0 - f32::from(qr_pixel[3]), f32::from(qr_pixel[3]));
img.put_pixel(
px,
py,
Rgba([new_r as u8, new_g as u8, new_b as u8, new_a as u8]),
);
}
}
#[must_use]
pub fn create_multilanguage(data_map: &HashMap<String, String>, language: &str) -> Self {
let selected_data = data_map
.get(language)
.or_else(|| data_map.get("en"))
.or_else(|| data_map.values().next())
.map_or("", String::as_str);
Self::from_string(selected_data.to_string())
}
#[must_use]
pub fn create_dynamic(initial_data: &str) -> Self {
let dynamic_url = format!("https://your-api-endpoint.com/update?qrcode={initial_data}");
Self::from_string(dynamic_url)
}
#[allow(clippy::cast_possible_truncation)]
pub fn combine_qr_codes(codes: &[Self]) -> Result<Self, &'static str> {
if codes.is_empty() {
return Err("No QR codes to combine");
}
let total_width: u32 = codes
.iter()
.map(|code| code.to_qrcode().width() as u32)
.sum();
let mut combined_image: RgbaImage =
ImageBuffer::from_pixel(total_width, total_width, Rgba([255, 255, 255, 255]));
let mut x_offset: u32 = 0;
for code in codes {
let qrcode = code.to_qrcode();
let width = qrcode.width() as u32;
for x in 0..width {
for y in 0..width {
let pixel = qrcode[(x as usize, y as usize)];
let combined_x = x + x_offset;
if pixel == Color::Dark {
combined_image.put_pixel(combined_x, y, Rgba([0, 0, 0, 255]));
}
}
}
x_offset += width;
}
let mut combined_qrcode = Self::from_bytes(Vec::new());
combined_qrcode.data = combined_image.into_raw();
Ok(combined_qrcode)
}
#[must_use]
pub fn compress_data(data: &str) -> Vec<u8> {
compress_to_vec_zlib(data.as_bytes(), 6)
}
#[must_use]
pub fn batch_generate_qr_codes(data: Vec<String>) -> Vec<Self> {
data.into_iter().map(Self::from_string).collect()
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn overlay_image(&self, overlay: &RgbaImage) -> RgbaImage {
const MODULE_PX: u32 = 10;
const QUIET: u32 = 4;
let qrcode = self.to_qrcode();
let n = qrcode.width() as u32;
let dim = (n + 2 * QUIET) * MODULE_PX;
let mut combined_image: RgbaImage =
ImageBuffer::from_pixel(dim, dim, Rgba([255, 255, 255, 255]));
for y in 0..qrcode.width() {
for x in 0..qrcode.width() {
if qrcode[(x, y)] == Color::Dark {
let px0 = (x as u32 + QUIET) * MODULE_PX;
let py0 = (y as u32 + QUIET) * MODULE_PX;
for dy in 0..MODULE_PX {
for dx in 0..MODULE_PX {
combined_image.put_pixel(px0 + dx, py0 + dy, Rgba([0, 0, 0, 255]));
}
}
}
}
}
let (ow, oh) = overlay.dimensions();
let ox = dim.saturating_sub(ow) / 2;
let oy = dim.saturating_sub(oh) / 2;
for (x, y, pixel) in overlay.enumerate_pixels() {
if pixel[3] == 0 {
continue;
}
let (px, py) = (ox + x, oy + y);
if px < dim && py < dim {
combined_image.put_pixel(px, py, *pixel);
}
}
combined_image
}
#[must_use]
pub fn to_control_image(&self, size: u32) -> RgbaImage {
art::control_image(&self.to_qrcode(), size)
}
#[must_use]
pub fn blend_image(&self, background: &RgbaImage, opts: &BlendOptions) -> RgbaImage {
art::blend(&self.to_qrcode(), background, opts)
}
pub fn set_encoding_format(&self, format: &str) -> Result<Self, &'static str> {
if format != "utf-8" {
return Err("Unsupported encoding format");
}
Ok(Self {
data: self.data.clone(),
encoding_format: format.to_string(),
ec_level: self.ec_level,
shape: self.shape,
})
}
#[must_use]
pub fn get_encoding_format(&self) -> &str {
&self.encoding_format
}
}
#[allow(clippy::many_single_char_names)]
fn is_inside_rounded_rect(x: f64, y: f64, w: f64, h: f64, radius: f64) -> bool {
let r = radius.min(w / 2.0).min(h / 2.0);
if x >= r && x <= w - r {
return true;
}
if y >= r && y <= h - r {
return true;
}
let corners = [(r, r), (w - r, r), (r, h - r), (w - r, h - r)];
for (cx, cy) in &corners {
let dx = x - cx;
let dy = y - cy;
if dx * dx + dy * dy <= r * r {
return true;
}
}
false
}