443 lines
No EOL
18 KiB
Rust
443 lines
No EOL
18 KiB
Rust
// Copyright 2024 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::sync::Arc;
|
|
use parking_lot::RwLock;
|
|
use std::thread;
|
|
use eframe::emath::Align;
|
|
use egui::load::SizedTexture;
|
|
use egui::{Layout, Pos2, Rect, RichText, TextureOptions, Widget};
|
|
use image::{DynamicImage, EncodableLayout, ImageFormat};
|
|
|
|
use grin_util::ZeroingString;
|
|
use grin_wallet_libwallet::SlatepackAddress;
|
|
use grin_keychain::mnemonic::WORDS;
|
|
|
|
use crate::gui::Colors;
|
|
use crate::gui::icons::CAMERA_ROTATE;
|
|
use crate::gui::platform::PlatformCallbacks;
|
|
use crate::gui::views::types::{QrScanResult, QrScanState};
|
|
use crate::gui::views::View;
|
|
use crate::wallet::types::PhraseSize;
|
|
use crate::wallet::WalletUtils;
|
|
|
|
/// Camera QR code scanner.
|
|
pub struct CameraContent {
|
|
/// QR code scanning progress and result.
|
|
qr_scan_state: Arc<RwLock<QrScanState>>,
|
|
|
|
/// Uniform Resources URIs collected from QR code scanning.
|
|
ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>,
|
|
}
|
|
|
|
impl Default for CameraContent {
|
|
fn default() -> Self {
|
|
Self {
|
|
qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
|
|
ur_data: Arc::new(RwLock::new(None)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CameraContent {
|
|
/// Draw camera content.
|
|
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
|
// Draw last image from camera or loader.
|
|
if let Some(img_data) = cb.camera_image() {
|
|
// Load image to draw.
|
|
if let Ok(mut img) =
|
|
image::load_from_memory_with_format(&*img_data.0, ImageFormat::Jpeg) {
|
|
// Process image to find QR code.
|
|
self.scan_qr(&img);
|
|
// Setup image rotation.
|
|
img = match img_data.1 {
|
|
90 => img.rotate90(),
|
|
180 => img.rotate180(),
|
|
270 => img.rotate270(),
|
|
_ => img
|
|
};
|
|
// Convert to ColorImage to add at content.
|
|
let color_img = match &img {
|
|
DynamicImage::ImageRgb8(image) => {
|
|
egui::ColorImage::from_rgb(
|
|
[image.width() as usize, image.height() as usize],
|
|
image.as_bytes(),
|
|
)
|
|
},
|
|
other => {
|
|
let image = other.to_rgba8();
|
|
egui::ColorImage::from_rgba_unmultiplied(
|
|
[image.width() as usize, image.height() as usize],
|
|
image.as_bytes(),
|
|
)
|
|
},
|
|
};
|
|
// Create image texture.
|
|
let texture = ui.ctx().load_texture("camera_image",
|
|
color_img.clone(),
|
|
TextureOptions::default());
|
|
let img_size = egui::emath::vec2(color_img.width() as f32,
|
|
color_img.height() as f32);
|
|
let sized_img = SizedTexture::new(texture.id(), img_size);
|
|
// Add image to content.
|
|
ui.vertical_centered(|ui| {
|
|
egui::Image::from_texture(sized_img)
|
|
// Setup to crop image at square.
|
|
.uv(Rect::from([
|
|
Pos2::new(1.0 - (img_size.y / img_size.x), 0.0),
|
|
Pos2::new(1.0, 1.0)
|
|
]))
|
|
.max_height(ui.available_width())
|
|
.maintain_aspect_ratio(false)
|
|
.shrink_to_fit()
|
|
.ui(ui);
|
|
});
|
|
|
|
// Show UR scan progress.
|
|
let show_ur_progress = {
|
|
self.ur_data.clone().read().is_some()
|
|
};
|
|
let ur_progress = self.ur_progress();
|
|
if show_ur_progress && ur_progress != 0 {
|
|
ui.add_space(-52.0);
|
|
ui.vertical_centered(|ui| {
|
|
ui.label(RichText::new(format!("{}%", ur_progress))
|
|
.size(16.0)
|
|
.color(Colors::yellow()));
|
|
});
|
|
}
|
|
|
|
// Show button to switch cameras.
|
|
if cb.can_switch_camera() {
|
|
ui.add_space(-52.0);
|
|
let mut size = ui.available_size();
|
|
size.y = 48.0;
|
|
ui.allocate_ui_with_layout(size, Layout::right_to_left(Align::Max), |ui| {
|
|
ui.add_space(4.0);
|
|
View::button(ui, CAMERA_ROTATE.to_string(), Colors::white_or_black(false), || {
|
|
cb.switch_camera();
|
|
});
|
|
});
|
|
}
|
|
} else {
|
|
self.loading_content_ui(ui);
|
|
}
|
|
} else {
|
|
self.loading_content_ui(ui);
|
|
}
|
|
|
|
// Request redraw.
|
|
ui.ctx().request_repaint();
|
|
}
|
|
|
|
/// Draw camera loading progress content.
|
|
fn loading_content_ui(&self, ui: &mut egui::Ui) {
|
|
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);
|
|
});
|
|
}
|
|
|
|
/// Check if image is processing to find QR code.
|
|
fn image_processing(&self) -> bool {
|
|
let r_scan = self.qr_scan_state.read();
|
|
r_scan.image_processing
|
|
}
|
|
|
|
/// Get UR scanning progress in percents.
|
|
fn ur_progress(&self) -> i32 {
|
|
// Setup data.
|
|
let r_data = self.ur_data.read();
|
|
let (data, total) = r_data.clone().unwrap_or((vec![], 0));
|
|
if data.is_empty() {
|
|
return 0;
|
|
}
|
|
// Calculate progress.
|
|
let mut complete = 0;
|
|
for i in &data {
|
|
if !i.is_empty() {
|
|
complete += 1;
|
|
}
|
|
}
|
|
(100 * complete / total) as i32
|
|
}
|
|
|
|
/// Parse QR code from provided image data.
|
|
fn scan_qr(&self, image_data: &DynamicImage) {
|
|
// Do not scan when another image is processing.
|
|
if self.image_processing() {
|
|
return;
|
|
}
|
|
// Setup scanning flag.
|
|
{
|
|
let mut w_scan = self.qr_scan_state.write();
|
|
w_scan.image_processing = true;
|
|
}
|
|
|
|
let image_data = image_data.clone();
|
|
let qr_scan_state = self.qr_scan_state.clone();
|
|
let ur_data = self.ur_data.clone();
|
|
|
|
let on_scan = async move {
|
|
// Prepare image data.
|
|
let img = image_data.to_luma8();
|
|
let mut img: rqrr::PreparedImage<image::GrayImage>
|
|
= rqrr::PreparedImage::prepare(img);
|
|
// Scan and save results.
|
|
let grids = img.detect_grids();
|
|
if let Some(g) = grids.get(0) {
|
|
let mut qr_data = vec![];
|
|
if let Ok(_) = g.decode_to(&mut qr_data) {
|
|
// Setup scanned data into text.
|
|
let text = String::from_utf8(qr_data.clone()).unwrap_or("".to_string());
|
|
// Setup current text.
|
|
let cur_text = {
|
|
let r_scan = qr_scan_state.read();
|
|
let text = if let Some(res) = r_scan.qr_scan_result.clone() {
|
|
res.text()
|
|
} else {
|
|
"".to_string()
|
|
};
|
|
text
|
|
};
|
|
// Parse non-empty data if parsed text is different from saved.
|
|
if !qr_data.is_empty() && (cur_text.is_empty() || text != cur_text) {
|
|
let res = Self::parse_qr_code(qr_data);
|
|
match res {
|
|
QrScanResult::URPart(uri, index, total) => {
|
|
// Setup current UR data.
|
|
let mut cur_data = {
|
|
let r_data = ur_data.read();
|
|
let mut cur_data = vec!["".to_string(); total];
|
|
if let Some((d, _)) = r_data.clone() {
|
|
cur_data = d;
|
|
}
|
|
cur_data
|
|
};
|
|
if !cur_data.contains(&uri) {
|
|
// Save part of UR data.
|
|
{
|
|
cur_data.insert(index, uri);
|
|
let mut w_data = ur_data.write();
|
|
*w_data = Some((cur_data.clone(), total));
|
|
}
|
|
// Setup UR decoder.
|
|
let mut decoder = ur::Decoder::default();
|
|
for m in cur_data {
|
|
if !m.is_empty() {
|
|
if let Ok(_) = decoder.receive(m.as_str()) {
|
|
continue;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Check if UR data is complete.
|
|
if decoder.complete() {
|
|
if let Ok(data) = decoder.message() {
|
|
// Parse complete data.
|
|
let res = Self::parse_qr_code(data.unwrap_or(vec![]));
|
|
// Clean UR data.
|
|
let mut w_data = ur_data.write();
|
|
*w_data = None;
|
|
// Save scan result.
|
|
let mut w_scan = qr_scan_state.write();
|
|
w_scan.qr_scan_result = Some(res);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
// Clean UR data.
|
|
let mut w_data = ur_data.write();
|
|
*w_data = None;
|
|
// Save scan result.
|
|
let mut w_scan = qr_scan_state.write();
|
|
w_scan.qr_scan_result = Some(res);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Reset scanning flag to process again.
|
|
{
|
|
let mut w_scan = qr_scan_state.write();
|
|
w_scan.image_processing = false;
|
|
}
|
|
};
|
|
|
|
// Launch scanner at separate thread.
|
|
thread::spawn(move || {
|
|
tokio::runtime::Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap()
|
|
.block_on(on_scan);
|
|
});
|
|
}
|
|
|
|
/// Parse QR code scan result.
|
|
fn parse_qr_code(data: Vec<u8>) -> QrScanResult {
|
|
// Check if string starts with Grin address prefix.
|
|
let text_string = String::from_utf8(data.clone()).unwrap_or("".to_string());
|
|
let text = text_string.trim();
|
|
if text.starts_with("tgrin") || text.starts_with("grin") {
|
|
if SlatepackAddress::try_from(text).is_ok() {
|
|
return QrScanResult::Address(ZeroingString::from(text));
|
|
}
|
|
}
|
|
|
|
// Check if string contains Slatepack message prefix and postfix.
|
|
if text.starts_with("BEGINSLATEPACK.") && text.ends_with("ENDSLATEPACK.") {
|
|
return QrScanResult::Slatepack(ZeroingString::from(text));
|
|
}
|
|
|
|
// Check Uniform Resource data.
|
|
// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
|
|
if text.starts_with("ur:bytes/") {
|
|
let split = text.split("/").collect::<Vec<_>>();
|
|
if let Some(index_total) = split.get(1) {
|
|
if let Some((index, total)) = index_total.split_once("-") {
|
|
let index = index.parse::<usize>();
|
|
let total = total.parse::<usize>();
|
|
if index.is_ok() && total.is_ok() {
|
|
let index = index.unwrap() - 1;
|
|
let total = total.unwrap();
|
|
return QrScanResult::URPart(text_string, index, total);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check Compact SeedQR format.
|
|
// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#compactseedqr-specification
|
|
if data.len() <= 32 && 16 <= data.len() && data.len() % 4 == 0 {
|
|
// Setup words amount.
|
|
let total_bits = data.len() * 8;
|
|
let checksum_bits = total_bits / 32;
|
|
let total_words = (total_bits + checksum_bits) / 11;
|
|
// Setup entropy.
|
|
let mut entropy = data.clone();
|
|
WalletUtils::setup_checksum(&mut entropy);
|
|
// Setup bits.
|
|
let mut bits = vec![false; entropy.len() * 8];
|
|
for i in 0..entropy.len() {
|
|
for j in 0..8 {
|
|
bits[(i * 8) + j] = (entropy[i] & (1 << (7 - j))) != 0;
|
|
}
|
|
}
|
|
// Extract word index.
|
|
let extract_index = |i: usize| -> usize {
|
|
let mut index = 0;
|
|
for j in 0..11 {
|
|
index = index << 1;
|
|
if bits[(i * 11) + j] {
|
|
index += 1;
|
|
}
|
|
}
|
|
return index;
|
|
};
|
|
// Setup words.
|
|
let mut words = "".to_string();
|
|
for n in 0..total_words {
|
|
// Setup word index.
|
|
let index = extract_index(n);
|
|
// Setup word.
|
|
let empty_word = "".to_string();
|
|
let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone();
|
|
if word.is_empty() {
|
|
words = empty_word;
|
|
break;
|
|
}
|
|
words = if words.is_empty() {
|
|
format!("{}", word)
|
|
} else {
|
|
format!("{} {}", words, word)
|
|
};
|
|
}
|
|
if !words.is_empty() {
|
|
return QrScanResult::SeedQR(ZeroingString::from(words));
|
|
}
|
|
}
|
|
|
|
// Check Standard SeedQR format.
|
|
// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#standard-seedqr-specification
|
|
let only_numbers = || {
|
|
for c in text.chars() {
|
|
if !c.is_numeric() {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
};
|
|
if !text.is_empty() && data.len() <= 96 && data.len() % 4 == 0 && only_numbers() {
|
|
if let Some(_) = PhraseSize::type_for_value(text.len() / 4) {
|
|
let chars: Vec<char> = text.trim().chars().collect();
|
|
let split = &chars.chunks(4)
|
|
.map(|chunk| chunk.iter().collect::<String>()
|
|
.trim()
|
|
.trim_start_matches("0")
|
|
.to_string()
|
|
)
|
|
.collect::<Vec<_>>();
|
|
let mut words = "".to_string();
|
|
for i in split {
|
|
let index = if i.is_empty() {
|
|
0usize
|
|
} else {
|
|
i.parse::<usize>().unwrap_or(WORDS.len())
|
|
};
|
|
let empty_word = "".to_string();
|
|
let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone();
|
|
// Return text result when BIP39 word was not found.
|
|
if word.is_empty() {
|
|
return QrScanResult::Text(ZeroingString::from(text));
|
|
}
|
|
words = if words.is_empty() {
|
|
format!("{}", word)
|
|
} else {
|
|
format!("{} {}", words, word)
|
|
};
|
|
}
|
|
return QrScanResult::SeedQR(ZeroingString::from(words));
|
|
}
|
|
}
|
|
|
|
// Return default text result.
|
|
QrScanResult::Text(ZeroingString::from(text))
|
|
}
|
|
|
|
/// Get QR code scan result.
|
|
pub fn qr_scan_result(&self) -> Option<QrScanResult> {
|
|
let r_scan = self.qr_scan_state.read();
|
|
if r_scan.qr_scan_result.is_some() {
|
|
return Some(r_scan.qr_scan_result.clone().unwrap());
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Reset camera content state to default.
|
|
pub fn clear_state(&mut self) {
|
|
// Clear QR code scanning state.
|
|
let mut w_scan = self.qr_scan_state.write();
|
|
*w_scan = QrScanState::default();
|
|
// Clear UR data.
|
|
let mut w_data = self.ur_data.write();
|
|
*w_data = None;
|
|
}
|
|
} |