Merge lp:~jeremywootten/screenshot-tool/fix-1438250-include-child-windows into lp:~elementary-apps/screenshot-tool/trunk

Proposed by Jeremy Wootten
Status: Work in progress
Proposed branch: lp:~jeremywootten/screenshot-tool/fix-1438250-include-child-windows
Merge into: lp:~elementary-apps/screenshot-tool/trunk
Diff against target: 425 lines (+282/-53)
1 file modified
src/ScreenshotWindow.vala (+282/-53)
To merge this branch: bzr merge lp:~jeremywootten/screenshot-tool/fix-1438250-include-child-windows
Reviewer Review Type Date Requested Status
Danielle Foré Needs Fixing
Sergey Kislyakov (community) Approve
Review via email: mp+322250@code.launchpad.net

This proposal supersedes a proposal from 2017-03-21.

Commit message

Include menus, tooltips and other child windows as well as non-CSD decorations in screenshot.

Description of the change

This branch provides a solution to the problem of including popups and menus in a snapshot of a current window. Instead of using pixbuf_get_from_window, the same method as used for grabbing selected areas is used - that is obtaining a subpixbuf from the whole window - after calculating a suitable selection rectangle.

This is complicated by the fact that window.get_frame_extents includes any border around the main window area used to show shadows etc. The function that discounts this relies on it having a low opacity compared with the rest of the window. The method therefore relies on the pixbufs obtained will have four channels per pixel with one byte per channel. It is not known whether changing to Wayland will affect this.

The final screenshot is a composite of both methods in order that the window shadow and non-CSD decorations are included (if present). In order to show the shadow more clearly it is drawn on a white background.

It is recognised that this solution is "hacky" so if a simpler solution is available it should be preferred.

If the current window is partially off the current screen any menus or popups extending outside the main window will be truncated (as they are not drawn by the current window).

While rewriting grab_save (), the method of capturing the cursor position was changed in order to place it more accurately. (lp:1627704)

In addition, due to problems experienced during development where the program crashed while system text was redacted, code was included that recovered the normal text when the program was run again.

Testing on a hi-DPI as well as a standard display is desirable.

To post a comment you must log in.
Revision history for this message
Sergey Kislyakov (defman21) wrote : Posted in a previous version of this proposal

You can't capture the Terminal window anymore - it crashes with segfault.

review: Needs Fixing
Revision history for this message
Sergey Kislyakov (defman21) wrote : Posted in a previous version of this proposal

There are no shadows when making a screenshot of a window (no matter CSD or not). They were there before.

review: Needs Fixing
Revision history for this message
Jeremy Wootten (jeremywootten) wrote : Posted in a previous version of this proposal

The r317 should fix the problems with capturing terminal (which is translucent) and adds the shadow to the final result. However, it no longer fixes bug 1678520 so I am putting it back to "in progress" while seeking a solution.

Revision history for this message
Jeremy Wootten (jeremywootten) wrote : Posted in a previous version of this proposal

The last revision provides a solution to the non-CSD window bug by using Cairo rather than Gdk to composite the final screenshot. Some care was required to ensure reasonable handling of partially off-screen windows in each case.

319. By Jeremy Wootten

Composite using Cairo

Revision history for this message
Sergey Kislyakov (defman21) wrote : Posted in a previous version of this proposal

Well it works fine, but non CSD windows lacks shadow. Regardless of that, it works fine. Nice job!

review: Needs Fixing
Revision history for this message
Jeremy Wootten (jeremywootten) wrote :

I do not think there is a way of capturing shadows on non CSD windows without capturing the underlying screen which could be objected to on security grounds. This was the problem with an earlier version of this fix. At the end of the day, it is always possible to include whatever you want by using the "grab selected area" option.

Revision history for this message
Sergey Kislyakov (defman21) wrote :

I thought shadows are being applied after taking a screenshot of a window. I can workaround it by applying shadows using some external tools like ImageMagick though.

Revision history for this message
Jeremy Wootten (jeremywootten) wrote :

