// Copyright 2023 The Grim Developers // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::mem::size_of; use std::sync::Arc; use parking_lot::RwLock; use std::thread; use egui::{SizeHint, TextureHandle}; use egui::epaint::RectShape; use image::{ExtendedColorType, ImageEncoder}; use image::codecs::png::{CompressionType, FilterType, PngEncoder}; use qrcodegen::QrCode; use crate::gui::Colors; use crate::gui::icons::IMAGES_SQUARE; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::QrImageState; use crate::gui::views::View; /// QR code image from text. pub struct QrCodeContent { /// QR code text. text: String, /// Flag to draw animated QR with Uniform Resources /// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md animated: bool, /// Index of current image at animation. animated_index: Option, /// Time of last image draw. animation_time: Option, /// Texture handle to show image when created. texture_handle: Option, /// QR code view data state. qr_image_state: Arc>, } const DEFAULT_QR_SIZE: u32 = 512; impl QrCodeContent { pub fn new(text: String, animated: bool) -> Self { Self { text, animated, animated_index: None, animation_time: None, texture_handle: None, qr_image_state: Arc::new(RwLock::new(QrImageState::default())), } } /// Draw QR code. pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { if self.animated { // Show animated QR code. self.animated_ui(ui, cb); } else { // Show static QR code. self.static_ui(ui, cb); } } /// Draw animated QR code content. fn animated_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { if !self.has_image() { let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; ui.vertical_centered(|ui| { ui.add_space(space); View::big_loading_spinner(ui); ui.add_space(space); }); // Create multiple vector images from text if not creating. if !self.loading() { self.create_svg_list(); } } else { let svg_list = { let r_create = self.qr_image_state.read(); r_create.svg_list.clone().unwrap() }; // Setup animated index. let now = chrono::Utc::now().timestamp_millis(); if now - *self.animation_time.get_or_insert(now) > 100 { if let Some(i) = self.animated_index { self.animated_index = Some(i + 1); } if *self.animated_index.get_or_insert(0) == svg_list.len() { self.animated_index = Some(0); } self.animation_time = Some(now); } let svg = svg_list[self.animated_index.unwrap_or(0)].clone(); // Create images from SVG data. self.qr_image_ui(svg, ui); // Show QR code text. ui.add_space(6.0); View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text()); ui.add_space(6.0); ui.vertical_centered(|ui| { let sharing = { let r_state = self.qr_image_state.read(); r_state.exporting || r_state.gif_creating }; if !sharing { // Show button to share QR. let share_text = format!("{} {}", IMAGES_SQUARE, t!("share")); View::colored_text_button(ui, share_text, Colors::blue(), Colors::white_or_black(false), || { { let mut w_state = self.qr_image_state.write(); w_state.exporting = true; } // Create GIF to export. self.create_qr_gif(); }); } else { ui.vertical_centered(|ui| { ui.add_space(2.0); View::small_loading_spinner(ui); ui.add_space(1.0); }); } ui.add_space(8.0); View::horizontal_line(ui, Colors::item_stroke()); ui.add_space(8.0); // Check if GIF was created to share. let has_gif = { let r_state = self.qr_image_state.read(); r_state.gif_data.is_some() }; if has_gif { let data = { let r_state = self.qr_image_state.read(); r_state.gif_data.clone().unwrap() }; let name = format!("{}.gif", chrono::Utc::now().timestamp()); cb.share_data(name, data).unwrap_or_default(); // Clear GIF data and exporting flag. { let mut w_state = self.qr_image_state.write(); w_state.gif_data = None; w_state.exporting = false; } } }); ui.ctx().request_repaint(); } } /// Draw static QR code content. fn static_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { if !self.has_image() { let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; ui.vertical_centered(|ui| { ui.add_space(space); View::big_loading_spinner(ui); ui.add_space(space); }); // Create vector image from text if not creating. if !self.loading() { self.create_svg(); } } else { // Create image from SVG data. let svg = { let r_state = self.qr_image_state.read(); r_state.svg.clone().unwrap() }; self.qr_image_ui(svg, ui); // Show QR code text. ui.add_space(6.0); View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text()); ui.add_space(6.0); // Show button to share QR. ui.vertical_centered(|ui| { let share_text = format!("{} {}", IMAGES_SQUARE, t!("share")); View::colored_text_button(ui, share_text, Colors::blue(), Colors::white_or_black(false), || { let text = self.text.as_str(); if let Ok(qr) = QrCode::encode_text(text, qrcodegen::QrCodeEcc::Low) { if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) { let mut png = vec![]; let png_enc = PngEncoder::new_with_quality(&mut png, CompressionType::Best, FilterType::NoFilter); if let Ok(()) = png_enc.write_image(data.as_slice(), DEFAULT_QR_SIZE, DEFAULT_QR_SIZE, ExtendedColorType::L8) { let name = format!("{}.png", chrono::Utc::now().timestamp()); cb.share_data(name, png).unwrap_or_default(); } } } }); }); ui.add_space(8.0); View::horizontal_line(ui, Colors::item_stroke()); ui.add_space(8.0); } } /// Draw QR code image content. fn qr_image_ui(&mut self, svg: Vec, ui: &mut egui::Ui) { let mut rect = ui.available_rect_before_wrap(); rect.min += egui::emath::vec2(10.0, 0.0); rect.max -= egui::emath::vec2(10.0, 0.0); // Create background shape. let mut bg_shape = RectShape { rect, rounding: egui::Rounding::default(), fill: egui::Color32::WHITE, stroke: egui::Stroke::NONE, blur_width: 0.0, fill_texture_id: Default::default(), uv: egui::Rect::ZERO }; let bg_idx = ui.painter().add(bg_shape); // Draw QR code image content. let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| { ui.add_space(10.0); let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32); self.texture_handle = Some(View::svg_image(ui, "qr_code", svg.as_slice(), Some(size))); ui.add_space(10.0); }).response.rect; // Setup background shape to be painted behind content. content_rect.min -= egui::emath::vec2(10.0, 0.0); content_rect.max += egui::emath::vec2(10.0, 0.0); bg_shape.rect = content_rect; ui.painter().set(bg_idx, bg_shape); } /// Check if QR code is loading. fn loading(&self) -> bool { let r_state = self.qr_image_state.read(); r_state.loading } /// Create multiple vector QR code images at separate thread. fn create_svg_list(&self) { let qr_state = self.qr_image_state.clone(); let text = self.text.clone(); thread::spawn(move || { let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap(); let mut data = Vec::with_capacity(encoder.fragment_count()); for _ in 0..encoder.fragment_count() { let ur = encoder.next_part().unwrap(); if let Ok(qr) = QrCode::encode_text(ur.as_str(), qrcodegen::QrCodeEcc::Low) { let svg = Self::qr_to_svg(qr, 0); data.push(svg.into_bytes()); } } let mut w_state = qr_state.write(); if !data.is_empty() { w_state.svg_list = Some(data); } w_state.loading = false; }); } /// Check if image was created. fn has_image(&self) -> bool { let r_state = self.qr_image_state.read(); r_state.svg.is_some() || r_state.svg_list.is_some() } /// Create vector QR code image at separate thread. fn create_svg(&self) { let qr_state = self.qr_image_state.clone(); let text = self.text.clone(); thread::spawn(move || { if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) { let svg = Self::qr_to_svg(qr, 0); let mut w_state = qr_state.write(); w_state.loading = false; w_state.svg = Some(svg.into_bytes()); } }); } /// Convert QR code to SVG string. fn qr_to_svg(qr: QrCode, border: i32) -> String { let mut result = String::new(); let dimension = qr.size().checked_add(border.checked_mul(2).unwrap()).unwrap(); result += "\n"; result += "\n"; result += &format!( "\n", dimension); result += "\t\n"; result += "\t\n"; result += "\n"; result } /// Create GIF image at separate thread. fn create_qr_gif(&self) { { let mut w_state = self.qr_image_state.write(); w_state.gif_creating = true; } let qr_state = self.qr_image_state.clone(); let text = self.text.clone(); thread::spawn(move || { // Setup GIF image encoder. let mut gif = vec![]; { // Generate QR codes from text. let mut qrs = vec![]; let mut ur_enc = ur::Encoder::bytes(text.as_bytes(), 100).unwrap(); for _ in 0..ur_enc.fragment_count() { let ur = ur_enc.next_part().unwrap(); if let Ok(qr) = qrcode::QrCode::with_error_correction_level( ur.as_bytes(), qrcode::EcLevel::L ) { // Create an image from QR data. let image = qr.render() .max_dimensions(DEFAULT_QR_SIZE, DEFAULT_QR_SIZE) .dark_color(image::Rgb([0, 0, 0])) .light_color(image::Rgb([255, 255, 255])) .build(); qrs.push(image); } } if !qrs.is_empty() { // Generate GIF data. let color_map = &[0, 0, 0, 0xFF, 0xFF, 0xFF]; let mut gif_enc = gif::Encoder::new(&mut gif, qrs[0].width() as u16, qrs[0].height() as u16, color_map).unwrap(); gif_enc.set_repeat(gif::Repeat::Infinite).unwrap(); for qr in qrs { let mut frame = gif::Frame::from_rgb(qr.width() as u16, qr.height() as u16, qr.as_raw().as_slice()); frame.delay = 10; // Write an image to GIF encoder. if let Ok(_) = gif_enc.write_frame(&frame) { continue; } // Exit on error. let mut w_state = qr_state.write(); w_state.gif_creating = false; return; } } } // Setup GIF image data. let mut w_state = qr_state.write(); if !gif.is_empty() { w_state.gif_data = Some(gif); } w_state.gif_creating = false; }); } /// Convert QR code to image data. fn qr_to_image_data(qr: QrCode, size: usize) -> Option> { if size >= 2usize.pow((size_of::() * 4) as u32) { return None; } let margin_size = 1; let s = qr.size(); let data_length = s as usize; let data_length_with_margin = data_length + 2 * margin_size; let point_size = size / data_length_with_margin; if point_size == 0 { return None; } let margin = (size - (point_size * data_length)) / 2; let length = size * size; let mut img_raw: Vec = vec![255u8; length]; for i in 0..s { for j in 0..s { if qr.get_module(i, j) { let x = i as usize * point_size + margin; let y = j as usize * point_size + margin; for j in y..(y + point_size) { let offset = j * size; for i in x..(x + point_size) { img_raw[offset + i] = 0; } } } } } Some(img_raw) } }