Skip to main content

memvid_rs/qr/
decoder.rs

1//! QR code decoding functionality
2//!
3//! This module provides QR code decoding with automatic decompression
4//! for extracting text content from video frames.
5
6use crate::error::{MemvidError, Result};
7use base64::{Engine as _, engine::general_purpose};
8use flate2::read::GzDecoder;
9use image::DynamicImage;
10use std::io::Read;
11
12/// QR code decoder with decompression support
13pub struct QrDecoder;
14
15/// Result of QR code decoding
16#[derive(Debug, Clone)]
17pub struct DecodeResult {
18    /// The decoded text content
19    pub text: String,
20
21    /// Whether the data was compressed
22    pub was_compressed: bool,
23
24    /// Size of the encoded data before decompression
25    pub encoded_size: usize,
26}
27
28impl QrDecoder {
29    /// Create a new QR decoder
30    pub fn new() -> Self {
31        Self
32    }
33
34    /// Decode QR code from image data
35    pub fn decode_image(&self, image: &DynamicImage) -> Result<DecodeResult> {
36        // Convert to grayscale for better QR detection
37        let gray_image = image.to_luma8();
38
39        // Convert to rqrr format
40        let mut img = rqrr::PreparedImage::prepare(gray_image);
41
42        // Find and decode QR codes
43        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        // Decode the first QR code found
50        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        // Check if data is compressed and decompress if needed
58        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    /// Decode QR code from raw image bytes
68    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    /// Decode multiple QR codes from images
76    pub fn decode_batch(&self, images: &[DynamicImage]) -> Vec<Result<DecodeResult>> {
77        images.iter().map(|img| self.decode_image(img)).collect()
78    }
79
80    /// Process decoded data (handle decompression if needed)
81    fn process_decoded_data(&self, data: &str) -> Result<(String, bool)> {
82        // Check if data has compression prefix
83        if let Some(compressed_data) = data.strip_prefix("GZ:") {
84            // Remove "GZ:" prefix
85            let decompressed = self.decompress_data(compressed_data)?;
86            Ok((decompressed, true))
87        } else {
88            Ok((data.to_string(), false))
89        }
90    }
91
92    /// Decompress base64-encoded gzip data
93    fn decompress_data(&self, base64_data: &str) -> Result<String> {
94        // Decode base64
95        let compressed_bytes = general_purpose::STANDARD
96            .decode(base64_data)
97            .map_err(|e| MemvidError::QrCode(format!("Base64 decode failed: {}", e)))?;
98
99        // Decompress gzip
100        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    /// Try to decode QR code with multiple preprocessing strategies
110    pub fn decode_with_preprocessing(&self, image: &DynamicImage) -> Result<DecodeResult> {
111        // Try direct decoding first
112        if let Ok(result) = self.decode_image(image) {
113            return Ok(result);
114        }
115
116        // Try with contrast enhancement
117        if let Ok(result) = self.decode_with_contrast_enhancement(image) {
118            return Ok(result);
119        }
120
121        // Try with different scaling
122        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    /// Decode with contrast enhancement
132    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    /// Decode with different scaling factors
143    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    /// Validate that decoded text is reasonable
170    pub fn validate_decoded_text(&self, text: &str) -> bool {
171        // Basic validation: not empty, reasonable length, valid UTF-8
172        !text.is_empty() &&
173        text.len() < 100_000 && // Reasonable size limit
174        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
184// Convert rqrr error to MemvidError
185impl 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; // Make QR code larger for better decoding
200        config.border = 4; // Ensure sufficient quiet zone
201
202        let encoder = QrEncoder::new(config);
203        let frame = encoder.encode_text(text).unwrap();
204
205        // Resize the image to be larger for better decoding reliability
206        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; // Force compression
227        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        // Note: compression might not always be used depending on efficiency
239    }
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        // Create a blank image with no QR code
266        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("")); // Empty
279
280        // Very long text should fail
281        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}