Commit bacf1c4fb4d407879af640eed4a0f4e449e16ec7

constantoine 2022-05-05T11:50:41

Switch to qrcodegen, and update image dependency Signed-off-by: constantoine <cleo.rebert@gmail.com>

diff --git a/Cargo.toml b/Cargo.toml
index c44f914..0e621a2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,7 +16,7 @@ features = [ "qr", "serde_support" ]
 
 [features]
 default = []
-qr = ["qrcode", "image", "base64"]
+qr = ["qrcodegen", "image", "base64"]
 serde_support = ["serde"]
 
 [dependencies]
@@ -26,6 +26,6 @@ sha-1 = "~0.10.0"
 hmac = "~0.12.1"
 base32 = "~0.4"
 constant_time_eq = "~0.2.1"
-qrcode = { version = "~0.12", optional = true }
-image = { version = "~0.23.14", optional = true}
+qrcodegen = { version = "~1.8", optional = true }
+image = { version = "~0.24.2", optional = true}
 base64 = { version = "~0.13", optional = true }
diff --git a/src/lib.rs b/src/lib.rs
index 0476204..ca28739 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -52,7 +52,7 @@ use serde::{Deserialize, Serialize};
 use core::fmt;
 
 #[cfg(feature = "qr")]
-use {base64, image::Luma, qrcode::QrCode};
+use {base64, image::Luma, qrcodegen};
 
 use hmac::Mac;
 
@@ -206,19 +206,78 @@ impl<T: AsRef<[u8]>> TOTP<T> {
     ///
     /// # Errors
     ///
-    /// This will return an error in case the URL gets too long to encode into a QR code
+    /// This will return an error in case the URL gets too long to encode into a QR code.
+    /// This would require the get_url method to generate an url bigger than 2000 characters,
+    /// Which would be too long for some browsers anyway.
     ///
     /// It will also return an error in case it can't encode the qr into a png. This shouldn't happen unless either the qrcode library returns malformed data, or the image library doesn't encode the data correctly
     #[cfg(feature = "qr")]
     pub fn get_qr(&self, label: &str, issuer: &str) -> Result<String, Box<dyn std::error::Error>> {
+        use image::ImageEncoder;
+
         let url = self.get_url(label, issuer);
-        let code = QrCode::new(&url)?;
         let mut vec = Vec::new();
-        let encoder = image::png::PngEncoder::new(&mut vec);
-        encoder.encode(
-            code.render::<Luma<u8>>().build().as_ref(),
-            ((code.width() + 8) * 8) as u32,
-            ((code.width() + 8) * 8) as u32,
+        let qr = qrcodegen::QrCode::encode_text(&url, qrcodegen::QrCodeEcc::Medium)?;
+        let size = qr.size() as u32;
+        
+        // "+ 8 * 8" is here to add padding (the white border around the QRCode)
+        // As some QRCode readers don't work without padding 
+        let image_size = size * 8 + 8 * 8;
+        let mut canvas = image::GrayImage::new(image_size, image_size);
+        
+        // Draw the border
+        for x in 0..image_size {
+            for y in 0..image_size {
+                if y < 8*4 || y >= image_size - 8*4 {
+                    canvas.put_pixel(
+                        x,
+                        y,
+                        Luma([255]),
+                    );
+                    continue;
+                }
+                if x < 8*4 || x >= image_size - 8*4 {
+                    canvas.put_pixel(
+                        x,
+                        y,
+                        Luma([255]),
+                    );
+                }
+            }
+        }
+
+        // The QR inside the white border
+        for x_qr in 0..size {
+            for y_qr in 0..size {
+                // The canvas is a grayscale image without alpha. Hence it's only one 8-bits byte longs
+                // This clever trick to one-line the value was achieved with advanced mathematics
+                // And deep understanding of Boolean algebra.
+                let val = !qr.get_module(x_qr as i32, y_qr as i32) as u8 * 255;
+                
+                // Multiply coordinates by width of pixels
+                // And take into account the 8*4 padding on top and left side
+                let x_start = x_qr * 8 + 8*4;
+                let y_start = y_qr * 8 + 8*4;
+                
+                // Draw a 8-pixels-wide square
+                for x_img in x_start..x_start + 8 {
+                    for y_img in y_start..y_start + 8 {
+                        canvas.put_pixel(
+                            x_img,
+                            y_img,
+                            Luma([val]),
+                        );
+                    }
+                }
+            }
+        }
+
+        // Encode the canvas into a PNG
+        let encoder = image::codecs::png::PngEncoder::new(&mut vec);
+        encoder.write_image(
+            &image::ImageBuffer::from(canvas).into_raw(),
+            image_size,
+            image_size,
             image::ColorType::L8,
         )?;
         Ok(base64::encode(vec))
@@ -345,7 +404,7 @@ mod tests {
         let hash_digest = Sha1::digest(qr.as_bytes());
         assert_eq!(
             format!("{:x}", hash_digest).as_str(),
-            "3abc0127e7a2b1013fb25c97ef14422c1fe9e878"
+            "f671a5a553227a9565c6132024808123f2c9e5e3"
         );
     }
 }