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

Proposed by Jeremy Wootten
Status: Superseded
Proposed branch: lp:~jeremywootten/screenshot-tool/fix-1438250-include-child-windows
Merge into: lp:~elementary-apps/screenshot-tool/trunk
Diff against target: 427 lines (+284/-53)
1 file modified
src/ScreenshotWindow.vala (+284/-53)
To merge this branch: bzr merge lp:~jeremywootten/screenshot-tool/fix-1438250-include-child-windows
Reviewer Review Type Date Requested Status
Sergey Kislyakov (community) Needs Fixing
elementary Apps team Pending
Review via email: mp+320543@code.launchpad.net

This proposal has been superseded by a proposal from 2017-04-08.

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.

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 the the grabbed screenshot will be truncated - unlike in the trunk. Any menus or popups extending outside the main window will also be truncated.

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.

To post a comment you must log in.
315. By Jeremy Wootten

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

316. By Jeremy Wootten

Fix placement of cursor

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

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

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

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

review: Needs Fixing
317. By Jeremy Wootten

Fix detect shadow; composite shadow in screenshot

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

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.

318. By Jeremy Wootten

Merge trunk to r318 and resolve conflict

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

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 :

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

review: Needs Fixing

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 17:28:26 +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,93 @@
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, scale_factor);
196+ } else {
197+ screenshot = subpix;
198+ }
199+
200+
201+ } else {
202+ screenshot = root_pix;
203+ }
204+
205+ if (screenshot == null) {
206+ show_error_dialog ();
207+ return false;
208 }
209
210 play_shutter_sound ("screen-capture", _("Screenshot taken"));
211@@ -349,6 +407,7 @@
212 /// TRANSLATORS: %s represents a timestamp here
213 string file_name = _("Screenshot from %s").printf (date_time);
214 string format = settings.get_string ("format");
215+
216 try {
217 save_file (file_name, format, "", screenshot);
218 } catch (GLib.Error e) {
219@@ -356,12 +415,180 @@
220 debug (e.message);
221 }
222 }
223+
224 this.destroy ();
225 }
226
227 return false;
228 }
229
230+ private Gdk.Pixbuf? get_scaled_pixbuf_from_window (Gdk.Window win, CaptureType mode, int scale_factor) {
231+ int width = win.get_width ();
232+ int height = win.get_height ();
233+ // Check the scaling factor in use, and if greater than 1 scale the image. (for HiDPI displays)
234+ Gdk.Pixbuf? pix;
235+ if (scale_factor > 1 && mode == CaptureType.SCREEN) {
236+ pix = Gdk.pixbuf_get_from_window (win, 0, 0, width / scale_factor, height / scale_factor);
237+ pix.scale (pix, width, height, width, height, 0, 0, scale_factor, scale_factor, Gdk.InterpType.BILINEAR);
238+ } else {
239+ pix = Gdk.pixbuf_get_from_window (win, 0, 0, width, height);
240+ }
241+
242+ return pix;
243+ }
244+
245+
246+ private Gdk.Rectangle remove_translucent_border (Gdk.Pixbuf? pix, Gdk.Rectangle rect, out int offset_x, out int offset_y) {
247+ const int MIN_ALPHA = 128;
248+ const int MIN_COLOR = 10;
249+ offset_x = offset_y = 0;
250+
251+ if (pix == null) {
252+ rect.x = rect.y = rect.width = rect.height = 0;
253+ } else {
254+ /* Measure any dark translucent shadow around pix */
255+ var height = pix.get_height ();
256+ var width = pix.get_width ();
257+ uint8[] bytes = pix.read_pixel_bytes ().get_data ();
258+ var length = pix.get_byte_length ();
259+ var rowstride = pix.rowstride;
260+ var half_row = (width / 2) * 4;
261+
262+ /* Measure top margin */
263+ int count = 0;
264+ uint index = half_row + 3;
265+ while (index < length &&
266+ bytes[index] < MIN_ALPHA &&
267+ bytes[index - 1] < MIN_COLOR &&
268+ bytes[index - 2] < MIN_COLOR &&
269+ bytes[index - 3] < MIN_COLOR) {
270+
271+
272+ index += rowstride;
273+ count++;
274+ }
275+
276+ rect.height -= count;
277+ rect.y += count;
278+ offset_y = count;
279+
280+ /* Measure bottom margin */
281+ count = 0;
282+ index = (uint)(length - half_row + 3);
283+
284+ while (index < length &&
285+ bytes[index] < MIN_ALPHA &&
286+ bytes[index - 1] < MIN_COLOR &&
287+ bytes[index - 2] < MIN_COLOR &&
288+ bytes[index - 3] < MIN_COLOR) {
289+
290+ index -= rowstride;
291+ count++;
292+ }
293+
294+ rect.height -= count;
295+
296+ /* Measure left margin */
297+ count = 0;
298+ index = (uint)(rowstride * (height / 2) + 3);
299+ while (index < length &&
300+ bytes[index] < MIN_ALPHA &&
301+ bytes[index - 1] < MIN_COLOR &&
302+ bytes[index - 2] < MIN_COLOR &&
303+ bytes[index - 3] < MIN_COLOR) {
304+
305+ index += 4;
306+ count++;
307+ }
308+
309+ rect.width -= count;
310+ rect.x += count;
311+ offset_x = count;
312+
313+ /* Measure right margin */
314+ count = 0;
315+ index = rowstride * (height / 2) - 1;
316+ while (index < length &&
317+ bytes[index] < MIN_ALPHA &&
318+ bytes[index - 1] < MIN_COLOR &&
319+ bytes[index - 2] < MIN_COLOR &&
320+ bytes[index - 3] < MIN_COLOR) {
321+
322+ index -= 4;
323+ count++;
324+ }
325+
326+ rect.width -= count;
327+ }
328+
329+ return rect;
330+ }
331+ /*** @pix1 is the subpix of the root screen.
332+ * @pix2 is the pixbuf of the whole window with CSD decorations (if any) including shadows.
333+ * @selection_rect is the area of the whole window, including non-CSD decorations (if any) but excluding shadows.
334+ * @offset_x and @offset_y are zero for non-CSD window since @pix2 is smaller than @selection_rect.
335+ * @offset_x and @offset_y reflect the left and top shadow widths for CSD windows.
336+ * @pix1 will be composited on top of @pix2. The final result
337+ * will contain the whole of each pix, with any gaps transparent.
338+ ***/
339+ private Gdk.Pixbuf composite_pix (Gdk.Pixbuf pix1, Gdk.Pixbuf pix2, Gdk.Rectangle selection_rect,
340+ int offset_x, int offset_y, int scale_factor) {
341+
342+ double offset_x1 = 0.0;
343+ double offset_y1 = 0.0;
344+ double offset_x2 = 0.0;
345+ double offset_y2 = 0.0;
346+ int width1 = pix1.get_width ();
347+ int width2 = pix2.get_width ();
348+ int height1 = pix1.get_height ();
349+ int height2 = pix2.get_height ();
350+
351+ offset_x1 = offset_x;
352+ offset_y1 = offset_y;
353+
354+ bool left_truncation = (selection_rect.width > width1 && selection_rect.x < 0);
355+ bool top_truncation = (selection_rect.height > height1 && selection_rect.y < 0);
356+ int top_truncation_amount = top_truncation ? selection_rect.height - height1 : 0;
357+ bool non_CSD = (selection_rect.height > height2);
358+ int non_CSD_amount = non_CSD ? selection_rect.height - height2 : 0;
359+ int non_CSD_not_showing_amount = non_CSD ? int.min (top_truncation_amount, non_CSD_amount) : 0;
360+
361+ if (left_truncation) {
362+ offset_x1 -= selection_rect.x;
363+ }
364+
365+ if (top_truncation) {
366+ if (!non_CSD) {
367+ offset_y1 -= selection_rect.y;
368+ } else {
369+ offset_y1 -= (selection_rect.y + non_CSD_amount);
370+ }
371+ } else {
372+ offset_y2 = non_CSD_amount;
373+ }
374+
375+ var cairo_width = int.max (selection_rect.width, width2);
376+ var cairo_height = int.max (selection_rect.height - non_CSD_not_showing_amount, height2);
377+
378+ /* Create an Image surface large enough to hold composite image*/
379+ var cs = new Cairo.ImageSurface (Cairo.Format.ARGB32, cairo_width, cairo_height);
380+ var cr = new Cairo.Context (cs);
381+ cr.set_operator (Cairo.Operator.OVER);
382+
383+ cr.set_source_rgba (255, 255, 255, 255); // White background to show shadow
384+ cr.paint ();
385+
386+ cr.translate (offset_x2, offset_y2);
387+ Gdk.cairo_set_source_pixbuf (cr, pix2, 0.0, 0.0);
388+ cr.paint ();
389+
390+ cr.translate (offset_x1 - offset_x2, offset_y1 - offset_y2);
391+ Gdk.cairo_set_source_pixbuf (cr, pix1, 0.0, 0.0);
392+ cr.paint ();
393+
394+ return Gdk.pixbuf_get_from_surface (cs, 0, 0, cairo_width, cairo_height);
395+ }
396+
397 private void save_file (string file_name, string format, string folder_dir, Gdk.Pixbuf screenshot) throws GLib.Error {
398 string full_file_name = "";
399
400@@ -479,9 +706,9 @@
401 var win = selection_area.get_window ();
402
403 selection_area.captured.connect (() => {
404- if (delay == 0) {
405- selection_area.set_opacity (0);
406- }
407+ if (delay == 0) {
408+ selection_area.set_opacity (0);
409+ }
410 selection_area.close ();
411 Timeout.add_seconds (delay - (redact ? 1 : 0), () => {
412 if (from_command == false) {
413@@ -511,10 +738,14 @@
414 settings.set_string ("font-name", "Redacted Script Regular 9");
415 settings.set_string ("monospace-font-name", "Redacted Script Light 10");
416 settings.set_string ("document-font-name", "Redacted Script Regular 10");
417- } else {
418+ } else if (prev_font_regular != null) {
419 settings.set_string ("font-name", prev_font_regular);
420 settings.set_string ("monospace-font-name", prev_font_mono);
421 settings.set_string ("document-font-name", prev_font_document);
422+ } else if (settings.get_string ("font-name").contains ("Redacted")) { /* Fallback in case a program crash leaves the system redacted */
423+ settings.reset ("font-name");
424+ settings.reset ("monospace-font-name");
425+ settings.reset ("document-font-name");
426 }
427 }
428

Subscribers

People subscribed via source and target branches