use crate::error::{MemvidError, Result};
use base64::{Engine as _, engine::general_purpose};
use flate2::read::GzDecoder;
use image::DynamicImage;
use std::io::Read;
pub struct QrDecoder;
#[derive(Debug, Clone)]
pub struct DecodeResult {
pub text: String,
pub was_compressed: bool,
pub encoded_size: usize,
}
impl QrDecoder {
pub fn new() -> Self {
Self
}
pub fn decode_image(&self, image: &DynamicImage) -> Result<DecodeResult> {
let gray_image = image.to_luma8();
let mut img = rqrr::PreparedImage::prepare(gray_image);
let grids = img.detect_grids();
if grids.is_empty() {
return Err(MemvidError::QrCode("No QR code found in image".to_string()));
}
let grid = &grids[0];
let (_meta, content) = grid.decode()?;
let encoded_data = content;
let encoded_size = encoded_data.len();
let (text, was_compressed) = self.process_decoded_data(&encoded_data)?;
Ok(DecodeResult {
text,
was_compressed,
encoded_size,
})
}
pub fn decode_bytes(&self, image_bytes: &[u8]) -> Result<DecodeResult> {
let image = image::load_from_memory(image_bytes)
.map_err(|e| MemvidError::Image(format!("Failed to load image: {}", e)))?;
self.decode_image(&image)
}
pub fn decode_batch(&self, images: &[DynamicImage]) -> Vec<Result<DecodeResult>> {
images.iter().map(|img| self.decode_image(img)).collect()
}
fn process_decoded_data(&self, data: &str) -> Result<(String, bool)> {
if let Some(compressed_data) = data.strip_prefix("GZ:") {
let decompressed = self.decompress_data(compressed_data)?;
Ok((decompressed, true))
} else {
Ok((data.to_string(), false))
}
}
fn decompress_data(&self, base64_data: &str) -> Result<String> {
let compressed_bytes = general_purpose::STANDARD
.decode(base64_data)
.map_err(|e| MemvidError::QrCode(format!("Base64 decode failed: {}", e)))?;
let mut decoder = GzDecoder::new(&compressed_bytes[..]);
let mut decompressed = String::new();
decoder
.read_to_string(&mut decompressed)
.map_err(|e| MemvidError::QrCode(format!("Decompression failed: {}", e)))?;
Ok(decompressed)
}
pub fn decode_with_preprocessing(&self, image: &DynamicImage) -> Result<DecodeResult> {
if let Ok(result) = self.decode_image(image) {
return Ok(result);
}
if let Ok(result) = self.decode_with_contrast_enhancement(image) {
return Ok(result);
}
if let Ok(result) = self.decode_with_scaling(image) {
return Ok(result);
}
Err(MemvidError::QrCode(
"Failed to decode QR code with all strategies".to_string(),
))
}
fn decode_with_contrast_enhancement(&self, image: &DynamicImage) -> Result<DecodeResult> {
use imageproc::contrast::*;
let gray_image = image.to_luma8();
let enhanced = stretch_contrast(&gray_image, 0, 255, 0, 255);
let enhanced_dynamic = DynamicImage::ImageLuma8(enhanced);
self.decode_image(&enhanced_dynamic)
}
fn decode_with_scaling(&self, image: &DynamicImage) -> Result<DecodeResult> {
let scale_factors = [0.5, 1.5, 2.0, 0.75, 1.25];
for &scale in &scale_factors {
let (new_width, new_height) = (
(image.width() as f32 * scale) as u32,
(image.height() as f32 * scale) as u32,
);
if new_width > 0 && new_height > 0 {
let resized = image.resize_exact(
new_width,
new_height,
image::imageops::FilterType::Lanczos3,
);
if let Ok(result) = self.decode_image(&resized) {
return Ok(result);
}
}
}
Err(MemvidError::QrCode(
"Failed to decode with scaling".to_string(),
))
}
pub fn validate_decoded_text(&self, text: &str) -> bool {
!text.is_empty() &&
text.len() < 100_000 && text.chars().all(|c| c.is_ascii() || c.is_alphabetic() || c.is_numeric() || c.is_whitespace())
}
}
impl Default for QrDecoder {
fn default() -> Self {
Self::new()
}
}
impl From<rqrr::DeQRError> for MemvidError {
fn from(error: rqrr::DeQRError) -> Self {
MemvidError::QrCode(format!("QR decode error: {:?}", error))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::QrConfig;
use crate::qr::encoder::QrEncoder;
fn create_test_qr_image(text: &str) -> DynamicImage {
let mut config = QrConfig::default();
config.box_size = 10; config.border = 4;
let encoder = QrEncoder::new(config);
let frame = encoder.encode_text(text).unwrap();
let resized = frame
.image
.resize(200, 200, image::imageops::FilterType::Nearest);
resized
}
#[test]
fn test_simple_decode() {
let decoder = QrDecoder::new();
let test_text = "Hello, World!";
let qr_image = create_test_qr_image(test_text);
let result = decoder.decode_image(&qr_image).unwrap();
assert_eq!(result.text, test_text);
assert!(!result.was_compressed);
}
#[test]
fn test_compressed_decode() {
let mut config = QrConfig::default();
config.compression_threshold = 5; config.enable_compression = true;
let encoder = QrEncoder::new(config);
let test_text =
"This is a longer text that should be compressed and then decompressed correctly.";
let frame = encoder.encode_text(test_text).unwrap();
let decoder = QrDecoder::new();
let result = decoder.decode_image(&frame.image).unwrap();
assert_eq!(result.text, test_text);
}
#[test]
fn test_batch_decode() {
let decoder = QrDecoder::new();
let texts = vec!["Text 1", "Text 2", "Text 3"];
let images: Vec<DynamicImage> = texts
.iter()
.map(|text| create_test_qr_image(text))
.collect();
let results = decoder.decode_batch(&images);
assert_eq!(results.len(), 3);
for (i, result) in results.iter().enumerate() {
match result {
Ok(decode_result) => assert_eq!(decode_result.text, texts[i]),
Err(e) => panic!("Decode failed: {}", e),
}
}
}
#[test]
fn test_invalid_image() {
let decoder = QrDecoder::new();
let blank_image = DynamicImage::new_luma8(100, 100);
let result = decoder.decode_image(&blank_image);
assert!(result.is_err());
}
#[test]
fn test_text_validation() {
let decoder = QrDecoder::new();
assert!(decoder.validate_decoded_text("Valid text"));
assert!(decoder.validate_decoded_text("Text with numbers 123"));
assert!(!decoder.validate_decoded_text(""));
let long_text = "a".repeat(200_000);
assert!(!decoder.validate_decoded_text(&long_text));
}
#[test]
fn test_encode_decode_roundtrip() {
let encoder = QrEncoder::default();
let decoder = QrDecoder::new();
let original_texts = vec![
"Simple text",
"Text with special characters: !@#$%^&*()",
"Multi-line\ntext\nwith\nbreaks",
"Unicode text: 🚀🎯✨",
];
for original in original_texts {
let frame = encoder.encode_text(original).unwrap();
let result = decoder.decode_image(&frame.image).unwrap();
assert_eq!(result.text, original);
}
}
}