From f6eef228e247a2105538cef7a45d4cc76632d809 Mon Sep 17 00:00:00 2001 From: Brian Sobulefsky Date: Mon, 27 Sep 2021 08:29:50 -0700 Subject: [PATCH 1/8] Updated API in the same interface as the client.content property to support screenshots at the root window and screen levels. A call to the root.content() method will return a screenshot as a Cairo surface of the entire root window (generally all physical screens). Getting the screen.content property will return a screenshot as a Cairo surface of the screen object (generally a physical screen) just as client.content will for a client object. Sample usage - the traditional API supported focused client screenshot as: c = client.focus if c then gears.surface(c.content):write_to_png("/path/to/screenshot.png") end Similarly, this API extension adds: s = awful.screen.focused() if s then gears.surface(s.content):write_to_png("/path/to/screenshot.png") end for the screen class and: gears.surface(root.content()):write_to_png("/path/to/screenshot.png") for the root window. Note that the example shows how to get a screenshot of the focused screen, but this is not a limitation. A lua script could call it on any screen object. Signed off by Brian Sobulefsky --- objects/screen.c | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ root.c | 26 +++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/objects/screen.c b/objects/screen.c index 5fd2b501..79b036d9 100644 --- a/objects/screen.c +++ b/objects/screen.c @@ -59,6 +59,9 @@ #include #include +#include +#include + /* The XID that is used on fake screens. X11 guarantees that the top three bits * of a valid XID are zero, so this will not clash with anything. */ @@ -1655,6 +1658,60 @@ luaA_screen_get_name(lua_State *L, screen_t *s) return 1; } +/** Get the content of the active screen as a cairo surface + * + * @return A cairo surface + * @staticfct content + */ +static int +luaA_screen_get_content(lua_State *L, screen_t *s) +{ + + cairo_surface_t *root_surface; + cairo_surface_t *scrn_surface; + cairo_t *scrn_cr; + + area_t *scrn_geom; + + int root_width = globalconf.screen->width_in_pixels; + int root_height = globalconf.screen->height_in_pixels; + + scrn_geom = &(s->geometry); + + root_surface = cairo_xcb_surface_create(globalconf.connection, + globalconf.screen->root, + globalconf.default_visual, + root_width, root_height); + + scrn_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, + scrn_geom->width, scrn_geom->height); + scrn_cr = cairo_create(scrn_surface); + + if(cairo_surface_status(root_surface) != CAIRO_STATUS_SUCCESS || + cairo_surface_status(scrn_surface) != CAIRO_STATUS_SUCCESS){ + cairo_surface_destroy(root_surface); + cairo_surface_destroy(scrn_surface); + cairo_destroy(scrn_cr); + return 0; + } + + cairo_set_source_surface(scrn_cr, root_surface, + -(scrn_geom->x), -(scrn_geom->y)); + cairo_rectangle(scrn_cr, 0, 0, scrn_geom->width, scrn_geom->height); + cairo_fill(scrn_cr); + + cairo_surface_destroy(root_surface); + cairo_destroy(scrn_cr); + + lua_pushlightuserdata(L, scrn_surface); + + return 1; + +} + + + + /** Get the number of screens. * * @return The screen count, at least 1. @@ -1902,6 +1959,10 @@ screen_class_setup(lua_State *L) (lua_class_propfunc_t) luaA_screen_set_name, (lua_class_propfunc_t) luaA_screen_get_name, (lua_class_propfunc_t) luaA_screen_set_name); + luaA_class_add_property(&screen_class, "content", + NULL, + (lua_class_propfunc_t) luaA_screen_get_content, + NULL); } /* @DOC_cobject_COMMON@ */ diff --git a/root.c b/root.c index 4b5ebfbe..822fa01c 100644 --- a/root.c +++ b/root.c @@ -500,6 +500,31 @@ luaA_root_wallpaper(lua_State *L) return 1; } + +/** Get the content of the root window as a cairo surface + * + * @return A cairo surface + * @staticfct content + */ +static int +luaA_root_get_content(lua_State *L) +{ + + cairo_surface_t *surface; + int width = globalconf.screen->width_in_pixels; + int height = globalconf.screen->height_in_pixels; + + surface = cairo_xcb_surface_create(globalconf.connection, + globalconf.screen->root, + globalconf.default_visual, + width, height); + + lua_pushlightuserdata(L, surface); + return 1; + +} + + /** Get the size of the root window. * * @return Width of the root window. @@ -608,6 +633,7 @@ const struct luaL_Reg awesome_root_methods[] = { "fake_input", luaA_root_fake_input }, { "drawins", luaA_root_drawins }, { "_wallpaper", luaA_root_wallpaper }, + { "content", luaA_root_get_content}, { "size", luaA_root_size }, { "size_mm", luaA_root_size_mm }, { "tags", luaA_root_tags }, From 5f58bc31951c9e82c31e6f54ceb47f6271c23d03 Mon Sep 17 00:00:00 2001 From: poisson-aerohead Date: Mon, 27 Sep 2021 16:02:49 -0700 Subject: [PATCH 2/8] Apply suggestions from code review Co-authored-by: Aire-One --- objects/screen.c | 9 ++++----- root.c | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/objects/screen.c b/objects/screen.c index 79b036d9..1d318112 100644 --- a/objects/screen.c +++ b/objects/screen.c @@ -1658,15 +1658,15 @@ luaA_screen_get_name(lua_State *L, screen_t *s) return 1; } -/** Get the content of the active screen as a cairo surface +/** Get the content of the active screen as a cairo surface. * - * @return A cairo surface - * @staticfct content + * @property content + * @tparam surface A cairo surface with the screen content. + * @see gears.surface */ static int luaA_screen_get_content(lua_State *L, screen_t *s) { - cairo_surface_t *root_surface; cairo_surface_t *scrn_surface; cairo_t *scrn_cr; @@ -1706,7 +1706,6 @@ luaA_screen_get_content(lua_State *L, screen_t *s) lua_pushlightuserdata(L, scrn_surface); return 1; - } diff --git a/root.c b/root.c index 822fa01c..01f23d1b 100644 --- a/root.c +++ b/root.c @@ -501,15 +501,15 @@ luaA_root_wallpaper(lua_State *L) } -/** Get the content of the root window as a cairo surface +/** Get the content of the root window as a cairo surface. * - * @return A cairo surface - * @staticfct content + * @property content + * @tparam surface A cairo surface with the root window content (aka the whole surface from every screens). + * @see gears.surface */ static int luaA_root_get_content(lua_State *L) { - cairo_surface_t *surface; int width = globalconf.screen->width_in_pixels; int height = globalconf.screen->height_in_pixels; @@ -521,7 +521,6 @@ luaA_root_get_content(lua_State *L) lua_pushlightuserdata(L, surface); return 1; - } From a9bf812c17e0d220288177e699b114756f7e2d75 Mon Sep 17 00:00:00 2001 From: poisson-aerohead Date: Thu, 30 Sep 2021 17:58:10 -0700 Subject: [PATCH 3/8] Update screen.c --- objects/screen.c | 60 ------------------------------------------------ 1 file changed, 60 deletions(-) diff --git a/objects/screen.c b/objects/screen.c index 1d318112..5fd2b501 100644 --- a/objects/screen.c +++ b/objects/screen.c @@ -59,9 +59,6 @@ #include #include -#include -#include - /* The XID that is used on fake screens. X11 guarantees that the top three bits * of a valid XID are zero, so this will not clash with anything. */ @@ -1658,59 +1655,6 @@ luaA_screen_get_name(lua_State *L, screen_t *s) return 1; } -/** Get the content of the active screen as a cairo surface. - * - * @property content - * @tparam surface A cairo surface with the screen content. - * @see gears.surface - */ -static int -luaA_screen_get_content(lua_State *L, screen_t *s) -{ - cairo_surface_t *root_surface; - cairo_surface_t *scrn_surface; - cairo_t *scrn_cr; - - area_t *scrn_geom; - - int root_width = globalconf.screen->width_in_pixels; - int root_height = globalconf.screen->height_in_pixels; - - scrn_geom = &(s->geometry); - - root_surface = cairo_xcb_surface_create(globalconf.connection, - globalconf.screen->root, - globalconf.default_visual, - root_width, root_height); - - scrn_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, - scrn_geom->width, scrn_geom->height); - scrn_cr = cairo_create(scrn_surface); - - if(cairo_surface_status(root_surface) != CAIRO_STATUS_SUCCESS || - cairo_surface_status(scrn_surface) != CAIRO_STATUS_SUCCESS){ - cairo_surface_destroy(root_surface); - cairo_surface_destroy(scrn_surface); - cairo_destroy(scrn_cr); - return 0; - } - - cairo_set_source_surface(scrn_cr, root_surface, - -(scrn_geom->x), -(scrn_geom->y)); - cairo_rectangle(scrn_cr, 0, 0, scrn_geom->width, scrn_geom->height); - cairo_fill(scrn_cr); - - cairo_surface_destroy(root_surface); - cairo_destroy(scrn_cr); - - lua_pushlightuserdata(L, scrn_surface); - - return 1; -} - - - - /** Get the number of screens. * * @return The screen count, at least 1. @@ -1958,10 +1902,6 @@ screen_class_setup(lua_State *L) (lua_class_propfunc_t) luaA_screen_set_name, (lua_class_propfunc_t) luaA_screen_get_name, (lua_class_propfunc_t) luaA_screen_set_name); - luaA_class_add_property(&screen_class, "content", - NULL, - (lua_class_propfunc_t) luaA_screen_get_content, - NULL); } /* @DOC_cobject_COMMON@ */ From b4cb3eae7b9f7bca04fb814e993a910507b84f3b Mon Sep 17 00:00:00 2001 From: poisson-aerohead Date: Thu, 30 Sep 2021 18:13:35 -0700 Subject: [PATCH 4/8] Debug the suggested changes to lib/awful/screen.lua per the discussion thread at PR 3448. PR 3448 involves changes to expand the content (screenshot) API. Originally, I added both root.content() and and screen.content to the C source, as client.content has always been handled. However, screen.content in effect takes a root screenshot and returns a crop of it. This can just as easily be done through Lua. When this quick update was implemented in github, the code added to awful.screen.lua was not quite correct. These changes represent the debugged version. Users can now call s.content for a screen object, s, and the screenshot will work transparently. Signed Off: Brian Sobulefsky --- lib/awful/screen.lua | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/awful/screen.lua b/lib/awful/screen.lua index c1411470..ee384139 100644 --- a/lib/awful/screen.lua +++ b/lib/awful/screen.lua @@ -19,6 +19,8 @@ local gdebug = require("gears.debug") local gmath = require("gears.math") local object = require("gears.object") local grect = require("gears.geometry").rectangle +local gsurf = require("gears.surface") +local cairo = require("lgi").cairo local function get_screen(s) return s and capi.screen[s] @@ -201,6 +203,22 @@ function screen.object.get_tiling_area(s) } end +--- Take a screenshot of the physical screen. +-- +-- Reading this (read only) property returns a screenshot of the physical +-- (Xinerama) screen as a cairo surface. + +function screen.object.get_content(s) + local geo = s.geometry + local source = gsurf(root.content()) + local target = source:create_similar(cairo.Content.COLOR, geo.width, geo.height) + local cr = cairo.Context(target) + cr:set_source_surface(source, -geo.x, -geo.y) + cr:rectangle(0, 0, geo.width, geo.height) + cr:fill() + return target +end + --- Get or set the screen padding. -- -- @deprecated awful.screen.padding From 02e12f4cb4e1fd60b6374ada2cd0388359a390f5 Mon Sep 17 00:00:00 2001 From: poisson-aerohead Date: Fri, 1 Oct 2021 10:24:55 -0700 Subject: [PATCH 5/8] Update screen.lua Add DOC comments to the new screen.object.get_content property in lib/awful/screen.lua. Signed Off: Brian Sobulefsky --- lib/awful/screen.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/awful/screen.lua b/lib/awful/screen.lua index ee384139..161b9682 100644 --- a/lib/awful/screen.lua +++ b/lib/awful/screen.lua @@ -207,6 +207,12 @@ end -- -- Reading this (read only) property returns a screenshot of the physical -- (Xinerama) screen as a cairo surface. +-- +-- @DOC_screen_content_EXAMPLE@ +-- +-- @property content +-- @tparam screen s (self) +-- @treturn cairo scurface of the screen content function screen.object.get_content(s) local geo = s.geometry From 7a786dab1d3184a34828e26a422cc78fefb9bcd9 Mon Sep 17 00:00:00 2001 From: poisson-aerohead Date: Fri, 1 Oct 2021 11:39:01 -0700 Subject: [PATCH 6/8] Update root.c Remove unnecessary function variables width and height, as they do not add any real value, and rather pass these as all other arguments are (such as the connection), giving the routine a uniform look. Signed Off: Brian Sobulefsky --- root.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/root.c b/root.c index 01f23d1b..2b803089 100644 --- a/root.c +++ b/root.c @@ -511,13 +511,12 @@ static int luaA_root_get_content(lua_State *L) { cairo_surface_t *surface; - int width = globalconf.screen->width_in_pixels; - int height = globalconf.screen->height_in_pixels; surface = cairo_xcb_surface_create(globalconf.connection, globalconf.screen->root, globalconf.default_visual, - width, height); + globalconf.screen->width_in_pixels, + globalconf.screen->height_in_pixels); lua_pushlightuserdata(L, surface); return 1; From 4f0c3c5d906b3aff3068757940df43e449327a01 Mon Sep 17 00:00:00 2001 From: Brian Sobulefsky Date: Sat, 2 Oct 2021 23:07:14 -0700 Subject: [PATCH 7/8] Add tests for the new content API. This includes tests for root.content(), screen.content, and client.content. Run with TESTS_SCREEN_SIZE set equal to 1280x800. This test suite will be expanded once the more user friendly awful.screenshot API is merged. Signed off: Brian Sobulefsky --- tests/test-screenshot.lua | 164 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 tests/test-screenshot.lua diff --git a/tests/test-screenshot.lua b/tests/test-screenshot.lua new file mode 100644 index 00000000..3332a15f --- /dev/null +++ b/tests/test-screenshot.lua @@ -0,0 +1,164 @@ +-- This test suite tests the various screenshot related APIs. +-- +-- Credit: https://www.reddit.com/r/awesomewm/comments/i6nf7z/need_help_writing_a_color_picker_widget_using_lgi/ +local wibox = require("wibox") +local spawn = require("awful.spawn") +local gsurface = require("gears.surface") +local lgi = require 'lgi' +local cairo = lgi.cairo +local gdk = lgi.Gdk + +-- Dummy blue client for the client.content test +-- the lua_executable portion may need to get ironed out. I need to specify 5.3 +local lua_executable = os.getenv("LUA") +if lua_executable == nil or lua_executable == "" then + lua_executable = "lua5.3" +end +local client_dim = 250 +local tiny_client_code_template = [[ +local Gtk = require('lgi').Gtk +local Gdk = require('lgi').Gdk +local class = 'client' +window = Gtk.Window {default_width=%d, default_height=%d, title='title'} +window:set_wmclass(class, class) +window:override_background_color(0, + Gdk.RGBA({red = 0, green = 0, blue = 1, alpha = 1})) +window:show_all() +Gtk:main{...} +]] +local tiny_client = { lua_executable, "-e", string.format( + tiny_client_code_template, client_dim, client_dim)} + +-- how to make a GTK window using LGI + +-- Split in the screen into 2 distict screens. +screen[1]:split() + +-- Add a green wibox on screen 1 at (100, 100) +wibox { + bg = "#00ff00", + visible = true, + width = 100, + height = 100, + x = 100, + y = 100, +} + +local corners = {} + +-- Add red squares at 1 pixel away from each screen corners. +for s in screen do + -- Top left + table.insert(corners, { + x = s.geometry.x + 1, + y = s.geometry.y + 1, + }) + + -- Top right + table.insert(corners, { + x = s.geometry.x + s.geometry.width - 11, + y = s.geometry.y + 1, + }) + + -- Bottom left + table.insert(corners, { + x = s.geometry.x + 1, + y = s.geometry.y + s.geometry.height - 11, + }) + + -- Bottom right + table.insert(corners, { + x = s.geometry.x + s.geometry.width - 11, + y = s.geometry.y + s.geometry.height - 11, + }) + +end + +-- Make 10x10 wibox in each corner to eliminate any geometry errors +for _, corner in ipairs(corners) do + corner.bg = "#ff0000" + corner.visible = true + corner.width = 10 + corner.height = 10 + + wibox(corner) +end + +local function copy_to_image_surface(content, w, h) + local sur = gsurface(content) + + local img = cairo.ImageSurface(cairo.Format.RGB24, w, h) + local cr = cairo.Context(img) + + cr:set_source_surface(sur) + cr:paint() + img:finish() + + return img +end + +local function get_pixel(img, x, y) + local bytes = gdk.pixbuf_get_from_surface(img, x, y, 1, 1):get_pixels() + return "#" .. bytes:gsub('.', function(c) return ('%02x'):format(c:byte()) end) +end + +local steps = {} + +-- Check the whole root window. +table.insert(steps, function() + local img = copy_to_image_surface(root.content(), root.size()) + local root_width, root_height = root.size() + + if get_pixel(img, 100, 100) ~= "#00ff00" then return end + if get_pixel(img, 2, 2) ~= "#ff0000" then return end + + assert(get_pixel(img, 100, 100) == "#00ff00") + assert(get_pixel(img, 199, 199) == "#00ff00") + assert(get_pixel(img, 201, 201) ~= "#00ff00") + + assert(get_pixel(img, 2, 2) == "#ff0000") + assert(get_pixel(img, root_width - 2, 2) == "#ff0000") + assert(get_pixel(img, 2, root_height - 2) == "#ff0000") + assert(get_pixel(img, root_width - 2, root_height - 2) == "#ff0000") + + return true +end) + +-- Check the screen.content +table.insert(steps, function() + for s in screen do + + local geo = s.geometry + local img = copy_to_image_surface(s.content, geo.width, geo.height) + + assert(get_pixel(img, geo.x + 4, geo.y + 4) == "#ff0000") + assert(get_pixel(img, geo.x + geo.width - 4, geo.y + 4) == "#ff0000") + assert(get_pixel(img, geo.x + 4, geo.y + geo.height - 4) == "#ff0000") + assert(get_pixel(img, geo.x + geo.width - 4, geo.y + geo.height - 4) == "#ff0000") + + end + + -- Spawn for the client.content test + assert(#client.get() == 0) + spawn(tiny_client) + + return true +end) + +-- Check the client.content +table.insert(steps, function() + if #client.get() ~= 1 then return end + local c = client.get()[1] + local geo = c:geometry() + local img = copy_to_image_surface(c.content, geo.width, geo.height) + + if get_pixel(img, math.floor(geo.width / 2), math.floor(geo.height / 2)) ~= "#0000ff" then + return + end + + awesome.kill(c.pid, 9) + + return true +end) + +require("_runner").run_steps(steps) From eb89c3688c6728a1f10a98ade9e4f63a09d609d3 Mon Sep 17 00:00:00 2001 From: Aire-One Date: Mon, 4 Oct 2021 19:48:59 -0700 Subject: [PATCH 8/8] Update screen.lua --- lib/awful/screen.lua | 7 ++----- tests/test-screenshot.lua | 5 +++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/awful/screen.lua b/lib/awful/screen.lua index 161b9682..08970644 100644 --- a/lib/awful/screen.lua +++ b/lib/awful/screen.lua @@ -208,11 +208,8 @@ end -- Reading this (read only) property returns a screenshot of the physical -- (Xinerama) screen as a cairo surface. -- --- @DOC_screen_content_EXAMPLE@ --- -- @property content --- @tparam screen s (self) --- @treturn cairo scurface of the screen content +-- @tparam gears.surface content function screen.object.get_content(s) local geo = s.geometry @@ -222,7 +219,7 @@ function screen.object.get_content(s) cr:set_source_surface(source, -geo.x, -geo.y) cr:rectangle(0, 0, geo.width, geo.height) cr:fill() - return target + return target end --- Get or set the screen padding. diff --git a/tests/test-screenshot.lua b/tests/test-screenshot.lua index 3332a15f..34f812f8 100644 --- a/tests/test-screenshot.lua +++ b/tests/test-screenshot.lua @@ -29,8 +29,6 @@ Gtk:main{...} local tiny_client = { lua_executable, "-e", string.format( tiny_client_code_template, client_dim, client_dim)} --- how to make a GTK window using LGI - -- Split in the screen into 2 distict screens. screen[1]:split() @@ -156,6 +154,9 @@ table.insert(steps, function() return end + -- Make sure the process finishes. Just `c:kill()` only + -- closes the window. Adding some handlers to the GTK "app" + -- created some unwanted side effects in the CI. awesome.kill(c.pid, 9) return true