diff options
Diffstat (limited to 'src/gs-screenshot-image.c')
-rw-r--r-- | src/gs-screenshot-image.c | 592 |
1 files changed, 592 insertions, 0 deletions
diff --git a/src/gs-screenshot-image.c b/src/gs-screenshot-image.c new file mode 100644 index 0000000..0ecba07 --- /dev/null +++ b/src/gs-screenshot-image.c @@ -0,0 +1,592 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> + +#include "gs-screenshot-image.h" +#include "gs-common.h" + +struct _GsScreenshotImage +{ + GtkBin parent_instance; + + AsScreenshot *screenshot; + GtkWidget *stack; + GtkWidget *box_error; + GtkWidget *image1; + GtkWidget *image2; + GtkWidget *label_error; + GSettings *settings; + SoupSession *session; + SoupMessage *message; + gchar *filename; + const gchar *current_image; + guint width; + guint height; + guint scale; + gboolean showing_image; +}; + +G_DEFINE_TYPE (GsScreenshotImage, gs_screenshot_image, GTK_TYPE_BIN) + +AsScreenshot * +gs_screenshot_image_get_screenshot (GsScreenshotImage *ssimg) +{ + g_return_val_if_fail (GS_IS_SCREENSHOT_IMAGE (ssimg), NULL); + return ssimg->screenshot; +} + +static void +gs_screenshot_image_set_error (GsScreenshotImage *ssimg, const gchar *message) +{ + gint width, height; + + gtk_stack_set_visible_child_name (GTK_STACK (ssimg->stack), "error"); + gtk_label_set_label (GTK_LABEL (ssimg->label_error), message); + gtk_widget_get_size_request (ssimg->stack, &width, &height); + if (width < 200) + gtk_widget_hide (ssimg->label_error); + else + gtk_widget_show (ssimg->label_error); + ssimg->showing_image = FALSE; +} + +static void +as_screenshot_show_image (GsScreenshotImage *ssimg) +{ + g_autoptr(GdkPixbuf) pixbuf = NULL; + + /* no need to composite */ + if (ssimg->width == G_MAXUINT || ssimg->height == G_MAXUINT) { + pixbuf = gdk_pixbuf_new_from_file (ssimg->filename, NULL); + } else { + /* this is always going to have alpha */ + pixbuf = gdk_pixbuf_new_from_file_at_scale (ssimg->filename, + (gint) (ssimg->width * ssimg->scale), + (gint) (ssimg->height * ssimg->scale), + FALSE, NULL); + } + + /* show icon */ + if (g_strcmp0 (ssimg->current_image, "image1") == 0) { + if (pixbuf != NULL) { + gs_image_set_from_pixbuf_with_scale (GTK_IMAGE (ssimg->image2), + pixbuf, (gint) ssimg->scale); + } + gtk_stack_set_visible_child_name (GTK_STACK (ssimg->stack), "image2"); + ssimg->current_image = "image2"; + } else { + if (pixbuf != NULL) { + gs_image_set_from_pixbuf_with_scale (GTK_IMAGE (ssimg->image1), + pixbuf, (gint) ssimg->scale); + } + gtk_stack_set_visible_child_name (GTK_STACK (ssimg->stack), "image1"); + ssimg->current_image = "image1"; + } + + gtk_widget_show (GTK_WIDGET (ssimg)); + ssimg->showing_image = TRUE; +} + +static void +gs_screenshot_image_show_blurred (GsScreenshotImage *ssimg, + const gchar *filename_thumb) +{ + g_autoptr(AsImage) im = NULL; + g_autoptr(GdkPixbuf) pb = NULL; + + /* create an helper which can do the blurring for us */ + im = as_image_new (); + if (!as_image_load_filename (im, filename_thumb, NULL)) + return; + pb = as_image_save_pixbuf (im, + ssimg->width * ssimg->scale, + ssimg->height * ssimg->scale, + AS_IMAGE_SAVE_FLAG_BLUR); + if (pb == NULL) + return; + + if (g_strcmp0 (ssimg->current_image, "image1") == 0) { + gs_image_set_from_pixbuf_with_scale (GTK_IMAGE (ssimg->image1), + pb, (gint) ssimg->scale); + } else { + gs_image_set_from_pixbuf_with_scale (GTK_IMAGE (ssimg->image2), + pb, (gint) ssimg->scale); + } +} + +static gboolean +gs_screenshot_image_save_downloaded_img (GsScreenshotImage *ssimg, + GdkPixbuf *pixbuf, + GError **error) +{ + g_autoptr(AsImage) im = NULL; + gboolean ret; + const GPtrArray *images; + g_autoptr(GError) error_local = NULL; + g_autofree char *filename = NULL; + g_autofree char *size_dir = NULL; + g_autofree char *cache_kind = NULL; + g_autofree char *basename = NULL; + guint width = ssimg->width; + guint height = ssimg->height; + + /* save to file, using the same code as the AppStream builder + * so the preview looks the same */ + im = as_image_new (); + as_image_set_pixbuf (im, pixbuf); + ret = as_image_save_filename (im, ssimg->filename, + ssimg->width * ssimg->scale, + ssimg->height * ssimg->scale, + AS_IMAGE_SAVE_FLAG_PAD_16_9, + error); + + if (!ret) + return FALSE; + + if (ssimg->screenshot == NULL) + return TRUE; + + images = as_screenshot_get_images (ssimg->screenshot); + if (images->len > 1) + return TRUE; + + if (width == AS_IMAGE_THUMBNAIL_WIDTH && + height == AS_IMAGE_THUMBNAIL_HEIGHT) { + width = AS_IMAGE_NORMAL_WIDTH; + height = AS_IMAGE_NORMAL_HEIGHT; + } else { + width = AS_IMAGE_THUMBNAIL_WIDTH; + height = AS_IMAGE_THUMBNAIL_HEIGHT; + } + + width *= ssimg->scale; + height *= ssimg->scale; + basename = g_path_get_basename (ssimg->filename); + size_dir = g_strdup_printf ("%ux%u", width, height); + cache_kind = g_build_filename ("screenshots", size_dir, NULL); + filename = gs_utils_get_cache_filename (cache_kind, basename, + GS_UTILS_CACHE_FLAG_WRITEABLE, + &error_local); + + if (filename == NULL) { + /* if we cannot get a cache filename, warn about that but do not + * set a user's visible error because this is a complementary + * operation */ + g_warning ("Failed to get cache filename for counterpart " + "screenshot '%s' in folder '%s': %s", basename, + cache_kind, error_local->message); + return TRUE; + } + + ret = as_image_save_filename (im, filename, width, height, + AS_IMAGE_SAVE_FLAG_PAD_16_9, + &error_local); + + if (!ret) { + /* if we cannot save this screenshot, warn about that but do not + * set a user's visible error because this is a complementary + * operation */ + g_warning ("Failed to save screenshot '%s': %s", filename, + error_local->message); + } + + return TRUE; +} + +static void +gs_screenshot_image_complete_cb (SoupSession *session, + SoupMessage *msg, + gpointer user_data) +{ + g_autoptr(GsScreenshotImage) ssimg = GS_SCREENSHOT_IMAGE (user_data); + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GdkPixbuf) pixbuf = NULL; + g_autoptr(GInputStream) stream = NULL; + + /* return immediately if the message was cancelled or if we're in destruction */ + if (msg->status_code == SOUP_STATUS_CANCELLED || ssimg->session == NULL) + return; + + if (msg->status_code == SOUP_STATUS_NOT_MODIFIED) { + g_debug ("screenshot has not been modified"); + as_screenshot_show_image (ssimg); + return; + } + if (msg->status_code != SOUP_STATUS_OK) { + g_warning ("Result of screenshot downloading attempt with " + "status code '%u': %s", msg->status_code, + msg->reason_phrase); + /* if we're already showing an image, then don't set the error + * as having an image (even if outdated) is better */ + if (ssimg->showing_image) + return; + /* TRANSLATORS: this is when we try to download a screenshot and + * we get back 404 */ + gs_screenshot_image_set_error (ssimg, _("Screenshot not found")); + return; + } + + /* create a buffer with the data */ + stream = g_memory_input_stream_new_from_data (msg->response_body->data, + msg->response_body->length, + NULL); + if (stream == NULL) + return; + + /* load the image */ + pixbuf = gdk_pixbuf_new_from_stream (stream, NULL, NULL); + if (pixbuf == NULL) { + /* TRANSLATORS: possibly image file corrupt or not an image */ + gs_screenshot_image_set_error (ssimg, _("Failed to load image")); + return; + } + + /* is image size destination size unknown or exactly the correct size */ + if (ssimg->width == G_MAXUINT || ssimg->height == G_MAXUINT || + (ssimg->width * ssimg->scale == (guint) gdk_pixbuf_get_width (pixbuf) && + ssimg->height * ssimg->scale == (guint) gdk_pixbuf_get_height (pixbuf))) { + ret = g_file_set_contents (ssimg->filename, + msg->response_body->data, + msg->response_body->length, + &error); + if (!ret) { + gs_screenshot_image_set_error (ssimg, error->message); + return; + } + } else if (!gs_screenshot_image_save_downloaded_img (ssimg, pixbuf, + &error)) { + gs_screenshot_image_set_error (ssimg, error->message); + return; + } + + /* got image, so show */ + as_screenshot_show_image (ssimg); +} + +void +gs_screenshot_image_set_screenshot (GsScreenshotImage *ssimg, + AsScreenshot *screenshot) +{ + g_return_if_fail (GS_IS_SCREENSHOT_IMAGE (ssimg)); + g_return_if_fail (AS_IS_SCREENSHOT (screenshot)); + + if (ssimg->screenshot == screenshot) + return; + if (ssimg->screenshot) + g_object_unref (ssimg->screenshot); + ssimg->screenshot = g_object_ref (screenshot); + + /* we reset this flag here too because it referred to the previous + * screenshot, and thus avoids potentially assuming that the new + * screenshot is shown when it is the previous one instead */ + ssimg->showing_image = FALSE; +} + +void +gs_screenshot_image_set_size (GsScreenshotImage *ssimg, + guint width, guint height) +{ + g_return_if_fail (GS_IS_SCREENSHOT_IMAGE (ssimg)); + g_return_if_fail (width != 0); + g_return_if_fail (height != 0); + + ssimg->width = width; + ssimg->height = height; + gtk_widget_set_size_request (ssimg->stack, (gint) width, (gint) height); +} + +static gchar * +gs_screenshot_get_cachefn_for_url (const gchar *url) +{ + g_autofree gchar *basename = NULL; + g_autofree gchar *checksum = NULL; + checksum = g_compute_checksum_for_string (G_CHECKSUM_SHA256, url, -1); + basename = g_path_get_basename (url); + return g_strdup_printf ("%s-%s", checksum, basename); +} + +static void +gs_screenshot_soup_msg_set_modified_request (SoupMessage *msg, GFile *file) +{ +#ifndef GLIB_VERSION_2_62 + GTimeVal time_val; +#endif + g_autoptr(GDateTime) date_time = NULL; + g_autoptr(GFileInfo) info = NULL; + g_autofree gchar *mod_date = NULL; + + info = g_file_query_info (file, + G_FILE_ATTRIBUTE_TIME_MODIFIED, + G_FILE_QUERY_INFO_NONE, + NULL, + NULL); + if (info == NULL) + return; +#ifdef GLIB_VERSION_2_62 + date_time = g_file_info_get_modification_date_time (info); +#else + g_file_info_get_modification_time (info, &time_val); + date_time = g_date_time_new_from_timeval_local (&time_val); +#endif + mod_date = g_date_time_format (date_time, "%a, %d %b %Y %H:%M:%S %Z"); + soup_message_headers_append (msg->request_headers, + "If-Modified-Since", + mod_date); +} + +void +gs_screenshot_image_load_async (GsScreenshotImage *ssimg, + GCancellable *cancellable) +{ + AsImage *im = NULL; + const gchar *url; + g_autofree gchar *basename = NULL; + g_autofree gchar *cache_kind = NULL; + g_autofree gchar *cachefn_thumb = NULL; + g_autofree gchar *sizedir = NULL; + g_autoptr(SoupURI) base_uri = NULL; + + g_return_if_fail (GS_IS_SCREENSHOT_IMAGE (ssimg)); + + g_return_if_fail (AS_IS_SCREENSHOT (ssimg->screenshot)); + g_return_if_fail (ssimg->width != 0); + g_return_if_fail (ssimg->height != 0); + + /* load an image according to the scale factor */ + ssimg->scale = (guint) gtk_widget_get_scale_factor (GTK_WIDGET (ssimg)); + im = as_screenshot_get_image (ssimg->screenshot, + ssimg->width * ssimg->scale, + ssimg->height * ssimg->scale); + + /* if we've failed to load a HiDPI image, fallback to LoDPI */ + if (im == NULL && ssimg->scale > 1) { + ssimg->scale = 1; + im = as_screenshot_get_image (ssimg->screenshot, + ssimg->width, + ssimg->height); + } + if (im == NULL) { + /* TRANSLATORS: this is when we request a screenshot size that + * the generator did not create or the parser did not add */ + gs_screenshot_image_set_error (ssimg, _("Screenshot size not found")); + return; + } + + /* check if the URL points to a local file */ + url = as_image_get_url (im); + if (g_str_has_prefix (url, "file://")) { + g_free (ssimg->filename); + ssimg->filename = g_strdup (url + 7); + if (g_file_test (ssimg->filename, G_FILE_TEST_EXISTS)) { + as_screenshot_show_image (ssimg); + return; + } + } + + basename = gs_screenshot_get_cachefn_for_url (url); + if (ssimg->width == G_MAXUINT || ssimg->height == G_MAXUINT) { + sizedir = g_strdup ("unknown"); + } else { + sizedir = g_strdup_printf ("%ux%u", ssimg->width * ssimg->scale, ssimg->height * ssimg->scale); + } + cache_kind = g_build_filename ("screenshots", sizedir, NULL); + g_free (ssimg->filename); + ssimg->filename = gs_utils_get_cache_filename (cache_kind, + basename, + GS_UTILS_CACHE_FLAG_NONE, + NULL); + if (ssimg->filename == NULL) { + /* TRANSLATORS: this is when we try create the cache directory + * but we were out of space or permission was denied */ + gs_screenshot_image_set_error (ssimg, _("Could not create cache")); + return; + } + + /* does local file already exist and has recently been downloaded */ + if (g_file_test (ssimg->filename, G_FILE_TEST_EXISTS)) { + guint64 age_max; + g_autoptr(GFile) file = NULL; + + /* show the image we have in cache while we're checking for the + * new screenshot (which probably won't have changed) */ + as_screenshot_show_image (ssimg); + + /* verify the cache age against the maximum allowed */ + age_max = g_settings_get_uint (ssimg->settings, + "screenshot-cache-age-maximum"); + file = g_file_new_for_path (ssimg->filename); + /* image new enough, not re-requesting from server */ + if (age_max > 0 && gs_utils_get_file_age (file) < age_max) + return; + } + + /* if we're not showing a full-size image, we try loading a blurred + * smaller version of it straight away */ + if (!ssimg->showing_image && + ssimg->width > AS_IMAGE_THUMBNAIL_WIDTH && + ssimg->height > AS_IMAGE_THUMBNAIL_HEIGHT) { + const gchar *url_thumb; + g_autofree gchar *basename_thumb = NULL; + g_autofree gchar *cache_kind_thumb = NULL; + im = as_screenshot_get_image (ssimg->screenshot, + AS_IMAGE_THUMBNAIL_WIDTH * ssimg->scale, + AS_IMAGE_THUMBNAIL_HEIGHT * ssimg->scale); + url_thumb = as_image_get_url (im); + basename_thumb = gs_screenshot_get_cachefn_for_url (url_thumb); + cache_kind_thumb = g_build_filename ("screenshots", "112x63", NULL); + cachefn_thumb = gs_utils_get_cache_filename (cache_kind_thumb, + basename_thumb, + GS_UTILS_CACHE_FLAG_NONE, + NULL); + if (cachefn_thumb == NULL) + return; + if (g_file_test (cachefn_thumb, G_FILE_TEST_EXISTS)) + gs_screenshot_image_show_blurred (ssimg, cachefn_thumb); + } + + /* re-request the cache filename, which might be different as it needs + * to be writable this time */ + g_free (ssimg->filename); + ssimg->filename = gs_utils_get_cache_filename (cache_kind, + basename, + GS_UTILS_CACHE_FLAG_WRITEABLE, + NULL); + + /* download file */ + g_debug ("downloading %s to %s", url, ssimg->filename); + base_uri = soup_uri_new (url); + if (base_uri == NULL || !SOUP_URI_VALID_FOR_HTTP (base_uri)) { + /* TRANSLATORS: this is when we try to download a screenshot + * that was not a valid URL */ + gs_screenshot_image_set_error (ssimg, _("Screenshot not valid")); + return; + } + + /* cancel any previous messages */ + if (ssimg->message != NULL) { + soup_session_cancel_message (ssimg->session, + ssimg->message, + SOUP_STATUS_CANCELLED); + g_clear_object (&ssimg->message); + } + + ssimg->message = soup_message_new_from_uri (SOUP_METHOD_GET, base_uri); + if (ssimg->message == NULL) { + /* TRANSLATORS: this is when networking is not available */ + gs_screenshot_image_set_error (ssimg, _("Screenshot not available")); + return; + } + + /* not all servers support If-Modified-Since, but worst case we just + * re-download the entire file again every 30 days */ + if (g_file_test (ssimg->filename, G_FILE_TEST_EXISTS)) { + g_autoptr(GFile) file = g_file_new_for_path (ssimg->filename); + gs_screenshot_soup_msg_set_modified_request (ssimg->message, file); + } + + /* send async */ + soup_session_queue_message (ssimg->session, + g_object_ref (ssimg->message) /* transfer full */, + gs_screenshot_image_complete_cb, + g_object_ref (ssimg)); +} + +gboolean +gs_screenshot_image_is_showing (GsScreenshotImage *ssimg) +{ + return ssimg->showing_image; +} + +static void +gs_screenshot_image_destroy (GtkWidget *widget) +{ + GsScreenshotImage *ssimg = GS_SCREENSHOT_IMAGE (widget); + + if (ssimg->message != NULL) { + soup_session_cancel_message (ssimg->session, + ssimg->message, + SOUP_STATUS_CANCELLED); + g_clear_object (&ssimg->message); + } + g_clear_object (&ssimg->screenshot); + g_clear_object (&ssimg->session); + g_clear_object (&ssimg->settings); + + g_clear_pointer (&ssimg->filename, g_free); + + GTK_WIDGET_CLASS (gs_screenshot_image_parent_class)->destroy (widget); +} + +static void +gs_screenshot_image_init (GsScreenshotImage *ssimg) +{ + AtkObject *accessible; + + ssimg->settings = g_settings_new ("org.gnome.software"); + ssimg->showing_image = FALSE; + + gtk_widget_set_has_window (GTK_WIDGET (ssimg), FALSE); + gtk_widget_init_template (GTK_WIDGET (ssimg)); + + accessible = gtk_widget_get_accessible (GTK_WIDGET (ssimg)); + if (accessible != 0) { + atk_object_set_role (accessible, ATK_ROLE_IMAGE); + atk_object_set_name (accessible, _("Screenshot")); + } +} + +static gboolean +gs_screenshot_image_draw (GtkWidget *widget, cairo_t *cr) +{ + GtkStyleContext *context; + + context = gtk_widget_get_style_context (widget); + gtk_render_background (context, cr, + 0, 0, + gtk_widget_get_allocated_width (widget), + gtk_widget_get_allocated_height (widget)); + gtk_render_frame (context, cr, + 0, 0, + gtk_widget_get_allocated_width (widget), + gtk_widget_get_allocated_height (widget)); + + return GTK_WIDGET_CLASS (gs_screenshot_image_parent_class)->draw (widget, cr); +} + +static void +gs_screenshot_image_class_init (GsScreenshotImageClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + widget_class->destroy = gs_screenshot_image_destroy; + widget_class->draw = gs_screenshot_image_draw; + + gtk_widget_class_set_template_from_resource (widget_class, + "/org/gnome/Software/gs-screenshot-image.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsScreenshotImage, stack); + gtk_widget_class_bind_template_child (widget_class, GsScreenshotImage, image1); + gtk_widget_class_bind_template_child (widget_class, GsScreenshotImage, image2); + gtk_widget_class_bind_template_child (widget_class, GsScreenshotImage, box_error); + gtk_widget_class_bind_template_child (widget_class, GsScreenshotImage, label_error); +} + +GtkWidget * +gs_screenshot_image_new (SoupSession *session) +{ + GsScreenshotImage *ssimg; + ssimg = g_object_new (GS_TYPE_SCREENSHOT_IMAGE, NULL); + ssimg->session = g_object_ref (session); + return GTK_WIDGET (ssimg); +} |