summaryrefslogtreecommitdiffstats
path: root/src/gs-screenshot-image.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gs-screenshot-image.c')
-rw-r--r--src/gs-screenshot-image.c592
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);
+}