From fb5f322406068fb9e514d204f9ba999bc40e2c55 Mon Sep 17 00:00:00 2001 From: Claudio Saavedra Date: Tue, 24 Jan 2012 14:25:29 +0200 Subject: Add a service for snapshotting webpages This service provides pixbufs for URLs while caching these locally as thumbnails. gnome-desktop-thumbnail is used to handle caching. https://bugzilla.gnome.org/show_bug.cgi?id=668578 --- configure.ac | 4 + lib/Makefile.am | 2 + lib/ephy-snapshot-service.c | 456 ++++++++++++++++++++++++++++++++++++++++++++ lib/ephy-snapshot-service.h | 94 +++++++++ 4 files changed, 556 insertions(+) create mode 100644 lib/ephy-snapshot-service.c create mode 100644 lib/ephy-snapshot-service.h diff --git a/configure.ac b/configure.ac index 01335ed7b..10946e853 100644 --- a/configure.ac +++ b/configure.ac @@ -86,6 +86,9 @@ LIBXML_REQUIRED=2.6.12 LIBXSLT_REQUIRED=1.1.7 WEBKIT_GTK_REQUIRED=1.9.6 LIBSOUP_GNOME_REQUIRED=2.39.6 +WEBKIT_GTK_REQUIRED=1.9.5 +LIBSOUP_GNOME_REQUIRED=2.37.1 +GNOME_DESKTOP_REQUIRED=2.91.2 GNOME_KEYRING_REQUIRED=2.26.0 GSETTINGS_DESKTOP_SCHEMAS_REQUIRED=0.0.1 LIBNOTIFY_REQUIRED=0.5.1 @@ -126,6 +129,7 @@ PKG_CHECK_MODULES([DEPENDENCIES], [ libxslt >= $LIBXSLT_REQUIRED $WEBKIT_GTK_PC_NAME >= $WEBKIT_GTK_REQUIRED libsoup-gnome-2.4 >= $LIBSOUP_GNOME_REQUIRED + gnome-desktop-3.0 >= $GNOME_DESKTOP_REQUIRED gnome-keyring-1 >= $GNOME_KEYRING_REQUIRED gsettings-desktop-schemas >= $GSETTINGS_DESKTOP_SCHEMAS_REQUIRED libnotify >= $LIBNOTIFY_REQUIRED diff --git a/lib/Makefile.am b/lib/Makefile.am index ba15f2706..7c9401db5 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -28,6 +28,7 @@ NOINST_H_FILES = \ ephy-sqlite-connection.h \ ephy-sqlite-statement.h \ ephy-string.h \ + ephy-snapshot-service.h \ ephy-time-helpers.h \ ephy-web-app-utils.h \ ephy-zoom.h @@ -68,6 +69,7 @@ libephymisc_la_SOURCES = \ ephy-shlib-loader.c \ ephy-signal-accumulator.c \ ephy-smaps.c \ + ephy-snapshot-service.c \ ephy-sqlite-connection.c \ ephy-sqlite-statement.c \ ephy-state.c \ diff --git a/lib/ephy-snapshot-service.c b/lib/ephy-snapshot-service.c new file mode 100644 index 000000000..4b1e4e4db --- /dev/null +++ b/lib/ephy-snapshot-service.c @@ -0,0 +1,456 @@ +/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Copyright © 2012 Igalia S.L. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "config.h" +#include "ephy-snapshot-service.h" + +#ifndef GNOME_DESKTOP_USE_UNSTABLE_API +#define GNOME_DESKTOP_USE_UNSTABLE_API +#endif +#include +#ifdef HAVE_WEBKIT2 +#include +#else +#include +#endif + +#define EPHY_SNAPSHOT_SERVICE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), EPHY_TYPE_SNAPSHOT_SERVICE, EphySnapshotServicePrivate)) + +struct _EphySnapshotServicePrivate +{ + GnomeDesktopThumbnailFactory *factory; +}; + +G_DEFINE_TYPE (EphySnapshotService, ephy_snapshot_service, G_TYPE_OBJECT) + +typedef struct { + WebKitWebView *webview; + char *url; + time_t mtime; + GdkPixbuf *snapshot; + GCancellable *cancellable; + GAsyncReadyCallback callback; + gpointer user_data; +} SnapshotOp; + +static gboolean ephy_snapshot_service_complete_async (SnapshotOp *op); +static gboolean ephy_snapshot_service_take_from_webview (SnapshotOp *op); + +static void +snapshot_op_free (SnapshotOp *op) +{ + g_free (op->url); + + if (op->cancellable) + g_object_unref (op->cancellable); + if (op->webview) + g_object_unref (op->webview); + if (op->snapshot) + g_object_unref (op->snapshot); + + g_slice_free (SnapshotOp, op); +} + +/* GObject boilerplate methods. */ + +static void +ephy_snapshot_service_class_init (EphySnapshotServiceClass *klass) +{ + g_type_class_add_private (klass, sizeof (EphySnapshotServicePrivate)); +} + + +static void +ephy_snapshot_service_init (EphySnapshotService *self) +{ + + self->priv = EPHY_SNAPSHOT_SERVICE_GET_PRIVATE (self); + self->priv->factory = gnome_desktop_thumbnail_factory_new (GNOME_DESKTOP_THUMBNAIL_SIZE_LARGE); +} + +/* IO scheduler methods, used for IO. */ + +static GdkPixbuf * +io_scheduler_get_cached_snapshot (EphySnapshotService *service, + char *url, time_t mtime) +{ + GdkPixbuf *snapshot; + char *uri; + + uri = gnome_desktop_thumbnail_factory_lookup (service->priv->factory, + url, mtime); + if (uri == NULL) + return NULL; + + snapshot = gdk_pixbuf_new_from_file (uri, NULL); + g_free (uri); + + return snapshot; +} + +static gboolean +io_scheduler_try_cache_query (GIOSchedulerJob *job, + GCancellable *cancellable, + gpointer user_data) +{ + SnapshotOp *op; + EphySnapshotService *service = ephy_snapshot_service_get_default (); + GSourceFunc func; + + op = (SnapshotOp*) user_data; + + if (g_cancellable_is_cancelled (op->cancellable)) { + func = (GSourceFunc)ephy_snapshot_service_complete_async; + goto out; + } + + op->snapshot = io_scheduler_get_cached_snapshot (service, op->url, op->mtime); + + /* If we do have a cached snapshot or we don't have a webview + already, just complete early. */ + if (op->snapshot || !op->webview) + func = (GSourceFunc)ephy_snapshot_service_complete_async; + else + func = (GSourceFunc)ephy_snapshot_service_take_from_webview; + +out: + g_io_scheduler_job_send_to_mainloop (job, func, op, NULL); + return FALSE; +} + +static gboolean +io_scheduler_save_thumbnail (GIOSchedulerJob *job, + GCancellable *cancellable, + gpointer user_data) +{ + SnapshotOp *op; + EphySnapshotService *service; + + op = (SnapshotOp*) user_data; + service = ephy_snapshot_service_get_default (); + gnome_desktop_thumbnail_factory_save_thumbnail (service->priv->factory, + op->snapshot, + op->url, + op->mtime); + + g_io_scheduler_job_send_to_mainloop_async (job, + (GSourceFunc)ephy_snapshot_service_complete_async, + op, NULL); + + return FALSE; +} + +/* Methods that run in the mainloop. */ + +static gboolean +ephy_snapshot_service_complete_async (SnapshotOp *op) +{ + GSimpleAsyncResult *res; + + res = g_simple_async_result_new (G_OBJECT (ephy_snapshot_service_get_default ()), + op->callback, + op->user_data, + ephy_snapshot_service_complete_async); + g_simple_async_result_set_check_cancellable (res, op->cancellable); + g_simple_async_result_set_op_res_gpointer (res, op, (GDestroyNotify)snapshot_op_free); + g_simple_async_result_complete (res); + g_object_unref (res); + + return FALSE; +} + +static gboolean +webview_retrieve_snapshot (SnapshotOp *op) +{ + cairo_surface_t *surface; + +#ifdef HAVE_WEBKIT2 + /* FIXME: We need to add this API to WebKit2. */ + surface = NULL; +#else + surface = webkit_web_view_get_snapshot (WEBKIT_WEB_VIEW (op->webview)); +#endif + + if (surface == NULL) { + ephy_snapshot_service_complete_async (op); + return FALSE; + } + + op->snapshot = ephy_snapshot_service_crop_snapshot (surface); + + g_io_scheduler_push_job ((GIOSchedulerJobFunc)io_scheduler_save_thumbnail, + op, NULL, G_PRIORITY_LOW, NULL); + return FALSE; +} + +#ifdef HAVE_WEBKIT2 +static void +webview_load_changed_cb (WebKitWebView *webview, + WebKitLoadEvent load_event, + SnapshotOp *op) +{ + switch (load_event) { + case WEBKIT_LOAD_FINISHED: + /* Load finished doesn't ensure that we actually have visible content yet, + so hold a bit before retrieving the snapshot. */ + g_idle_add ((GSourceFunc) webview_retrieve_snapshot, op); + /* Some pages might end up causing this condition to happen twice, so remove + the handler in order to avoid calling the above idle function twice. */ + g_signal_handlers_disconnect_by_func (webview, webview_load_changed_cb, op); + break; + } +} + +static gboolean +webview_load_failed_cb (WebKitWebView *webview, + WebKitLoadEvent load_event, + const char failing_uri, + GError *error, + SnapshotOp *op) +{ + ephy_snapshot_service_complete_async (op); + + return FALSE; +} +#else +static void +webview_load_status_changed_cb (WebKitWebView *webview, + GParamSpec *pspec, + SnapshotOp *op) +{ + WebKitLoadStatus status; + + status = webkit_web_view_get_load_status (webview); + + switch (status) { + case WEBKIT_LOAD_FINISHED: + /* Load finished doesn't ensure that we actually have visible + content yet, so hold a bit before retrieving the snapshot. */ + g_idle_add ((GSourceFunc) webview_retrieve_snapshot, op); + g_signal_handlers_disconnect_by_func (webview, webview_load_status_changed_cb, op); + break; + case WEBKIT_LOAD_FAILED: + g_signal_handlers_disconnect_by_func (webview, webview_load_status_changed_cb, op); + ephy_snapshot_service_complete_async (op); + break; + default: + break; + } +} +#endif + +static gboolean +ephy_snapshot_service_take_from_webview (SnapshotOp *op) +{ + WebKitWebView *webview = op->webview; + + if (g_cancellable_is_cancelled (op->cancellable)) { + ephy_snapshot_service_complete_async (op); + return FALSE; + } + +#ifdef HAVE_WEBKIT2 + if (webkit_web_view_get_estimated_load_progress (WEBKIT_WEB_VIEW (webview))== 1.0) + webview_retrieve_snapshot (op); + else { + g_signal_connect (webview, "load-changed", + G_CALLBACK (webview_load_changed_cb), op); + g_signal_connect (webview, "load-failed", + G_CALLBACK (webview_load_failed_cb), op); + } +#else + if (webkit_web_view_get_load_status (webview) == WEBKIT_LOAD_FINISHED) + webview_retrieve_snapshot (op); + else + g_signal_connect (webview, "notify::load-status", + G_CALLBACK (webview_load_status_changed_cb), + op); +#endif + + return FALSE; +} + +/** + * ephy_snapshot_service_get_default: + * + * Gets the default instance of #EphySnapshotService. + * + * Returns: a #EphySnapshotService + **/ +EphySnapshotService * +ephy_snapshot_service_get_default (void) +{ + static EphySnapshotService *service = NULL; + + if (service == NULL) + service = g_object_new (EPHY_TYPE_SNAPSHOT_SERVICE, NULL); + + return service; +} + +/** + * ephy_snapshot_service_get_snapshot: + * @service: a #EphySnapshotService + * @url: the URL for which a snapshot is needed + * @mtime: @the last + * @callback: a #EphySnapshotServiceCallback + * @userdata: user data to pass to @callback + * + * Schedules a query for a snapshot of @url. If there is an up-to-date + * snapshot in the cache, this will be retrieved. Otherwise, this + * the snapshot will be taken, cached, and retrieved. + * + **/ +void +ephy_snapshot_service_get_snapshot_async (EphySnapshotService *service, + WebKitWebView *webview, + const char *url, + const time_t mtime, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + SnapshotOp *op; + + g_return_if_fail (EPHY_IS_SNAPSHOT_SERVICE (service)); + g_return_if_fail (url != NULL); + + op = g_slice_alloc0 (sizeof(SnapshotOp)); + op->url = g_strdup (url); + op->mtime = mtime; + op->webview = webview ? g_object_ref (webview) : NULL; + op->cancellable = cancellable ? g_object_ref (cancellable) : NULL; + op->callback = callback; + op->user_data = user_data; + + /* Query for the snapshot from the cache using the IO scheduler, so + that there is no UI blocking during the cache query. */ + g_io_scheduler_push_job (io_scheduler_try_cache_query, + op, NULL, G_PRIORITY_LOW, NULL); +} + +/** + * ephy_snapshot_service_get_snapshot_finish: + * @service: a #EphySnapshotService + * @result: a #GAsyncResult + * @error: a location to store a #GError or %NULL + * + * Finishes the retrieval of a snapshot. Call from the + * #GAsyncReadyCallback passed to + * ephy_snapshot_service_get_snapshot_async(). + * + * Returns: (transfer full): the snapshot. + **/ +GdkPixbuf * +ephy_snapshot_service_get_snapshot_finish (EphySnapshotService *service, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + SnapshotOp *op; + + g_return_val_if_fail (g_simple_async_result_is_valid (result, + G_OBJECT (service), + ephy_snapshot_service_complete_async), + NULL); + + simple = (GSimpleAsyncResult *) result; + + if (g_simple_async_result_propagate_error (simple, error)) + return NULL; + + op = g_simple_async_result_get_op_res_gpointer (simple); + + return op->snapshot ? g_object_ref (op->snapshot) : NULL; +} + +void +ephy_snapshot_service_save_snapshot_async (EphySnapshotService *service, + GdkPixbuf *snapshot, + const char *url, + time_t mtime, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + SnapshotOp *op; + + g_return_if_fail (EPHY_IS_SNAPSHOT_SERVICE (service)); + g_return_if_fail (GDK_IS_PIXBUF (snapshot)); + g_return_if_fail (url != NULL); + + op = g_slice_alloc0 (sizeof(SnapshotOp)); + op->snapshot = g_object_ref (snapshot); + op->url = g_strdup (url); + op->mtime = mtime; + op->cancellable = cancellable ? g_object_ref (cancellable) : NULL; + op->callback = callback; + op->user_data = user_data; + + g_io_scheduler_push_job (io_scheduler_save_thumbnail, + op, NULL, G_PRIORITY_LOW, NULL); +} + +GdkPixbuf * +ephy_snapshot_service_crop_snapshot (cairo_surface_t *surface) +{ + GdkPixbuf *snapshot, *scaled; + int orig_width, orig_height; + float orig_aspect_ratio, dest_aspect_ratio; + int x_offset, new_width = 0, new_height; + + orig_width = cairo_image_surface_get_width (surface); + orig_height = cairo_image_surface_get_height (surface); + + if (orig_width < EPHY_THUMBNAIL_WIDTH || + orig_height < EPHY_THUMBNAIL_HEIGHT) { + snapshot = gdk_pixbuf_get_from_surface (surface, + 0, 0, + orig_width, orig_height); + scaled = gdk_pixbuf_scale_simple (snapshot, + EPHY_THUMBNAIL_WIDTH, + EPHY_THUMBNAIL_HEIGHT, + GDK_INTERP_TILES); + } else { + orig_aspect_ratio = orig_width / (float)orig_height; + dest_aspect_ratio = EPHY_THUMBNAIL_WIDTH / (float)EPHY_THUMBNAIL_HEIGHT; + + if (orig_aspect_ratio > dest_aspect_ratio) { + /* Wider than taller, crop the sides. */ + new_width = orig_height * dest_aspect_ratio; + new_height = orig_height; + x_offset = (orig_width - new_width) / 2; + } else { + /* Crop the bottom otherwise. */ + new_width = orig_width; + new_height = orig_width / (float)dest_aspect_ratio; + x_offset = 0; + } + + snapshot = gdk_pixbuf_get_from_surface (surface, x_offset, 0, new_width, new_height); + scaled = gnome_desktop_thumbnail_scale_down_pixbuf (snapshot, + EPHY_THUMBNAIL_WIDTH, + EPHY_THUMBNAIL_HEIGHT); + } + + g_object_unref (snapshot); + + return scaled; +} diff --git a/lib/ephy-snapshot-service.h b/lib/ephy-snapshot-service.h new file mode 100644 index 000000000..4de9745aa --- /dev/null +++ b/lib/ephy-snapshot-service.h @@ -0,0 +1,94 @@ +/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Copyright © 2012 Igalia S.L. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef _EPHY_SNAPSHOT_SERVICE_H +#define _EPHY_SNAPSHOT_SERVICE_H + +#include +#ifdef HAVE_WEBKIT2 +#include +#else +#include +#endif + +#include + +G_BEGIN_DECLS + +#define EPHY_TYPE_SNAPSHOT_SERVICE (ephy_snapshot_service_get_type()) +#define EPHY_SNAPSHOT_SERVICE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), EPHY_TYPE_SNAPSHOT_SERVICE, EphySnapshotService)) +#define EPHY_SNAPSHOT_SERVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), EPHY_TYPE_SNAPSHOT_SERVICE, EphySnapshotServiceClass)) +#define EPHY_IS_SNAPSHOT_SERVICE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), EPHY_TYPE_SNAPSHOT_SERVICE)) +#define EPHY_IS_SNAPSHOT_SERVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), EPHY_TYPE_SNAPSHOT_SERVICE)) +#define EPHY_SNAPSHOT_SERVICE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), EPHY_TYPE_SNAPSHOT_SERVICE, EphySnapshotServiceClass)) + +typedef struct _EphySnapshotService EphySnapshotService; +typedef struct _EphySnapshotServiceClass EphySnapshotServiceClass; +typedef struct _EphySnapshotServicePrivate EphySnapshotServicePrivate; + +struct _EphySnapshotService +{ + GObject parent; + + /*< private >*/ + EphySnapshotServicePrivate *priv; +}; + +struct _EphySnapshotServiceClass +{ + GObjectClass parent_class; +}; + +/* Values taken from the Web mockups. */ +#define EPHY_THUMBNAIL_WIDTH 172 +#define EPHY_THUMBNAIL_HEIGHT 172 + +typedef void (* EphySnapshotServiceCallback) (GdkPixbuf *snapshot, + gpointer user_data); + +GType ephy_snapshot_service_get_type (void) G_GNUC_CONST; + +EphySnapshotService *ephy_snapshot_service_get_default (void); + +void ephy_snapshot_service_get_snapshot_async (EphySnapshotService *service, + WebKitWebView *webview, + const char *url, + const time_t mtime, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +GdkPixbuf * ephy_snapshot_service_get_snapshot_finish (EphySnapshotService *service, + GAsyncResult *result, + GError **error); + +void ephy_snapshot_service_save_snapshot_async (EphySnapshotService *service, + GdkPixbuf *snapshot, + const char *url, + time_t mtime, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +GdkPixbuf* ephy_snapshot_service_crop_snapshot (cairo_surface_t *surface); + +G_END_DECLS + +#endif /* _EPHY_SNAPSHOT_SERVICE_H */ -- cgit v1.2.1