From 839b0e94af8cacdcd5a5ce99eb5757fba8fa2c32 Mon Sep 17 00:00:00 2001 From: bjoern Date: Tue, 22 Oct 2024 21:49:45 +0200 Subject: [PATCH] api: create QR codes from any data (#6090) this PR adds a function that can be used to create any QR code, in a raw form. this can be used to create add-contact as well as add-second-device QR codes (eg. `dc_create_qr_svg(dc_get_securejoin_qr())`) - as well as for other QR codes as proxies. the disadvantage of the rich-formatted QR codes as created by `dc_get_securejoin_qr_svg()` and `dc_backup_provider_get_qr_svg()` were: - they do not look good and cannot interact with UI layout wise (but also tapping eg. an address is not easily possible) - esp. text really looks bad. even with [some](https://github.com/deltachat/deltachat-android/pull/2138/commits/e5dc8fe3d82e8b72631ae5806e9c4c7eec93a37a) [hacks](https://github.com/deltachat/deltachat-android/pull/2215) it [stays buggy](https://github.com/deltachat/deltachat-ios/issues/2200); the bugs mainly come from different SVG implementation, all need their own quirks - accessibility is probably bad as well we thought that time, SVG is a great thing for QR codes, but apart from basic geometrics, it is not. so, we avoid text, this also means to avoid putting an avatar in the middle of the QR code (we can put some generic symbol there, eg. different ones for add-contact and add-second-device). while this looks like a degradation, also other messengers use more raw QR codes. also, we removed many data from the QR code anyway, eg. the email address is no longer there. that time, sharing QR images was more a thing, meanwhile we have invite links, that are much better for that purpose. in theory, we could also leave the SVG path completely and go for PNG - which we did not that time as PNG and text looks bad, as the system font is not easily usable :) but going for PNG would add further challenges as passing binary data around, and also UI-implemtation-wise, that would be a larger step. so, let's stay with SVG in a form we know is compatible. the old QR code functions are deprecated. --- deltachat-ffi/deltachat.h | 20 ++++++++++- deltachat-ffi/src/lib.rs | 14 +++++++- deltachat-repl/src/cmdline.rs | 9 +++++ deltachat-repl/src/main.rs | 3 +- src/qr_code_generator.rs | 67 +++++++++++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 3 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 61717a32d..fa87e0329 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -2611,6 +2611,7 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch * Get QR code image from the QR code text generated by dc_get_securejoin_qr(). * See dc_get_securejoin_qr() for details about the contained QR code. * + * @deprecated 2024-10 use dc_create_qr_svg(dc_get_securejoin_qr()) instead. * @memberof dc_context_t * @param context The context object. * @param chat_id group-chat-id for secure-join or 0 for setup-contact, @@ -2791,6 +2792,22 @@ dc_array_t* dc_get_locations (dc_context_t* context, uint32_t cha void dc_delete_all_locations (dc_context_t* context); +// misc + +/** + * Create a QR code from any input data. + * + * The QR code is returned as a square SVG image. + * + * @memberof dc_context_t + * @param payload The content for the QR code. + * @return SVG image with the QR code. + * On errors, an empty string is returned. + * The returned string must be released using dc_str_unref() after usage. + */ +char* dc_create_qr_svg (const char* payload); + + /** * Get last error string. * @@ -2879,6 +2896,7 @@ char* dc_backup_provider_get_qr (const dc_backup_provider_t* backup_provider); * This works like dc_backup_provider_qr() but returns the text of a rendered * SVG image containing the QR code. * + * @deprecated 2024-10 use dc_create_qr_svg(dc_backup_provider_get_qr()) instead. * @memberof dc_backup_provider_t * @param backup_provider The backup provider object as created by * dc_backup_provider_new(). @@ -2918,7 +2936,7 @@ void dc_backup_provider_unref (dc_backup_provider_t* backup_provider); * Gets a backup offered by a dc_backup_provider_t object on another device. * * This function is called on a device that scanned the QR code offered by - * dc_backup_sender_qr() or dc_backup_sender_qr_svg(). Typically this is a + * dc_backup_provider_get_qr(). Typically this is a * different device than that which provides the backup. * * This call will block while the backup is being transferred and only diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 6559ab663..aade1d4f2 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -30,7 +30,7 @@ use deltachat::ephemeral::Timer as EphemeralTimer; use deltachat::imex::BackupProvider; use deltachat::key::preconfigure_keypair; use deltachat::message::MsgId; -use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg}; +use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg}; use deltachat::stock_str::StockMessage; use deltachat::webxdc::StatusUpdateSerial; use deltachat::*; @@ -2594,6 +2594,18 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) { }); } +#[no_mangle] +pub unsafe extern "C" fn dc_create_qr_svg(payload: *const libc::c_char) -> *mut libc::c_char { + if payload.is_null() { + eprintln!("ignoring careless call to dc_create_qr_svg()"); + return "".strdup(); + } + + create_qr_svg(&to_string_lossy(payload)) + .unwrap_or_else(|_| "".to_string()) + .strdup() +} + #[no_mangle] pub unsafe extern "C" fn dc_get_last_error(context: *mut dc_context_t) -> *mut libc::c_char { if context.is_null() { diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs index 1c6fc9ecd..dd1883984 100644 --- a/deltachat-repl/src/cmdline.rs +++ b/deltachat-repl/src/cmdline.rs @@ -22,6 +22,7 @@ use deltachat::mimeparser::SystemMessage; use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data}; use deltachat::peerstate::*; use deltachat::qr::*; +use deltachat::qr_code_generator::create_qr_svg; use deltachat::reaction::send_reaction; use deltachat::receive_imf::*; use deltachat::sql; @@ -425,6 +426,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu checkqr \n\ joinqr \n\ setqr \n\ + createqrsvg \n\ providerinfo \n\ fileinfo \n\ estimatedeletion \n\ @@ -1249,6 +1251,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu Err(err) => println!("Cannot set config from QR code: {err:?}"), } } + "createqrsvg" => { + ensure!(!arg1.is_empty(), "Argument missing."); + let svg = create_qr_svg(arg1)?; + let file = dirs::home_dir().unwrap_or_default().join("qr.svg"); + fs::write(&file, svg).await?; + println!("{file:#?} written."); + } "providerinfo" => { ensure!(!arg1.is_empty(), "Argument missing."); let proxy_enabled = context diff --git a/deltachat-repl/src/main.rs b/deltachat-repl/src/main.rs index 213c28317..2b16cc68f 100644 --- a/deltachat-repl/src/main.rs +++ b/deltachat-repl/src/main.rs @@ -240,12 +240,13 @@ const CONTACT_COMMANDS: [&str; 9] = [ "unblock", "listblocked", ]; -const MISC_COMMANDS: [&str; 11] = [ +const MISC_COMMANDS: [&str; 12] = [ "getqr", "getqrsvg", "getbadqr", "checkqr", "joinqr", + "createqrsvg", "fileinfo", "clear", "exit", diff --git a/src/qr_code_generator.rs b/src/qr_code_generator.rs index fc747aba2..043b61407 100644 --- a/src/qr_code_generator.rs +++ b/src/qr_code_generator.rs @@ -14,6 +14,65 @@ use crate::qr::{self, Qr}; use crate::securejoin; use crate::stock_str::{self, backup_transfer_qr}; +/// Create a QR code from any input data. +pub fn create_qr_svg(qrcode_content: &str) -> Result { + let all_size = 512.0; + let qr_code_size = 416.0; + + let qr = QrCode::encode_text(qrcode_content, QrCodeEcc::Medium)?; + let mut svg = String::with_capacity(28000); + let mut w = tagger::new(&mut svg); + + w.elem("svg", |d| { + d.attr("xmlns", "http://www.w3.org/2000/svg")?; + d.attr("viewBox", format_args!("0 0 {all_size} {all_size}"))?; + d.attr("xmlns:xlink", "http://www.w3.org/1999/xlink")?; // required for enabling xlink:href on browsers + Ok(()) + })? + .build(|w| { + // background + w.single("rect", |d| { + d.attr("x", 0)?; + d.attr("y", 0)?; + d.attr("width", all_size)?; + d.attr("height", all_size)?; + d.attr("style", "fill:#ffffff")?; + Ok(()) + })?; + // QR code + w.elem("g", |d| { + d.attr( + "transform", + format!( + "translate({},{})", + (all_size - qr_code_size) / 2.0, + ((all_size - qr_code_size) / 2.0) + ), + ) + })? + .build(|w| { + w.single("path", |d| { + let mut path_data = String::with_capacity(0); + let scale = qr_code_size / qr.size() as f32; + + for y in 0..qr.size() { + for x in 0..qr.size() { + if qr.get_module(x, y) { + path_data += &format!("M{x},{y}h1v1h-1z"); + } + } + } + + d.attr("style", "fill:#000000")?; + d.attr("d", path_data)?; + d.attr("transform", format!("scale({scale})")) + }) + }) + })?; + + Ok(svg) +} + /// Returns SVG of the QR code to join the group or verify contact. /// /// If `chat_id` is `None`, returns verification QR code. @@ -304,6 +363,14 @@ mod tests { use super::*; + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_create_qr_svg() -> Result<()> { + let svg = create_qr_svg("this is a test QR code \" < > &")?; + assert!(svg.contains("")); + Ok(()) + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_svg_escaping() { let svg = inner_generate_secure_join_qr_code(