diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 61717a32d0..fa87e03298 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 6559ab663c..aade1d4f2a 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 1c6fc9ecdb..dd18839849 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 213c283175..2b16cc68fb 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 fc747aba20..043b614072 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(