diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss index 1d8845b76..e2c2dd4b9 100644 --- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss +++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss @@ -355,3 +355,11 @@ $_gdm_dialog_width: 25em; } } } + +// QR Code +.qr-code { + background: black; + border-radius: $base_border_radius * .5; + border-color: white; + border-width: 1em; +} diff --git a/meson.build b/meson.build index a6d83b101..b5b7f108c 100644 --- a/meson.build +++ b/meson.build @@ -34,6 +34,7 @@ systemd_req = '>= 246' ibus_req = '>= 1.5.19' gnome_desktop_req = '>= 40' pipewire_req = '>= 0.3.49' +qrencode_req = '>= 4.1.1' nm_req = '>= 1.10.4' secret_req = '>= 0.18' @@ -93,6 +94,7 @@ ibus_dep = dependency('ibus-1.0', version: ibus_req) x11_dep = dependency('x11') schemas_dep = dependency('gsettings-desktop-schemas', version: schemas_req) gnome_desktop_dep = dependency('gnome-desktop-4', version: gnome_desktop_req) +qrencode_dep = dependency('libqrencode', version: qrencode_req) nm_deps = [] if get_option('networkmanager') diff --git a/src/meson.build b/src/meson.build index 422a68d8d..f29ae197c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -73,6 +73,7 @@ gnome_shell_deps = [ gcr_dep, libsystemd_dep, libpipewire_dep, + qrencode_dep ] gnome_shell_deps += nm_deps @@ -156,6 +157,8 @@ libshell_sources = [ 'shell-perf-log.c', 'shell-polkit-authentication-agent.c', 'shell-polkit-authentication-agent.h', + 'shell-qr-code-generator.c', + 'shell-qr-code-generator.h', 'shell-screenshot.c', 'shell-secure-text-buffer.c', 'shell-secure-text-buffer.h', diff --git a/src/shell-qr-code-generator.c b/src/shell-qr-code-generator.c new file mode 100644 index 000000000..39c1b7a60 --- /dev/null +++ b/src/shell-qr-code-generator.c @@ -0,0 +1,275 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include +#include +#include +#include +#include +#include + +#include + +#include "shell-qr-code-generator.h" + +#define BYTES_PER_RGB_888 3 + +typedef struct _ShellQrCodeGeneratorPrivate ShellQrCodeGeneratorPrivate; + +struct _ShellQrCodeGenerator +{ + GObject parent_instance; + + ShellQrCodeGeneratorPrivate *priv; +}; + +struct _ShellQrCodeGeneratorPrivate +{ + char *url; + size_t width; + size_t height; + GTask *image_task; + GTask *icon_task; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellQrCodeGenerator, shell_qr_code_generator, G_TYPE_OBJECT); + +static void +shell_qr_code_generator_dispose (GObject *object) +{ + ShellQrCodeGenerator *self = SHELL_QR_CODE_GENERATOR (object); + g_clear_pointer (&self->priv->url, g_free); +} + +static void +shell_qr_code_generator_class_init (ShellQrCodeGeneratorClass *qr_code_generator_class) +{ + GObjectClass *gobject_class = (GObjectClass *) qr_code_generator_class; + gobject_class->dispose = shell_qr_code_generator_dispose; +} + +static void +shell_qr_code_generator_init (ShellQrCodeGenerator *qr_code_generator) +{ + qr_code_generator->priv = shell_qr_code_generator_get_instance_private (qr_code_generator); +} + +static guint8 * +generate_icon (const char *url, + size_t width, + size_t height, + GError **error) +{ + QRcode *qrcode; + g_autofree guint8 *pixel_data = NULL; + guint8 white_pixel[BYTES_PER_RGB_888] = {255, 255, 255}; + guint8 black_pixel[BYTES_PER_RGB_888] = {0, 0, 0}; + size_t pixel_size = sizeof (white_pixel); + size_t symbol_size; + size_t symbols_per_row, number_of_rows; + size_t code_width; + size_t code_height; + size_t offset_x; + size_t offset_y; + size_t row, symbol, symbol_x, symbol_y; + + qrcode = QRcode_encodeString (url, 1, QR_ECLEVEL_L, QR_MODE_8, 1); + + if (!qrcode) + { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "QRCode generation failed for url %s", + url); + return NULL; + } + + symbols_per_row = qrcode->width; + number_of_rows = qrcode->width; + + symbol_size = MIN (width, height) / symbols_per_row; + code_width = symbol_size * symbols_per_row; + code_height = symbol_size * number_of_rows; + offset_x = (width - code_width) / 2; + offset_y = (height - code_height) / 2; + + pixel_data = calloc (height, width * BYTES_PER_RGB_888); + + for (row = 0; row < number_of_rows; row++) + { + for (symbol = 0; symbol < symbols_per_row; symbol++) + { + guint8 *pixel; + + if (qrcode->data[row * symbols_per_row + symbol] & 1) + pixel = black_pixel; + else + pixel = white_pixel; + + for (symbol_y = 0; symbol_y < symbol_size; symbol_y++) + { + for (symbol_x = 0; symbol_x < symbol_size; symbol_x++) + { + size_t x, y; + y = offset_y + (row * symbol_size) + symbol_y; + x = offset_x + (symbol * symbol_size) + symbol_x; + + memcpy (&pixel_data[(y * width + x) * pixel_size], + pixel, + pixel_size); + } + } + } + } + + return g_steal_pointer (&pixel_data); +} + +static void +qr_code_generator_thread (GTask *task, + gpointer source, + gpointer task_data, + GCancellable *cancellable) +{ + GError *error = NULL; + ShellQrCodeGenerator *self = task_data; + g_autofree guint8 *pixel_data = NULL; + + pixel_data = generate_icon (self->priv->url, self->priv->width, self->priv->height, &error); + + if (error != NULL) + g_task_return_error (task, error); + else + g_task_return_pointer (task, g_steal_pointer (&pixel_data), NULL); +} + +static void +on_image_task_complete (ShellQrCodeGenerator *self, + GAsyncResult *result, + gpointer user_data) +{ + guint8 *pixel_data; + g_autoptr (ClutterContent) content = NULL; + g_autoptr (GError) error = NULL; + gboolean data_set; + + pixel_data = g_task_propagate_pointer (G_TASK (result), &error); + + if (error != NULL) + { + g_task_return_error (self->priv->icon_task, error); + return; + } + + content = st_image_content_new_with_preferred_size (self->priv->width, self->priv->height); + data_set = clutter_image_set_data (CLUTTER_IMAGE (content), + pixel_data, + COGL_PIXEL_FORMAT_RGB_888, + self->priv->width, + self->priv->height, + self->priv->width * BYTES_PER_RGB_888, + &error); + + if (!data_set) + { + g_task_return_error (self->priv->icon_task, error); + return; + } + + g_task_return_pointer (self->priv->icon_task, g_steal_pointer (&content), g_object_unref); + g_clear_object (&self->priv->image_task); +} + +/** + * shell_qr_code_generator_generator_qr_code: + * @qr_code_generator: the #ShellQrCodeGenerator + * @stream: The stream for the QR code generator + * @callback: (scope async): function to call returning success or failure + * of the async grabbing + * @user_data: the data to pass to callback function + * + */ +void +shell_qr_code_generator_generate_qr_code (ShellQrCodeGenerator *self, + const char *url, + size_t width, + size_t height, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ShellQrCodeGeneratorPrivate *priv; + + g_return_if_fail (SHELL_IS_QR_CODE_GENERATOR (self)); + + priv = self->priv; + + if (!url || *url == '\0') + { + if (callback) + g_task_report_new_error (self, + callback, + user_data, + shell_qr_code_generator_generate_qr_code, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "No valid QR code URI provided"); + return; + } + + if (priv->url != NULL) + { + if (callback) + g_task_report_new_error (self, + callback, + user_data, + shell_qr_code_generator_generate_qr_code, + G_IO_ERROR, + G_IO_ERROR_PENDING, + "Only one QR code generator operation at a time " + "is permitted"); + return; + } + + priv->url = g_strdup (url); + priv->width = width; + priv->height = height; + + priv->icon_task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (priv->icon_task, shell_qr_code_generator_generate_qr_code); + g_task_set_task_data (priv->icon_task, self, NULL); + + priv->image_task = g_task_new (self, NULL, (GAsyncReadyCallback) on_image_task_complete, NULL); + g_task_set_source_tag (priv->image_task, on_image_task_complete); + g_task_set_task_data (priv->image_task, self, NULL); + g_task_run_in_thread (priv->image_task, qr_code_generator_thread); +} + +/** + * shell_qr_code_generator_generate_qr_code_finish: + * @result: the #GAsyncResult that was provided to the callback + * @error: #GError for error reporting + * + * Finish the asynchronous operation started by shell_qr_code_generator_generate_qr_code() + * and obtain its result. + * + * Returns: (transfer full): a #GIcon of the QR code + * + */ +GIcon * +shell_qr_code_generator_generate_qr_code_finish (ShellQrCodeGenerator *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (SHELL_IS_QR_CODE_GENERATOR (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (g_async_result_is_tagged (result, + shell_qr_code_generator_generate_qr_code), + NULL); + return g_task_propagate_pointer (G_TASK (result), error); +} + +ShellQrCodeGenerator * +shell_qr_code_generator_new (void) +{ + return g_object_new (SHELL_TYPE_QR_CODE_GENERATOR, NULL); +} diff --git a/src/shell-qr-code-generator.h b/src/shell-qr-code-generator.h new file mode 100644 index 000000000..a12ce22bc --- /dev/null +++ b/src/shell-qr-code-generator.h @@ -0,0 +1,28 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_QR_CODE_GENERATOR_H__ +#define __SHELL_QR_CODE_GENERATOR_H__ + +/** + * SECTION:shell-qr-code-generator + * @short_description: Generates a QR code for a given url. + * + * The #ShellQrCodeGenerator object is used to generate QR codes for URLs + * + */ +#define SHELL_TYPE_QR_CODE_GENERATOR (shell_qr_code_generator_get_type ()) +G_DECLARE_FINAL_TYPE (ShellQrCodeGenerator, shell_qr_code_generator, + SHELL, QR_CODE_GENERATOR, GObject) + +ShellQrCodeGenerator *shell_qr_code_generator_new (void); + +void shell_qr_code_generator_generate_qr_code (ShellQrCodeGenerator *qr_code_generator, + const char *url, + size_t width, + size_t height, + GAsyncReadyCallback callback, + gpointer user_data); +GIcon *shell_qr_code_generator_generate_qr_code_finish (ShellQrCodeGenerator *qr_code_generator, + GAsyncResult *result, + GError **error); + +#endif /* ___SHELL_QR_CODE_GENERATOR_H__ */