1use crate::error::{MemvidError, Result};
7use base64::{Engine as _, engine::general_purpose};
8use flate2::read::GzDecoder;
9use image::DynamicImage;
10use std::io::Read;
11
12pub struct QrDecoder;
14
15#[derive(Debug, Clone)]
17pub struct DecodeResult {
18 pub text: String,
20
21 pub was_compressed: bool,
23
24 pub encoded_size: usize,
26}
27
28impl QrDecoder {
29 pub fn new() -> Self {
31 Self
32 }
33
34 pub fn decode_image(&self, image: &DynamicImage) -> Result<DecodeResult> {
36 let gray_image = image.to_luma8();
38
39 let mut img = rqrr::PreparedImage::prepare(gray_image);
41
42 let grids = img.detect_grids();
44
45 if grids.is_empty() {
46 return Err(MemvidError::QrCode("No QR code found in image".to_string()));
47 }
48
49 let grid = &grids[0];
51 let (_meta, content) = grid.decode()?;
52
53 let encoded_data = content;
54
55 let encoded_size = encoded_data.len();
56
57 let (text, was_compressed) = self.process_decoded_data(&encoded_data)?;
59
60 Ok(DecodeResult {
61 text,
62 was_compressed,
63 encoded_size,
64 })
65 }
66
67 pub fn decode_bytes(&self, image_bytes: &[u8]) -> Result<DecodeResult> {
69 let image = image::load_from_memory(image_bytes)
70 .map_err(|e| MemvidError::Image(format!("Failed to load image: {}", e)))?;
71
72 self.decode_image(&image)
73 }
74
75 pub fn decode_batch(&self, images: &[DynamicImage]) -> Vec<Result<DecodeResult>> {
77 images.iter().map(|img| self.decode_image(img)).collect()
78 }
79
80 fn process_decoded_data(&self, data: &str) -> Result<(String, bool)> {
82 if let Some(compressed_data) = data.strip_prefix("GZ:") {
84 let decompressed = self.decompress_data(compressed_data)?;
86 Ok((decompressed, true))
87 } else {
88 Ok((data.to_string(), false))
89 }
90 }
91
92 fn decompress_data(&self, base64_data: &str) -> Result<String> {
94 let compressed_bytes = general_purpose::STANDARD
96 .decode(base64_data)
97 .map_err(|e| MemvidError::QrCode(format!("Base64 decode failed: {}", e)))?;
98
99 let mut decoder = GzDecoder::new(&compressed_bytes[..]);
101 let mut decompressed = String::new();
102 decoder
103 .read_to_string(&mut decompressed)
104 .map_err(|e| MemvidError::QrCode(format!("Decompression failed: {}", e)))?;
105
106 Ok(decompressed)
107 }
108
109 pub fn decode_with_preprocessing(&self, image: &DynamicImage) -> Result<DecodeResult> {
111 if let Ok(result) = self.decode_image(image) {
113 return Ok(result);
114 }
115
116 if let Ok(result) = self.decode_with_contrast_enhancement(image) {
118 return Ok(result);
119 }
120
121 if let Ok(result) = self.decode_with_scaling(image) {
123 return Ok(result);
124 }
125
126 Err(MemvidError::QrCode(
127 "Failed to decode QR code with all strategies".to_string(),
128 ))
129 }
130
131 fn decode_with_contrast_enhancement(&self, image: &DynamicImage) -> Result<DecodeResult> {
133 use imageproc::contrast::*;
134
135 let gray_image = image.to_luma8();
136 let enhanced = stretch_contrast(&gray_image, 0, 255, 0, 255);
137 let enhanced_dynamic = DynamicImage::ImageLuma8(enhanced);
138
139 self.decode_image(&enhanced_dynamic)
140 }
141
142 fn decode_with_scaling(&self, image: &DynamicImage) -> Result<DecodeResult> {
144 let scale_factors = [0.5, 1.5, 2.0, 0.75, 1.25];
145
146 for &scale in &scale_factors {
147 let (new_width, new_height) = (
148 (image.width() as f32 * scale) as u32,
149 (image.height() as f32 * scale) as u32,
150 );
151
152 if new_width > 0 && new_height > 0 {
153 let resized = image.resize_exact(
154 new_width,
155 new_height,
156 image::imageops::FilterType::Lanczos3,
157 );
158 if let Ok(result) = self.decode_image(&resized) {
159 return Ok(result);
160 }
161 }
162 }
163
164 Err(MemvidError::QrCode(
165 "Failed to decode with scaling".to_string(),
166 ))
167 }
168
169 pub fn validate_decoded_text(&self, text: &str) -> bool {
171 !text.is_empty() &&
173 text.len() < 100_000 && text.chars().all(|c| c.is_ascii() || c.is_alphabetic() || c.is_numeric() || c.is_whitespace())
175 }
176}
177
178impl Default for QrDecoder {
179 fn default() -> Self {
180 Self::new()
181 }
182}
183
184impl From<rqrr::DeQRError> for MemvidError {
186 fn from(error: rqrr::DeQRError) -> Self {
187 MemvidError::QrCode(format!("QR decode error: {:?}", error))
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::config::QrConfig;
195 use crate::qr::encoder::QrEncoder;
196
197 fn create_test_qr_image(text: &str) -> DynamicImage {
198 let mut config = QrConfig::default();
199 config.box_size = 10; config.border = 4; let encoder = QrEncoder::new(config);
203 let frame = encoder.encode_text(text).unwrap();
204
205 let resized = frame
207 .image
208 .resize(200, 200, image::imageops::FilterType::Nearest);
209 resized
210 }
211
212 #[test]
213 fn test_simple_decode() {
214 let decoder = QrDecoder::new();
215 let test_text = "Hello, World!";
216 let qr_image = create_test_qr_image(test_text);
217
218 let result = decoder.decode_image(&qr_image).unwrap();
219 assert_eq!(result.text, test_text);
220 assert!(!result.was_compressed);
221 }
222
223 #[test]
224 fn test_compressed_decode() {
225 let mut config = QrConfig::default();
226 config.compression_threshold = 5; config.enable_compression = true;
228
229 let encoder = QrEncoder::new(config);
230 let test_text =
231 "This is a longer text that should be compressed and then decompressed correctly.";
232 let frame = encoder.encode_text(test_text).unwrap();
233
234 let decoder = QrDecoder::new();
235 let result = decoder.decode_image(&frame.image).unwrap();
236
237 assert_eq!(result.text, test_text);
238 }
240
241 #[test]
242 fn test_batch_decode() {
243 let decoder = QrDecoder::new();
244 let texts = vec!["Text 1", "Text 2", "Text 3"];
245 let images: Vec<DynamicImage> = texts
246 .iter()
247 .map(|text| create_test_qr_image(text))
248 .collect();
249
250 let results = decoder.decode_batch(&images);
251 assert_eq!(results.len(), 3);
252
253 for (i, result) in results.iter().enumerate() {
254 match result {
255 Ok(decode_result) => assert_eq!(decode_result.text, texts[i]),
256 Err(e) => panic!("Decode failed: {}", e),
257 }
258 }
259 }
260
261 #[test]
262 fn test_invalid_image() {
263 let decoder = QrDecoder::new();
264
265 let blank_image = DynamicImage::new_luma8(100, 100);
267 let result = decoder.decode_image(&blank_image);
268
269 assert!(result.is_err());
270 }
271
272 #[test]
273 fn test_text_validation() {
274 let decoder = QrDecoder::new();
275
276 assert!(decoder.validate_decoded_text("Valid text"));
277 assert!(decoder.validate_decoded_text("Text with numbers 123"));
278 assert!(!decoder.validate_decoded_text("")); let long_text = "a".repeat(200_000);
282 assert!(!decoder.validate_decoded_text(&long_text));
283 }
284
285 #[test]
286 fn test_encode_decode_roundtrip() {
287 let encoder = QrEncoder::default();
288 let decoder = QrDecoder::new();
289
290 let original_texts = vec![
291 "Simple text",
292 "Text with special characters: !@#$%^&*()",
293 "Multi-line\ntext\nwith\nbreaks",
294 "Unicode text: 🚀🎯✨",
295 ];
296
297 for original in original_texts {
298 let frame = encoder.encode_text(original).unwrap();
299 let result = decoder.decode_image(&frame.image).unwrap();
300 assert_eq!(result.text, original);
301 }
302 }
303}