source: ntrip/trunk/BNC/scripts/helpScreenshots/shoot.py

Last change on this file was 11160, checked in by stuerze, 5 days ago

screenshots are updated

  • Property svn:executable set to *
File size: 10.0 KB
Line 
1#!/usr/bin/env python3
2"""Screenshots for src/bnchelp.html: BNC's GUI on a virtual X display.
3
4Start the display first (Xvfb 2600x1800 + openbox, see start_display.sh), then
5
6 shoot.py [options] [job ...] jobs from jobs.py, default: all
7 shoot.py --list
8
9Options:
10 --user NAME account for the live streams, replaces 'Example:Configs' in
11 the example configurations; the password is taken from the
12 credentials file (~/.netrc), never from the command line
13 --work DIR working directory (default /tmp/bnc_screenshots); BNC runs
14 in DIR/run/<job>, the images are written to DIR/out
15 --install copy the produced FigureNN.png files into src/IMG
16
17The screenshots use Qt's Fusion style and an English locale; the main window
18has a client size of 1200x850. The figure numbers are those of bnchelp.html.
19Environment: BNC_SHOT_DISPLAY (default :99), BNC_SHOT_PREFIX (directory of
20the unpacked packages if xdotool is not installed, see start_display.sh).
21"""
22
23import argparse
24import glob
25import os
26import shutil
27import subprocess
28import sys
29import time
30
31HERE = os.path.dirname(os.path.abspath(__file__))
32BNC_ROOT = os.path.dirname(os.path.dirname(HERE))
33BNC = os.path.join(BNC_ROOT, "bnc")
34DISPLAY = os.environ.get("BNC_SHOT_DISPLAY", ":99")
35PREFIX = os.environ.get("BNC_SHOT_PREFIX", os.path.expanduser("~/.cache/bnc_screenshot_tools"))
36
37if shutil.which("xdotool"):
38 XDOTOOL, XLIBS = "xdotool", ""
39else:
40 XDOTOOL = os.path.join(PREFIX, "root/usr/bin/xdotool")
41 XLIBS = os.path.join(PREFIX, "root/usr/lib/x86_64-linux-gnu")
42XENV = dict(os.environ, DISPLAY=DISPLAY, LD_LIBRARY_PATH=XLIBS)
43BNCENV = dict(os.environ, DISPLAY=DISPLAY, QT_STYLE_OVERRIDE="Fusion", LC_ALL="C.UTF-8",
44 LANG="C.UTF-8", TZ="UTC")
45
46WORK = "/tmp/bnc_screenshots" # set by main()
47OUT = os.path.join(WORK, "out")
48USER = None
49
50
51def xdo(*args):
52 r = subprocess.run([XDOTOOL] + [str(a) for a in args], env=XENV, capture_output=True,
53 text=True)
54 return r.stdout.strip()
55
56
57def wait_window(title, timeout=30, count=1):
58 """id of the (last) visible window whose title contains title; with
59 count > 1 the list of ids once that many exist"""
60 t0 = time.time()
61 while time.time() - t0 < timeout:
62 ids = xdo("search", "--onlyvisible", "--name", title).split()
63 if len(ids) >= count:
64 time.sleep(0.5)
65 return ids[-1] if count == 1 else ids
66 time.sleep(0.3)
67 raise RuntimeError("window '%s' not found" % title)
68
69
70def new_window(before, timeout=60):
71 """first visible window with a title that was not in before"""
72 for _ in range(timeout):
73 new = set(xdo("search", "--onlyvisible", "--name", "").split()) - before
74 new = [w for w in new if xdo("getwindowname", w)]
75 if new:
76 return new[0]
77 time.sleep(1)
78 raise RuntimeError("no new window")
79
80
81def visible_windows():
82 return set(xdo("search", "--onlyvisible", "--name", "").split())
83
84
85def geometry(wid):
86 out = xdo("getwindowgeometry", "--shell", wid)
87 g = dict(l.split("=") for l in out.splitlines())
88 return int(g["X"]), int(g["Y"]), int(g["WIDTH"]), int(g["HEIGHT"])
89
90
91def place(wid, x, y, w=None, h=None):
92 """move (and resize) a window; repeated until the window manager applied it"""
93 time.sleep(1.0)
94 for _ in range(3):
95 if w:
96 xdo("windowsize", "--sync", wid, w, h)
97 xdo("windowmove", "--sync", wid, x, y)
98 time.sleep(0.5)
99 gx, gy, gw, gh = geometry(wid)
100 if (not w or (gw, gh) == (w, h)) and abs(gx - x) < 30 and abs(gy - y) < 40:
101 break
102
103
104def click(wid, x, y, button=1):
105 """click at x, y relative to the client area of window wid"""
106 xdo("mousemove", "--window", wid, x, y)
107 time.sleep(0.2)
108 xdo("click", button)
109 time.sleep(0.8)
110
111
112def key(wid, *keys):
113 xdo("windowactivate", "--sync", wid)
114 for k in keys:
115 xdo("key", "--clearmodifiers", k)
116 time.sleep(0.2)
117
118
119def typetext(wid, text):
120 xdo("windowactivate", "--sync", wid)
121 xdo("type", "--delay", "40", text)
122
123
124def minimize(wid):
125 xdo("windowminimize", wid)
126 time.sleep(0.5)
127
128
129def activate(wid):
130 xdo("windowactivate", "--sync", wid)
131 time.sleep(0.5)
132
133
134def pointer_away():
135 w, h = xdo("getdisplaygeometry").split()
136 xdo("mousemove", int(w) - 10, int(h) - 10)
137 time.sleep(0.5)
138
139
140def saved(path):
141 size = subprocess.run(["identify", "-format", "%wx%h", path], capture_output=True,
142 text=True).stdout
143 print(" saved", os.path.relpath(path, WORK), size)
144 return path
145
146
147def frame():
148 """screen rectangle of everything shown (windows incl. title bars) =
149 bounding box of the non-black part of the root window"""
150 raw = os.path.join(OUT, "_bbox.png")
151 subprocess.run(["import", "-window", "root", raw], env=XENV, check=True)
152 bb = subprocess.run(["convert", raw, "-format", "%@", "info:"], capture_output=True,
153 text=True).stdout.strip() # WxH+X+Y
154 wh, x, y = bb.split("+")
155 w, h = wh.split("x")
156 return int(x), int(y), int(w), int(h)
157
158
159def shot(name, crop=None):
160 """screenshot of all windows shown, or of crop=(x, y, w, h) in screen
161 coordinates (e.g. from frame()); saved as OUT/<name>.png"""
162 pointer_away()
163 raw = os.path.join(OUT, "_root.png")
164 subprocess.run(["import", "-window", "root", raw], env=XENV, check=True)
165 path = os.path.join(OUT, name + ".png")
166 if crop:
167 x, y, w, h = crop
168 subprocess.run(["convert", raw, "-crop", "%dx%d+%d+%d" % (w, h, x, y), "+repage", path],
169 check=True)
170 else:
171 subprocess.run(["convert", raw, "-trim", "+repage", path], check=True)
172 return saved(path)
173
174
175def wshot(name, wid):
176 """screenshot of one window incl. its window manager frame"""
177 pointer_away()
178 path = os.path.join(OUT, name + ".png")
179 subprocess.run(["import", "-frame", "-window", wid, path], env=XENV, check=True)
180 return saved(path)
181
182
183def crop_file(src, name, geometry):
184 """crop an image file, geometry 'WxH+X+Y'"""
185 path = os.path.join(OUT, name + ".png")
186 subprocess.run(["convert", src, "-crop", geometry, "+repage", path], check=True)
187 return saved(path)
188
189
190class Bnc:
191 """BNC instance in its own working directory (Input link, Output dir),
192 started with a copy of an example configuration"""
193
194 def __init__(self, job, conf=None, keys=(), live=False):
195 self.dir = os.path.join(WORK, "run", job)
196 shutil.rmtree(self.dir, ignore_errors=True)
197 os.makedirs(os.path.join(self.dir, "Output"))
198 os.symlink(os.path.join(BNC_ROOT, "Example_Configs", "Input"), os.path.join(self.dir, "Input"))
199 cmd = [BNC]
200 if conf:
201 text = open(os.path.join(BNC_ROOT, "Example_Configs", conf), errors="replace").read()
202 if live and USER:
203 text = text.replace("Example:Configs@", USER + "@")
204 open(os.path.join(self.dir, conf), "w").write(text)
205 cmd += ["--conf", conf]
206 for k, v in keys:
207 if live and USER:
208 v = str(v).replace("Example:Configs@", USER + "@")
209 cmd += ["-key", k, str(v)]
210 self.out = open(os.path.join(self.dir, "bnc.out"), "w")
211 self.p = subprocess.Popen(cmd, cwd=self.dir, env=BNCENV, stdout=self.out,
212 stderr=subprocess.STDOUT)
213 self.main = wait_window("BKG Ntrip Client")
214
215 def stop(self):
216 if self.p.poll() is None:
217 self.p.terminate()
218 try:
219 self.p.wait(10)
220 except subprocess.TimeoutExpired:
221 self.p.kill()
222 time.sleep(1)
223
224
225def cleanup():
226 """end leftover BNC instances of this driver: processes with a window on
227 the virtual display that run BNC_ROOT/bnc with this DISPLAY (nothing else)"""
228 pids = set()
229 for w in xdo("search", "--name", "").split():
230 p = xdo("getwindowpid", w)
231 if p:
232 pids.add(int(p))
233 for p in pids:
234 try:
235 cmd = open("/proc/%d/cmdline" % p, "rb").read().split(b"\0")[0].decode()
236 env = open("/proc/%d/environ" % p, "rb").read().split(b"\0")
237 except OSError:
238 continue
239 if cmd == BNC and ("DISPLAY=" + DISPLAY).encode() in env:
240 os.kill(p, 15)
241 print(" ended leftover BNC", p)
242 time.sleep(1)
243
244
245def main():
246 global WORK, OUT, USER
247 ap = argparse.ArgumentParser(description=__doc__,
248 formatter_class=argparse.RawDescriptionHelpFormatter)
249 ap.add_argument("jobs", nargs="*")
250 ap.add_argument("--user")
251 ap.add_argument("--work", default=WORK)
252 ap.add_argument("--install", action="store_true")
253 ap.add_argument("--list", action="store_true")
254 args = ap.parse_args()
255
256 # jobs.py imports this file as module 'shoot' (a second instance besides
257 # __main__): the settings have to be made there
258 sys.path.insert(0, HERE)
259 import shoot as S
260 S.WORK, S.USER = os.path.abspath(args.work), args.user
261 S.OUT = os.path.join(S.WORK, "out")
262 os.makedirs(S.OUT, exist_ok=True)
263 WORK, OUT, USER = S.WORK, S.OUT, S.USER
264 import jobs
265 if args.list:
266 for n in sorted(jobs.JOBS):
267 print("%-10s %s" % (n, (jobs.JOBS[n].__doc__ or "").strip().splitlines()[0]))
268 return 0
269 if not os.path.exists("/tmp/.X11-unix/X" + DISPLAY.lstrip(":")):
270 sys.exit("display %s not running, start it with start_display.sh" % DISPLAY)
271
272 failed = []
273 for n in args.jobs or sorted(jobs.JOBS):
274 print("job", n)
275 S.cleanup()
276 try:
277 jobs.JOBS[n]()
278 except Exception as e:
279 print(" FAILED:", e)
280 failed.append(n)
281 S.cleanup()
282
283 if args.install:
284 for f in sorted(glob.glob(os.path.join(OUT, "Figure[0-9][0-9].png"))):
285 shutil.copy(f, os.path.join(BNC_ROOT, "src", "IMG"))
286 print(" installed", os.path.basename(f))
287 if failed:
288 print("failed:", " ".join(failed))
289 return 1 if failed else 0
290
291
292if __name__ == "__main__":
293 sys.exit(main())
Note: See TracBrowser for help on using the repository browser.