That is the case for CSD windows, where "clean" shadows are present in the image obtained from Gdk.pixbuf_get_from_window () and are composited into the final result. However, this does not work for non-CSD windows - I do not know how to get "clean" shadows for those.

Revision history for this message
Sergey Kislyakov (defman21) wrote :

I see. Well at least I can now capture non-CSD windows with their titles.

review: Approve
Revision history for this message
Danielle Foré (danrabbit) wrote :

This branch breaks transparent screenshots. There is now a white background

review: Needs Fixing
Revision history for this message
Jeremy Wootten (jeremywootten) wrote :

The background of the shadow region is intentionally white in order to make the shadow visible but that could be changed easily. However, using terminal with the background alpha dialled right down is a test, the desktop is visible through the transparent parts so is whatever colour(s) the desktop is. This is inevitable if the associated child windows are to be captured using this method.

One possibility would be to have another RadioButton choice "Capture active window with child windows" and keep the old method as well for when that is more appropriate.

Revision history for this message
Jeremy Wootten (jeremywootten) wrote :

Actually, there may be a way of dealing with transparent windows without showing the underlying if it is assumed that any child windows (menus, tooltips etc) are opaque - at the cost of increased complexity in the compositing.

Revision history for this message
Jeremy Wootten (jeremywootten) wrote :

Now working on github branch.

Revision history for this message
Jeremy Wootten (jeremywootten) wrote :

