diff options
Diffstat (limited to 'src/shell-screenshot.c')
-rw-r--r-- | src/shell-screenshot.c | 780 |
1 files changed, 780 insertions, 0 deletions
diff --git a/src/shell-screenshot.c b/src/shell-screenshot.c new file mode 100644 index 0000000..135ac55 --- /dev/null +++ b/src/shell-screenshot.c @@ -0,0 +1,780 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include <clutter/clutter.h> +#include <cogl/cogl.h> +#include <meta/display.h> +#include <meta/util.h> +#include <meta/meta-plugin.h> +#include <meta/meta-cursor-tracker.h> +#include <st/st.h> + +#include "shell-global.h" +#include "shell-screenshot.h" +#include "shell-util.h" + +typedef enum _ShellScreenshotFlag +{ + SHELL_SCREENSHOT_FLAG_NONE, + SHELL_SCREENSHOT_FLAG_INCLUDE_CURSOR, +} ShellScreenshotFlag; + +typedef enum _ShellScreenshotMode +{ + SHELL_SCREENSHOT_SCREEN, + SHELL_SCREENSHOT_WINDOW, + SHELL_SCREENSHOT_AREA, +} ShellScreenshotMode; + +typedef struct _ShellScreenshotPrivate ShellScreenshotPrivate; + +struct _ShellScreenshot +{ + GObject parent_instance; + + ShellScreenshotPrivate *priv; +}; + +struct _ShellScreenshotPrivate +{ + ShellGlobal *global; + + GOutputStream *stream; + ShellScreenshotFlag flags; + ShellScreenshotMode mode; + + GDateTime *datetime; + + cairo_surface_t *image; + cairo_rectangle_int_t screenshot_area; + + gboolean include_frame; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellScreenshot, shell_screenshot, G_TYPE_OBJECT); + +static void +shell_screenshot_class_init (ShellScreenshotClass *screenshot_class) +{ + (void) screenshot_class; +} + +static void +shell_screenshot_init (ShellScreenshot *screenshot) +{ + screenshot->priv = shell_screenshot_get_instance_private (screenshot); + screenshot->priv->global = shell_global_get (); +} + +static void +on_screenshot_written (GObject *source, + GAsyncResult *task, + gpointer user_data) +{ + ShellScreenshot *screenshot = SHELL_SCREENSHOT (source); + ShellScreenshotPrivate *priv = screenshot->priv; + GTask *result = user_data; + + g_task_return_boolean (result, g_task_propagate_boolean (G_TASK (task), NULL)); + g_object_unref (result); + + g_clear_pointer (&priv->image, cairo_surface_destroy); + g_clear_object (&priv->stream); + g_clear_pointer (&priv->datetime, g_date_time_unref); +} + +static void +write_screenshot_thread (GTask *result, + gpointer object, + gpointer task_data, + GCancellable *cancellable) +{ + ShellScreenshot *screenshot = SHELL_SCREENSHOT (object); + ShellScreenshotPrivate *priv; + g_autoptr (GOutputStream) stream = NULL; + g_autoptr(GdkPixbuf) pixbuf = NULL; + g_autofree char *creation_time = NULL; + GError *error = NULL; + + g_assert (screenshot != NULL); + + priv = screenshot->priv; + + stream = g_object_ref (priv->stream); + + pixbuf = gdk_pixbuf_get_from_surface (priv->image, + 0, 0, + cairo_image_surface_get_width (priv->image), + cairo_image_surface_get_height (priv->image)); + creation_time = g_date_time_format (priv->datetime, "%c"); + + if (!creation_time) + creation_time = g_date_time_format (priv->datetime, "%FT%T%z"); + + gdk_pixbuf_save_to_stream (pixbuf, stream, "png", NULL, &error, + "tEXt::Software", "gnome-screenshot", + "tEXt::Creation Time", creation_time, + NULL); + + if (error) + g_task_return_error (result, error); + else + g_task_return_boolean (result, TRUE); +} + +static void +do_grab_screenshot (ShellScreenshot *screenshot, + int x, + int y, + int width, + int height, + ShellScreenshotFlag flags) +{ + ShellScreenshotPrivate *priv = screenshot->priv; + ClutterStage *stage = shell_global_get_stage (priv->global); + cairo_rectangle_int_t screenshot_rect = { x, y, width, height }; + int image_width; + int image_height; + float scale; + cairo_surface_t *image; + ClutterPaintFlag paint_flags = CLUTTER_PAINT_FLAG_NONE; + g_autoptr (GError) error = NULL; + + clutter_stage_get_capture_final_size (stage, &screenshot_rect, + &image_width, + &image_height, + &scale); + image = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + image_width, image_height); + + if (flags & SHELL_SCREENSHOT_FLAG_INCLUDE_CURSOR) + paint_flags |= CLUTTER_PAINT_FLAG_FORCE_CURSORS; + else + paint_flags |= CLUTTER_PAINT_FLAG_NO_CURSORS; + if (!clutter_stage_paint_to_buffer (stage, &screenshot_rect, scale, + cairo_image_surface_get_data (image), + cairo_image_surface_get_stride (image), + CLUTTER_CAIRO_FORMAT_ARGB32, + paint_flags, + &error)) + { + cairo_surface_destroy (image); + g_warning ("Failed to take screenshot: %s", error->message); + return; + } + + priv->image = image; + + priv->datetime = g_date_time_new_now_local (); +} + +static void +draw_cursor_image (cairo_surface_t *surface, + cairo_rectangle_int_t area) +{ + CoglTexture *texture; + int width, height; + int stride; + guint8 *data; + MetaDisplay *display; + MetaCursorTracker *tracker; + cairo_surface_t *cursor_surface; + cairo_region_t *screenshot_region; + cairo_t *cr; + int x, y; + int xhot, yhot; + double xscale, yscale; + + display = shell_global_get_display (shell_global_get ()); + tracker = meta_cursor_tracker_get_for_display (display); + texture = meta_cursor_tracker_get_sprite (tracker); + + if (!texture) + return; + + screenshot_region = cairo_region_create_rectangle (&area); + meta_cursor_tracker_get_pointer (tracker, &x, &y, NULL); + + if (!cairo_region_contains_point (screenshot_region, x, y)) + { + cairo_region_destroy (screenshot_region); + return; + } + + meta_cursor_tracker_get_hot (tracker, &xhot, &yhot); + width = cogl_texture_get_width (texture); + height = cogl_texture_get_height (texture); + stride = 4 * width; + data = g_new (guint8, stride * height); + cogl_texture_get_data (texture, CLUTTER_CAIRO_FORMAT_ARGB32, stride, data); + + /* FIXME: cairo-gl? */ + cursor_surface = cairo_image_surface_create_for_data (data, + CAIRO_FORMAT_ARGB32, + width, height, + stride); + + cairo_surface_get_device_scale (surface, &xscale, &yscale); + + if (xscale != 1.0 || yscale != 1.0) + { + int monitor; + float monitor_scale; + MetaRectangle cursor_rect = { + .x = x, .y = y, .width = width, .height = height + }; + + monitor = meta_display_get_monitor_index_for_rect (display, &cursor_rect); + monitor_scale = meta_display_get_monitor_scale (display, monitor); + + cairo_surface_set_device_scale (cursor_surface, monitor_scale, monitor_scale); + } + + cr = cairo_create (surface); + cairo_set_source_surface (cr, + cursor_surface, + x - xhot - area.x, + y - yhot - area.y); + cairo_paint (cr); + + cairo_destroy (cr); + cairo_surface_destroy (cursor_surface); + cairo_region_destroy (screenshot_region); + g_free (data); +} + +static void +grab_screenshot (ShellScreenshot *screenshot, + ShellScreenshotFlag flags, + GTask *result) +{ + ShellScreenshotPrivate *priv = screenshot->priv; + MetaDisplay *display; + int width, height; + GTask *task; + + display = shell_global_get_display (priv->global); + meta_display_get_size (display, &width, &height); + + do_grab_screenshot (screenshot, + 0, 0, width, height, + flags); + + priv->screenshot_area.x = 0; + priv->screenshot_area.y = 0; + priv->screenshot_area.width = width; + priv->screenshot_area.height = height; + + task = g_task_new (screenshot, NULL, on_screenshot_written, result); + g_task_run_in_thread (task, write_screenshot_thread); + g_object_unref (task); +} + +static void +grab_window_screenshot (ShellScreenshot *screenshot, + ShellScreenshotFlag flags, + GTask *result) +{ + ShellScreenshotPrivate *priv = screenshot->priv; + GTask *task; + MetaDisplay *display = shell_global_get_display (priv->global); + MetaWindow *window = meta_display_get_focus_window (display); + ClutterActor *window_actor; + gfloat actor_x, actor_y; + MetaRectangle rect; + + window_actor = CLUTTER_ACTOR (meta_window_get_compositor_private (window)); + clutter_actor_get_position (window_actor, &actor_x, &actor_y); + + meta_window_get_frame_rect (window, &rect); + + if (!priv->include_frame) + meta_window_frame_rect_to_client_rect (window, &rect, &rect); + + priv->screenshot_area = rect; + + priv->image = meta_window_actor_get_image (META_WINDOW_ACTOR (window_actor), + NULL); + + if (!priv->image) + { + g_task_report_new_error (screenshot, on_screenshot_written, result, NULL, + G_IO_ERROR, G_IO_ERROR_FAILED, + "Capturing window failed"); + return; + } + + priv->datetime = g_date_time_new_now_local (); + + if (flags & SHELL_SCREENSHOT_FLAG_INCLUDE_CURSOR) + { + if (meta_window_get_client_type (window) == META_WINDOW_CLIENT_TYPE_WAYLAND) + { + float resource_scale; + resource_scale = clutter_actor_get_resource_scale (window_actor); + + cairo_surface_set_device_scale (priv->image, resource_scale, resource_scale); + } + + draw_cursor_image (priv->image, priv->screenshot_area); + } + + task = g_task_new (screenshot, NULL, on_screenshot_written, result); + g_task_run_in_thread (task, write_screenshot_thread); + g_object_unref (task); +} + +static gboolean +finish_screenshot (ShellScreenshot *screenshot, + GAsyncResult *result, + cairo_rectangle_int_t **area, + GError **error) +{ + ShellScreenshotPrivate *priv = screenshot->priv; + + if (!g_task_propagate_boolean (G_TASK (result), error)) + return FALSE; + + if (area) + *area = &priv->screenshot_area; + + return TRUE; +} + +static void +on_after_paint (ClutterStage *stage, + ClutterStageView *view, + GTask *result) +{ + ShellScreenshot *screenshot = g_task_get_task_data (result); + ShellScreenshotPrivate *priv = screenshot->priv; + MetaDisplay *display = shell_global_get_display (priv->global); + GTask *task; + + g_signal_handlers_disconnect_by_func (stage, on_after_paint, result); + + if (priv->mode == SHELL_SCREENSHOT_AREA) + { + do_grab_screenshot (screenshot, + priv->screenshot_area.x, + priv->screenshot_area.y, + priv->screenshot_area.width, + priv->screenshot_area.height, + priv->flags); + + task = g_task_new (screenshot, NULL, on_screenshot_written, result); + g_task_run_in_thread (task, write_screenshot_thread); + } + else + { + grab_screenshot (screenshot, priv->flags, result); + } + + meta_enable_unredirect_for_display (display); +} + +/** + * shell_screenshot_screenshot: + * @screenshot: the #ShellScreenshot + * @include_cursor: Whether to include the cursor or not + * @stream: The stream for the screenshot + * @callback: (scope async): function to call returning success or failure + * of the async grabbing + * @user_data: the data to pass to callback function + * + * Takes a screenshot of the whole screen + * in @stream as png image. + * + */ +void +shell_screenshot_screenshot (ShellScreenshot *screenshot, + gboolean include_cursor, + GOutputStream *stream, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ShellScreenshotPrivate *priv; + GTask *result; + ShellScreenshotFlag flags; + + g_return_if_fail (SHELL_IS_SCREENSHOT (screenshot)); + g_return_if_fail (G_IS_OUTPUT_STREAM (stream)); + + priv = screenshot->priv; + + if (priv->stream != NULL) { + if (callback) + g_task_report_new_error (screenshot, + callback, + user_data, + shell_screenshot_screenshot, + G_IO_ERROR, + G_IO_ERROR_PENDING, + "Only one screenshot operation at a time " + "is permitted"); + return; + } + + result = g_task_new (screenshot, NULL, callback, user_data); + g_task_set_source_tag (result, shell_screenshot_screenshot); + g_task_set_task_data (result, screenshot, NULL); + + priv->stream = g_object_ref (stream); + + flags = SHELL_SCREENSHOT_FLAG_NONE; + if (include_cursor) + flags |= SHELL_SCREENSHOT_FLAG_INCLUDE_CURSOR; + + if (meta_is_wayland_compositor ()) + { + grab_screenshot (screenshot, flags, result); + } + else + { + MetaDisplay *display = shell_global_get_display (priv->global); + ClutterStage *stage = shell_global_get_stage (priv->global); + + meta_disable_unredirect_for_display (display); + clutter_actor_queue_redraw (CLUTTER_ACTOR (stage)); + priv->flags = flags; + priv->mode = SHELL_SCREENSHOT_SCREEN; + g_signal_connect (stage, "after-paint", + G_CALLBACK (on_after_paint), result); + } +} + +/** + * shell_screenshot_screenshot_finish: + * @screenshot: the #ShellScreenshot + * @result: the #GAsyncResult that was provided to the callback + * @area: (out) (transfer none): the area that was grabbed in screen coordinates + * @error: #GError for error reporting + * + * Finish the asynchronous operation started by shell_screenshot_screenshot() + * and obtain its result. + * + * Returns: whether the operation was successful + * + */ +gboolean +shell_screenshot_screenshot_finish (ShellScreenshot *screenshot, + GAsyncResult *result, + cairo_rectangle_int_t **area, + GError **error) +{ + g_return_val_if_fail (SHELL_IS_SCREENSHOT (screenshot), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (g_async_result_is_tagged (result, + shell_screenshot_screenshot), + FALSE); + return finish_screenshot (screenshot, result, area, error); +} + +/** + * shell_screenshot_screenshot_area: + * @screenshot: the #ShellScreenshot + * @x: The X coordinate of the area + * @y: The Y coordinate of the area + * @width: The width of the area + * @height: The height of the area + * @stream: The stream for the screenshot + * @callback: (scope async): function to call returning success or failure + * of the async grabbing + * @user_data: the data to pass to callback function + * + * Takes a screenshot of the passed in area and saves it + * in @stream as png image. + * + */ +void +shell_screenshot_screenshot_area (ShellScreenshot *screenshot, + int x, + int y, + int width, + int height, + GOutputStream *stream, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ShellScreenshotPrivate *priv; + GTask *result; + g_autoptr (GTask) task = NULL; + + g_return_if_fail (SHELL_IS_SCREENSHOT (screenshot)); + g_return_if_fail (G_IS_OUTPUT_STREAM (stream)); + + priv = screenshot->priv; + + if (priv->stream != NULL) { + if (callback) + g_task_report_new_error (screenshot, + callback, + NULL, + shell_screenshot_screenshot_area, + G_IO_ERROR, + G_IO_ERROR_PENDING, + "Only one screenshot operation at a time " + "is permitted"); + return; + } + + result = g_task_new (screenshot, NULL, callback, user_data); + g_task_set_source_tag (result, shell_screenshot_screenshot_area); + g_task_set_task_data (result, screenshot, NULL); + + priv->stream = g_object_ref (stream); + priv->screenshot_area.x = x; + priv->screenshot_area.y = y; + priv->screenshot_area.width = width; + priv->screenshot_area.height = height; + + + if (meta_is_wayland_compositor ()) + { + do_grab_screenshot (screenshot, + priv->screenshot_area.x, + priv->screenshot_area.y, + priv->screenshot_area.width, + priv->screenshot_area.height, + SHELL_SCREENSHOT_FLAG_NONE); + + task = g_task_new (screenshot, NULL, on_screenshot_written, result); + g_task_run_in_thread (task, write_screenshot_thread); + } + else + { + MetaDisplay *display = shell_global_get_display (priv->global); + ClutterStage *stage = shell_global_get_stage (priv->global); + + meta_disable_unredirect_for_display (display); + clutter_actor_queue_redraw (CLUTTER_ACTOR (stage)); + priv->flags = SHELL_SCREENSHOT_FLAG_NONE; + priv->mode = SHELL_SCREENSHOT_AREA; + g_signal_connect (stage, "after-paint", + G_CALLBACK (on_after_paint), result); + } +} + +/** + * shell_screenshot_screenshot_area_finish: + * @screenshot: the #ShellScreenshot + * @result: the #GAsyncResult that was provided to the callback + * @area: (out) (transfer none): the area that was grabbed in screen coordinates + * @error: #GError for error reporting + * + * Finish the asynchronous operation started by shell_screenshot_screenshot_area() + * and obtain its result. + * + * Returns: whether the operation was successful + * + */ +gboolean +shell_screenshot_screenshot_area_finish (ShellScreenshot *screenshot, + GAsyncResult *result, + cairo_rectangle_int_t **area, + GError **error) +{ + g_return_val_if_fail (SHELL_IS_SCREENSHOT (screenshot), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (g_async_result_is_tagged (result, + shell_screenshot_screenshot_area), + FALSE); + return finish_screenshot (screenshot, result, area, error); +} + +/** + * shell_screenshot_screenshot_window: + * @screenshot: the #ShellScreenshot + * @include_frame: Whether to include the frame or not + * @include_cursor: Whether to include the cursor or not + * @stream: The stream for the screenshot + * @callback: (scope async): function to call returning success or failure + * of the async grabbing + * @user_data: the data to pass to callback function + * + * Takes a screenshot of the focused window (optionally omitting the frame) + * in @stream as png image. + * + */ +void +shell_screenshot_screenshot_window (ShellScreenshot *screenshot, + gboolean include_frame, + gboolean include_cursor, + GOutputStream *stream, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ShellScreenshotPrivate *priv; + MetaDisplay *display; + MetaWindow *window; + GTask *result; + + g_return_if_fail (SHELL_IS_SCREENSHOT (screenshot)); + g_return_if_fail (G_IS_OUTPUT_STREAM (stream)); + + priv = screenshot->priv; + display = shell_global_get_display (priv->global); + window = meta_display_get_focus_window (display); + + if (priv->stream != NULL || !window) { + if (callback) + g_task_report_new_error (screenshot, + callback, + NULL, + shell_screenshot_screenshot_window, + G_IO_ERROR, + G_IO_ERROR_PENDING, + "Only one screenshot operation at a time " + "is permitted"); + return; + } + + result = g_task_new (screenshot, NULL, callback, user_data); + g_task_set_source_tag (result, shell_screenshot_screenshot_window); + + priv->stream = g_object_ref (stream); + priv->include_frame = include_frame; + + grab_window_screenshot (screenshot, include_cursor, result); +} + +/** + * shell_screenshot_screenshot_window_finish: + * @screenshot: the #ShellScreenshot + * @result: the #GAsyncResult that was provided to the callback + * @area: (out) (transfer none): the area that was grabbed in screen coordinates + * @error: #GError for error reporting + * + * Finish the asynchronous operation started by shell_screenshot_screenshot_window() + * and obtain its result. + * + * Returns: whether the operation was successful + * + */ +gboolean +shell_screenshot_screenshot_window_finish (ShellScreenshot *screenshot, + GAsyncResult *result, + cairo_rectangle_int_t **area, + GError **error) +{ + g_return_val_if_fail (SHELL_IS_SCREENSHOT (screenshot), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (g_async_result_is_tagged (result, + shell_screenshot_screenshot_window), + FALSE); + return finish_screenshot (screenshot, result, area, error); +} + +/** + * shell_screenshot_pick_color: + * @screenshot: the #ShellScreenshot + * @x: The X coordinate to pick + * @y: The Y coordinate to pick + * @callback: (scope async): function to call returning success or failure + * of the async grabbing + * + * Picks the pixel at @x, @y and returns its color as #ClutterColor. + * + */ +void +shell_screenshot_pick_color (ShellScreenshot *screenshot, + int x, + int y, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ShellScreenshotPrivate *priv; + g_autoptr (GTask) result = NULL; + + g_return_if_fail (SHELL_IS_SCREENSHOT (screenshot)); + + result = g_task_new (screenshot, NULL, callback, user_data); + g_task_set_source_tag (result, shell_screenshot_pick_color); + + priv = screenshot->priv; + + priv->screenshot_area.x = x; + priv->screenshot_area.y = y; + priv->screenshot_area.width = 1; + priv->screenshot_area.height = 1; + + do_grab_screenshot (screenshot, + priv->screenshot_area.x, + priv->screenshot_area.y, + 1, + 1, + SHELL_SCREENSHOT_FLAG_NONE); + + g_task_return_boolean (result, TRUE); +} + +#if G_BYTE_ORDER == G_LITTLE_ENDIAN +#define INDEX_A 3 +#define INDEX_R 2 +#define INDEX_G 1 +#define INDEX_B 0 +#else +#define INDEX_A 0 +#define INDEX_R 1 +#define INDEX_G 2 +#define INDEX_B 3 +#endif + +/** + * shell_screenshot_pick_color_finish: + * @screenshot: the #ShellScreenshot + * @result: the #GAsyncResult that was provided to the callback + * @color: (out caller-allocates): the picked color + * @error: #GError for error reporting + * + * Finish the asynchronous operation started by shell_screenshot_pick_color() + * and obtain its result. + * + * Returns: whether the operation was successful + * + */ +gboolean +shell_screenshot_pick_color_finish (ShellScreenshot *screenshot, + GAsyncResult *result, + ClutterColor *color, + GError **error) +{ + ShellScreenshotPrivate *priv; + + g_return_val_if_fail (SHELL_IS_SCREENSHOT (screenshot), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (color != NULL, FALSE); + g_return_val_if_fail (g_async_result_is_tagged (result, + shell_screenshot_pick_color), + FALSE); + + if (!g_task_propagate_boolean (G_TASK (result), error)) + return FALSE; + + priv = screenshot->priv; + + /* protect against mutter changing the format used for stage captures */ + g_assert (cairo_image_surface_get_format (priv->image) == CAIRO_FORMAT_ARGB32); + + if (color) + { + uint8_t *data = cairo_image_surface_get_data (priv->image); + + color->alpha = data[INDEX_A]; + color->red = data[INDEX_R]; + color->green = data[INDEX_G]; + color->blue = data[INDEX_B]; + } + + return TRUE; +} + +#undef INDEX_A +#undef INDEX_R +#undef INDEX_G +#undef INDEX_B + +ShellScreenshot * +shell_screenshot_new (void) +{ + return g_object_new (SHELL_TYPE_SCREENSHOT, NULL); +} |