use egui::{Context, Image, ImageSource, Vec2, load::Bytes};
use image::{ImageBuffer, ImageFormat, Luma};
use qrcodegen_no_heap::*;
use std::sync::Arc;
#[cfg(feature = "secure-types")]
use secure_types::Zeroize;
#[derive(Debug, Clone)]
pub enum QrError {
EncodingFailed(String),
ImageError(String),
Other(String),
}
impl QrError {
pub fn to_string(&self) -> String {
match self {
QrError::EncodingFailed(message) => format!("Encoding failed: {}", message),
QrError::ImageError(message) => format!("Image failed: {}", message),
QrError::Other(message) => message.to_string(),
}
}
}
impl std::fmt::Display for QrError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
QrError::EncodingFailed(message) => write!(f, "Encoding failed: {}", message),
QrError::ImageError(message) => write!(f, "Image failed: {}", message),
QrError::Other(message) => write!(f, "{}", message),
}
}
}
impl std::error::Error for QrError {}
#[cfg(feature = "secure-types")]
fn zeroize_image_data(data: &mut [u8]) -> bool {
data.zeroize();
true
}
#[cfg(not(feature = "secure-types"))]
fn zeroize_image_data(_data: &mut [u8]) -> bool {
false
}
const DEFAULT_TARGET_PX: u32 = 512;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum QrEcc {
Low,
Medium,
Quartile,
#[default]
High,
}
impl QrEcc {
const fn to_codegen(self) -> QrCodeEcc {
match self {
Self::Low => QrCodeEcc::Low,
Self::Medium => QrCodeEcc::Medium,
Self::Quartile => QrCodeEcc::Quartile,
Self::High => QrCodeEcc::High,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct QrEncoding {
pub ecc: QrEcc,
pub target_px: u32,
pub quiet_zone: u32,
}
impl Default for QrEncoding {
fn default() -> Self {
Self {
ecc: QrEcc::High,
target_px: DEFAULT_TARGET_PX,
quiet_zone: DEFAULT_QUIET_ZONE,
}
}
}
const DEFAULT_QUIET_ZONE: u32 = 4;
pub struct QrImage {
image_data: Arc<[u8]>,
uri: String,
modules: u32,
quiet_zone: u32,
module_px: u32,
error: Option<QrError>,
}
impl QrImage {
pub fn new(data: &str, uri: String) -> Self {
Self::with_encoding(data, uri, QrEncoding::default())
}
pub fn with_encoding(data: &str, uri: String, encoding: QrEncoding) -> Self {
let res = data_to_qr(data, encoding);
let (image_data, modules, module_px, error) = match res {
Ok((image_data, modules, module_px)) => (image_data, modules, module_px, None),
Err(e) => (Vec::new(), 0, 0, Some(e)),
};
Self {
image_data: image_data.into(),
uri,
modules,
quiet_zone: encoding.quiet_zone,
module_px,
error,
}
}
pub fn empty_with_error(error: String) -> Self {
Self {
image_data: Vec::new().into(),
uri: String::new(),
modules: 0,
quiet_zone: 0,
module_px: 0,
error: Some(QrError::Other(error)),
}
}
pub fn has_error(&self) -> bool {
self.error.is_some()
}
pub fn error(&self) -> Option<&QrError> {
self.error.as_ref()
}
pub fn is_cleared(&self) -> bool {
self.image_data.is_empty()
}
pub fn image(&self) -> Image<'static> {
let data = self.image_data.clone();
let image = Image::new(ImageSource::Bytes {
uri: self.uri.clone().into(),
bytes: Bytes::Shared(data),
});
image
}
pub fn image_size(&self) -> Vec2 {
Vec2::splat((self.raster_modules() * self.module_px) as f32)
}
pub fn image_size_pt(&self, pixels_per_point: f32) -> Vec2 {
self.image_size() / pixels_per_point.max(0.01)
}
pub fn modules(&self) -> u32 {
self.modules
}
pub fn module_px(&self) -> u32 {
self.module_px
}
pub fn quiet_zone(&self) -> u32 {
self.quiet_zone
}
pub fn raster_modules(&self) -> u32 {
if self.modules == 0 {
0
} else {
self.modules + self.quiet_zone * 2
}
}
pub fn min_display_px(&self, module_px: u32) -> u32 {
self.raster_modules() * module_px.max(1)
}
pub fn clear(&mut self, ctx: &Context) -> bool {
ctx.forget_image(&self.uri);
self.error = None;
self.modules = 0;
self.quiet_zone = 0;
self.module_px = 0;
let success = match Arc::get_mut(&mut self.image_data) {
Some(data) => {
let did_zeroize = zeroize_image_data(data);
self.image_data = Arc::new([0u8; 0]);
did_zeroize
}
None => false,
};
success
}
}
impl Drop for QrImage {
fn drop(&mut self) {
if let Some(data) = Arc::get_mut(&mut self.image_data) {
zeroize_image_data(data);
}
}
}
pub(crate) fn data_to_qr(data: &str, encoding: QrEncoding) -> Result<(Vec<u8>, u32, u32), QrError> {
let mut tempbuffer = vec![0u8; Version::MAX.buffer_len()];
let mut outbuffer = vec![0u8; Version::MAX.buffer_len()];
let qr = QrCode::encode_text(
data,
&mut tempbuffer,
&mut outbuffer,
encoding.ecc.to_codegen(),
Version::MIN,
Version::MAX,
None,
true,
)
.map_err(|e| QrError::EncodingFailed(e.to_string()))?;
let size = qr.size() as u32;
let side_modules = size + encoding.quiet_zone * 2;
let module_px = (encoding.target_px / side_modules).max(1);
let img_size = side_modules * module_px;
let mut img: ImageBuffer<Luma<u8>, Vec<u8>> = ImageBuffer::new(img_size, img_size);
for pixel in img.pixels_mut() {
*pixel = Luma([255u8]); }
let offset = encoding.quiet_zone * module_px;
for y in 0..size {
for x in 0..size {
if qr.get_module(x as i32, y as i32) {
let px = Luma([0u8]); for dy in 0..module_px {
for dx in 0..module_px {
*img.get_pixel_mut(
offset + x * module_px + dx,
offset + y * module_px + dy,
) = px;
}
}
}
}
}
let mut png_bytes = Vec::new();
img.write_to(
&mut std::io::Cursor::new(&mut png_bytes),
ImageFormat::Png,
)
.map_err(|e| QrError::ImageError(e.to_string()))?;
#[cfg(feature = "secure-types")]
{
let mut img_buf = img.into_vec();
img_buf.zeroize();
tempbuffer.zeroize();
outbuffer.zeroize();
}
Ok((png_bytes, size, module_px))
}
#[cfg(test)]
mod tests {
use super::*;
const DENSE_PAYLOAD: &str = concat!(
"0123456789abcdef0123456789abcdef",
"0123456789abcdef0123456789abcdef",
"0123456789abcdef0123456789abcdef",
"0123456789abcdef0123456789abcdef",
);
const ADDRESS: &str = "0x492e7Bc37cCBE1Afb1738ceE2aa880Fe7184C0d";
fn raster(png: &[u8]) -> image::GrayImage {
image::load_from_memory_with_format(png, ImageFormat::Png).unwrap().to_luma8()
}
#[test]
fn test_qr_image() {
let mut qr = QrImage::new("Hello, world!", "bytes://test".to_string());
{
let _image = qr.image();
}
assert!(!qr.is_cleared());
assert!(!qr.has_error());
assert!(qr.modules() > 0);
assert_eq!(
qr.image_size(),
Vec2::splat((qr.raster_modules() * qr.module_px()) as f32)
);
qr.clear(&Context::default());
assert!(qr.is_cleared());
assert_eq!(qr.modules(), 0);
assert_eq!(qr.raster_modules(), 0);
assert_eq!(qr.module_px(), 0);
assert_eq!(qr.image_size(), Vec2::ZERO);
}
#[test]
fn raster_is_whole_modules_within_the_budget() {
for target_px in [40, 120, 250, 512, 900] {
let encoding = QrEncoding {
target_px,
quiet_zone: 0,
..Default::default()
};
let qr = QrImage::with_encoding(
DENSE_PAYLOAD,
"bytes://dense".to_string(),
encoding,
);
assert!(!qr.has_error());
let (modules, module_px) = (qr.modules(), qr.module_px());
assert!(modules > 0, "target_px {target_px}");
assert!(module_px >= 1, "target_px {target_px}");
assert!(
qr.raster_modules() * module_px <= target_px || module_px == 1,
"target_px {target_px}"
);
assert_eq!(
qr.image_size(),
Vec2::splat((qr.raster_modules() * module_px) as f32)
);
let (png, size, mp) = data_to_qr(DENSE_PAYLOAD, encoding).unwrap();
assert_eq!((size, mp), (modules, module_px));
let img = image::load_from_memory_with_format(&png, ImageFormat::Png).unwrap().to_luma8();
assert_eq!(
img.dimensions(),
(modules * module_px, modules * module_px),
"quiet_zone 0: the raster is the symbol alone"
);
}
}
#[test]
fn lower_error_correction_uses_fewer_modules() {
let with = |ecc| {
QrImage::with_encoding(
DENSE_PAYLOAD,
format!("bytes://ecc-{ecc:?}"),
QrEncoding {
ecc,
..Default::default()
},
)
};
let low = with(QrEcc::Low);
let high = with(QrEcc::High);
assert!(low.modules() < high.modules());
assert!(low.min_display_px(4) < high.min_display_px(4));
assert_eq!(high.min_display_px(4), high.raster_modules() * 4);
}
#[test]
fn short_payloads_need_far_fewer_modules() {
let address = QrImage::new(ADDRESS, "bytes://address".to_string());
let digest = QrImage::new(DENSE_PAYLOAD, "bytes://digest".to_string());
assert!(digest.modules() >= address.modules() * 3 / 2);
assert!(digest.module_px() <= address.module_px() || digest.modules() > address.modules());
assert!(digest.min_display_px(4) > address.min_display_px(4));
}
#[test]
fn quiet_zone_adds_a_light_border() {
const QUIET_ZONE: u32 = 4;
let encoding = QrEncoding {
target_px: 250,
quiet_zone: QUIET_ZONE,
..Default::default()
};
let bordered = QrImage::with_encoding(DENSE_PAYLOAD, "bytes://qz".to_string(), encoding);
let plain = QrImage::with_encoding(
DENSE_PAYLOAD,
"bytes://plain".to_string(),
QrEncoding {
target_px: 250,
..Default::default()
},
);
assert_eq!(bordered.modules(), plain.modules());
assert!(bordered.module_px() <= plain.module_px());
let side_modules = bordered.modules() + QUIET_ZONE * 2;
let module_px = bordered.module_px();
let (png, _, _) = data_to_qr(DENSE_PAYLOAD, encoding).unwrap();
let img = raster(&png);
assert_eq!(
img.dimensions(),
(side_modules * module_px, side_modules * module_px)
);
assert!(bordered.image_size().x <= 250.0);
let side = side_modules * module_px;
for i in 0..side {
for b in 0..QUIET_ZONE * module_px {
assert_eq!(img.get_pixel(i, b)[0], 255);
assert_eq!(img.get_pixel(b, i)[0], 255);
}
}
}
#[test]
fn image_size_pt_follows_the_scale_factor() {
let qr = QrImage::new(ADDRESS, "bytes://pt".to_string());
assert_eq!(qr.image_size_pt(1.0), qr.image_size());
assert_eq!(qr.image_size_pt(2.0), qr.image_size() / 2.0);
}
}