The possible refinement mentioned above did not work out - sorry :(

Unmerged revisions

319. By Jeremy Wootten

Composite using Cairo

318. By Jeremy Wootten

Merge trunk to r318 and resolve conflict

317. By Jeremy Wootten

Fix detect shadow; composite shadow in screenshot

316. By Jeremy Wootten

Fix placement of cursor

315. By Jeremy Wootten

Include popups and menus in current window snapshot; Recover from redacted system after crash

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/ScreenshotWindow.vala'
2--- src/ScreenshotWindow.vala 2017-03-21 17:13:44 +0000
3+++ src/ScreenshotWindow.vala 2017-04-08 18:20:47 +0000
4@@ -30,9 +30,9 @@
5 private Settings settings = new Settings ("net.launchpad.screenshot");
6
7 private CaptureType capture_mode;
8- private string prev_font_regular;
9- private string prev_font_document;
10- private string prev_font_mono;
11+ private string? prev_font_regular = null;
12+ private string? prev_font_document = null;
13+ private string? prev_font_mono = null;
14
15 private bool mouse_pointer;
16 private bool from_command;
17@@ -55,6 +55,8 @@
18 }
19
20 construct {
21+ redact_text (false); /* Ensure system is not redacted */
22+
23 if (from_command) {
24 return;
25 }
26@@ -224,67 +226,44 @@
27 return false;
28 });
29
30- var win_rect = Gdk.Rectangle ();
31 var root = Gdk.get_default_root_window ();
32
33 if (win == null) {
34 win = root;
35 }
36
37- Gdk.Pixbuf? screenshot;
38- int scale_factor = win.get_scale_factor ();
39-
40- if (capture_mode == CaptureType.AREA) {
41- Gdk.Rectangle selection_rect;
42- win.get_frame_extents (out selection_rect);
43-
44- screenshot = new Gdk.Pixbuf.subpixbuf (Gdk.pixbuf_get_from_window (root, 0, 0, root.get_width (), root.get_height ()),
45- selection_rect.x, selection_rect.y, selection_rect.width, selection_rect.height);
46-
47- win_rect.x = selection_rect.x;
48- win_rect.y = selection_rect.y;
49- win_rect.width = selection_rect.width;
50- win_rect.height = selection_rect.height;
51- } else {
52- int width = win.get_width ();
53- int height = win.get_height ();
54-
55- // Check the scaling factor in use, and if greater than 1 scale the image. (for HiDPI displays)
56- if (scale_factor > 1 && capture_mode == CaptureType.SCREEN) {
57- screenshot = Gdk.pixbuf_get_from_window (win, 0, 0, width / scale_factor, height / scale_factor);
58- screenshot.scale (screenshot, width, height, width, height, 0, 0, scale_factor, scale_factor, Gdk.InterpType.BILINEAR);
59- } else {
60- screenshot = Gdk.pixbuf_get_from_window (win, 0, 0, width, height);
61- }
62-
63- win_rect.x = 0;
64- win_rect.y = 0;
65- win_rect.width = width;
66- win_rect.height = height;
67- }
68-
69- if (screenshot == null) {
70+ var root_width = root.get_width ();
71+ var root_height = root.get_height ();
72+ int scale_factor = root.get_scale_factor ();
73+
74+ Gdk.Pixbuf? root_pix = get_scaled_pixbuf_from_window (root, capture_mode, scale_factor);
75+ /* Ensure redacted text restored asap */
76+ redact_text (false);
77+
78+ if (root_pix == null) {
79 show_error_dialog ();
80 return false;
81 }
82
83 if (mouse_pointer) {
84+ /* Getting actual cursor for current window fails for some reason so we construct a default cursor */
85 var cursor = new Gdk.Cursor.for_display (Gdk.Display.get_default (), Gdk.CursorType.LEFT_PTR);
86 var cursor_pixbuf = cursor.get_image ();
87
88+ /* It is easier to accurately place cursor by compositing cursor image with the root screen */
89+ /* It may or may not appear in the final screenshot */
90 if (cursor_pixbuf != null) {
91 var manager = Gdk.Display.get_default ().get_device_manager ();
92 var device = manager.get_client_pointer ();
93
94 int cx, cy, xhot, yhot;
95- win.get_device_position (device, out cx, out cy, null);
96+ root.get_device_position (device, out cx, out cy, null);
97 xhot = int.parse (cursor_pixbuf.get_option ("x_hot")); // Left padding in cursor_pixbuf between the margin and the actual pointer
98 yhot = int.parse (cursor_pixbuf.get_option ("y_hot")); // Top padding in cursor_pixbuf between the margin and the actual pointer
99
100 var cursor_rect = Gdk.Rectangle ();
101-
102- cursor_rect.x = cx + win_rect.x - xhot;
103- cursor_rect.y = cy + win_rect.y - yhot;
104+ cursor_rect.x = cx - xhot;
105+ cursor_rect.y = cy - yhot;
106 cursor_rect.width = cursor_pixbuf.get_width ();
107 cursor_rect.height = cursor_pixbuf.get_height ();
108
109@@ -295,14 +274,91 @@
110 cursor_rect.height *= scale_factor;
111 }
112
113- if (win_rect.intersect (cursor_rect, out cursor_rect)) {
114- cursor_pixbuf.composite (screenshot, cursor_rect.x, cursor_rect.y, cursor_rect.width, cursor_rect.height, cursor_rect.x, cursor_rect.y, scale_factor, scale_factor, Gdk.InterpType.BILINEAR, 255);
115- }
116- }
117- }
118-
119- if (redact) {
120- redact_text (false);
121+ cursor_pixbuf.composite (root_pix,
122+ cursor_rect.x, cursor_rect.y,
123+ cursor_rect.width, cursor_rect.height,
124+ cursor_rect.x, cursor_rect.y,
125+ scale_factor,
126+ scale_factor,
127+ Gdk.InterpType.BILINEAR,
128+ 255);
129+ }
130+ }
131+
132+ Gdk.Pixbuf? screenshot = null;
133+
134+ if (capture_mode == CaptureType.AREA || capture_mode == CaptureType.CURRENT_WINDOW) {
135+ /* We use a subpixbuf of the root screen for capturing the current window because this will include
136+ * menus, popups, tooltips etc whereas pixbuf_get_from_window will not */
137+ Gdk.Rectangle selection_rect;
138+ win.get_frame_extents (out selection_rect); /* Includes non-CSD decorations and regions offscreen */
139+ Gdk.Pixbuf? window_pix = null;
140+ int offset_x, offset_y;
141+ offset_x = offset_y = 0;
142+ Gdk.Rectangle subpix_rect = {selection_rect.x, selection_rect.y ,selection_rect.width ,selection_rect.height};
143+
144+ if (capture_mode == CaptureType.CURRENT_WINDOW) {
145+ /* frame_extents may include shadow region as a translucent border which we
146+ * do not want when creating the subpixbuf because it will cause selection of
147+ * parts of the background screen.*/
148+
149+ /* Need to scale frame extents when not selected manually */
150+ if (scale_factor > 1) {
151+ selection_rect.x *= scale_factor;
152+ selection_rect.y *= scale_factor;
153+ selection_rect.width *= scale_factor;
154+ selection_rect.height *= scale_factor;
155+ }
156+
157+ window_pix = get_scaled_pixbuf_from_window (win, capture_mode, scale_factor);
158+ /* window_pix does not include non-CSD decorations but includes regions offscreen */
159+ /* For non-CSD windows window_pix has no shadow and selection_rect will be unaltered and larger than window_pix;
160+ * the offsets will be zero. For CSD windows, selection_rect starts the same size as window_pix
161+ * and will be shrunk to exclude the shadows; the offsets will reflect the thickness of the shadows. */
162+ selection_rect = remove_translucent_border (window_pix, selection_rect, out offset_x, out offset_y);
163+ subpix_rect = {selection_rect.x, selection_rect.y ,selection_rect.width ,selection_rect.height};
164+
165+ /* Do not try to select region outside the root window */
166+ if (selection_rect.x < 0) {
167+ subpix_rect.width += selection_rect.x;
168+ subpix_rect.x = 0;
169+ }
170+
171+ if (selection_rect.x + selection_rect.width > root_width) {
172+ subpix_rect.width = root_width - selection_rect.x;
173+ }
174+
175+ if (selection_rect.y < 0) {
176+ subpix_rect.height += selection_rect.y;
177+ subpix_rect.y = 0;
178+ }
179+
180+ if (selection_rect.y + selection_rect.height > root_height) {
181+ subpix_rect.height = root_height - selection_rect.y;
182+ }
183+ }
184+
185+ Gdk.Pixbuf subpix = new Gdk.Pixbuf.subpixbuf (root_pix,
186+ subpix_rect.x,
187+ subpix_rect.y,
188+ subpix_rect.width,
189+ subpix_rect.height);
190+
191+ if (capture_mode == CaptureType.CURRENT_WINDOW) {
192+ /* For whole window grab, construct a composite of selection_pix and window_pix
193+ * (including shadow if there is one - absent with non-CSD windows. Use Cairo since
194+ * we do not know which of the pixbufs is larger. */
195+ screenshot = composite_pix (subpix, window_pix, selection_rect, offset_x, offset_y);
196+ } else {
197+ screenshot = subpix;
198+ }
199+ } else {
200+ screenshot = root_pix;
201+ }
202+
203+ if (screenshot == null) {
204+ show_error_dialog ();
205+ return false;
206 }
207
208 play_shutter_sound ("screen-capture", _("Screenshot taken"));
209@@ -349,6 +405,7 @@
210 /// TRANSLATORS: %s represents a timestamp here
211 string file_name = _("Screenshot from %s").printf (date_time);
212 string format = settings.get_string ("format");
213+
214 try {
215 save_file (file_name, format, "", screenshot);
216 } catch (GLib.Error e) {
217@@ -356,12 +413,180 @@
218 debug (e.message);
219 }
220 }
221+
222 this.destroy ();
223 }
224
225 return false;
226 }
227
228+ private Gdk.Pixbuf? get_scaled_pixbuf_from_window (Gdk.Window win, CaptureType mode, int scale_factor) {
229+ int width = win.get_width ();
230+ int height = win.get_height ();
231+ // Check the scaling factor in use, and if greater than 1 scale the image. (for HiDPI displays)
232+ Gdk.Pixbuf? pix;
233+ if (scale_factor > 1 && mode == CaptureType.SCREEN) {
234+ pix = Gdk.pixbuf_get_from_window (win, 0, 0, width / scale_factor, height / scale_factor);
235+ pix.scale (pix, width, height, width, height, 0, 0, scale_factor, scale_factor, Gdk.InterpType.BILINEAR);
236+ } else {
237+ pix = Gdk.pixbuf_get_from_window (win, 0, 0, width, height);
238+ }
239+
240+ return pix;
241+ }
242+
243+
244+ private Gdk.Rectangle remove_translucent_border (Gdk.Pixbuf? pix, Gdk.Rectangle rect, out int offset_x, out int offset_y) {
245+ const int MIN_ALPHA = 128;
246+ const int MIN_COLOR = 10;
247+ offset_x = offset_y = 0;
248+
249+ if (pix == null) {
250+ rect.x = rect.y = rect.width = rect.height = 0;
251+ } else {
252+ /* Measure any dark translucent shadow around pix */
253+ var height = pix.get_height ();
254+ var width = pix.get_width ();
255+ uint8[] bytes = pix.read_pixel_bytes ().get_data ();
256+ var length = pix.get_byte_length ();
257+ var rowstride = pix.rowstride;
258+ var half_row = (width / 2) * 4;
259+
260+ /* Measure top margin */
261+ int count = 0;
262+ uint index = half_row + 3;
263+ while (index < length &&
264+ bytes[index] < MIN_ALPHA &&
265+ bytes[index - 1] < MIN_COLOR &&
266+ bytes[index - 2] < MIN_COLOR &&
267+ bytes[index - 3] < MIN_COLOR) {
268+
269+
270+ index += rowstride;
271+ count++;
272+ }
273+
274+ rect.height -= count;
275+ rect.y += count;
276+ offset_y = count;
277+
278+ /* Measure bottom margin */
279+ count = 0;
280+ index = (uint)(length - half_row + 3);
281+
282+ while (index < length &&
283+ bytes[index] < MIN_ALPHA &&
284+ bytes[index - 1] < MIN_COLOR &&
285+ bytes[index - 2] < MIN_COLOR &&
286+ bytes[index - 3] < MIN_COLOR) {
287+
288+ index -= rowstride;
289+ count++;
290+ }
291+
292+ rect.height -= count;
293+
294+ /* Measure left margin */
295+ count = 0;
296+ index = (uint)(rowstride * (height / 2) + 3);
297+ while (index < length &&
298+ bytes[index] < MIN_ALPHA &&
299+ bytes[index - 1] < MIN_COLOR &&
300+ bytes[index - 2] < MIN_COLOR &&
301+ bytes[index - 3] < MIN_COLOR) {
302+
303+ index += 4;
304+ count++;
305+ }
306+
307+ rect.width -= count;
308+ rect.x += count;
309+ offset_x = count;
310+
311+ /* Measure right margin */
312+ count = 0;
313+ index = rowstride * (height / 2) - 1;
314+ while (index < length &&
315+ bytes[index] < MIN_ALPHA &&
316+ bytes[index - 1] < MIN_COLOR &&
317+ bytes[index - 2] < MIN_COLOR &&
318+ bytes[index - 3] < MIN_COLOR) {
319+
320+ index -= 4;
321+ count++;
322+ }
323+
324+ rect.width -= count;
325+ }
326+
327+ return rect;
328+ }
329+ /*** @pix1 is the subpix of the root screen.
330+ * @pix2 is the pixbuf of the whole window with CSD decorations (if any) including shadows.
331+ * @selection_rect is the area of the whole window, including non-CSD decorations (if any) but excluding shadows.
332+ * @offset_x and @offset_y are zero for non-CSD window since @pix2 is smaller than @selection_rect.
333+ * @offset_x and @offset_y reflect the left and top shadow widths for CSD windows.
334+ * @pix1 will be composited on top of @pix2. The final result
335+ * will contain the whole of each pix on a white background.
336+ ***/
337+ private Gdk.Pixbuf composite_pix (Gdk.Pixbuf pix1, Gdk.Pixbuf pix2, Gdk.Rectangle selection_rect,
338+ int offset_x, int offset_y) {
339+
340+ double offset_x1 = 0.0;
341+ double offset_y1 = 0.0;
342+ double offset_x2 = 0.0;
343+ double offset_y2 = 0.0;
344+ int width1 = pix1.get_width ();
345+ int width2 = pix2.get_width ();
346+ int height1 = pix1.get_height ();
347+ int height2 = pix2.get_height ();
348+
349+ offset_x1 = offset_x;
350+ offset_y1 = offset_y;
351+
352+ bool left_truncation = (selection_rect.width > width1 && selection_rect.x < 0);
353+ bool top_truncation = (selection_rect.height > height1 && selection_rect.y < 0);
354+ int top_truncation_amount = top_truncation ? selection_rect.height - height1 : 0;
355+ bool non_CSD = (selection_rect.height > height2);
356+ int non_CSD_amount = non_CSD ? selection_rect.height - height2 : 0;
357+ int non_CSD_not_showing_amount = non_CSD ? int.min (top_truncation_amount, non_CSD_amount) : 0;
358+
359+ if (left_truncation) {
360+ offset_x1 -= selection_rect.x;
361+ }
362+
363+ if (top_truncation) {
364+ if (!non_CSD) {
365+ offset_y1 -= selection_rect.y;
366+ } else {
367+ offset_y1 -= (selection_rect.y + non_CSD_amount);
368+ }
369+ } else {
370+ offset_y2 = non_CSD_amount;
371+ }
372+
373+ var cairo_width = int.max (selection_rect.width, width2);
374+ var cairo_height = int.max (selection_rect.height - non_CSD_not_showing_amount, height2);
375+
376+ /* Create an Image surface large enough to hold composite image*/
377+ var cs = new Cairo.ImageSurface (Cairo.Format.ARGB32, cairo_width, cairo_height);
378+ var cr = new Cairo.Context (cs);
379+ cr.set_operator (Cairo.Operator.OVER);
380+
381+ cr.set_source_rgba (255, 255, 255, 255); // White background to show shadow
382+ cr.paint ();
383+
384+ cr.translate (offset_x2, offset_y2);
385+ Gdk.cairo_set_source_pixbuf (cr, pix2, 0.0, 0.0);
386+ cr.paint ();
387+
388+ cr.translate (offset_x1 - offset_x2, offset_y1 - offset_y2);
389+ Gdk.cairo_set_source_pixbuf (cr, pix1, 0.0, 0.0);
390+ cr.paint ();
391+
392+ return Gdk.pixbuf_get_from_surface (cs, 0, 0, cairo_width, cairo_height);
393+ }
394+
395 private void save_file (string file_name, string format, string folder_dir, Gdk.Pixbuf screenshot) throws GLib.Error {
396 string full_file_name = "";
397
398@@ -479,9 +704,9 @@
399 var win = selection_area.get_window ();
400
401 selection_area.captured.connect (() => {
402- if (delay == 0) {
403- selection_area.set_opacity (0);
404- }
405+ if (delay == 0) {
406+ selection_area.set_opacity (0);
407+ }
408 selection_area.close ();
409 Timeout.add_seconds (delay - (redact ? 1 : 0), () => {
410 if (from_command == false) {
411@@ -511,10 +736,14 @@
412 settings.set_string ("font-name", "Redacted Script Regular 9");
413 settings.set_string ("monospace-font-name", "Redacted Script Light 10");
414 settings.set_string ("document-font-name", "Redacted Script Regular 10");
415- } else {
416+ } else if (prev_font_regular != null) {
417 settings.set_string ("font-name", prev_font_regular);
418 settings.set_string ("monospace-font-name", prev_font_mono);
419 settings.set_string ("document-font-name", prev_font_document);
420+ } else if (settings.get_string ("font-name").contains ("Redacted")) { /* Fallback in case a program crash leaves the system redacted */
421+ settings.reset ("font-name");
422+ settings.reset ("monospace-font-name");
423+ settings.reset ("document-font-name");
424 }
425 }
426

Subscribers

People subscribed via source and target branches