5e8bdfa77365ab119cc7ccd39042e03a0fd17fa7
1import sys
2sys.coinit_flags= 0
3import win32com.client
4import pywintypes
5import os
6import shutil
7import http_server_39 as server
8#import http.server as server
9import socketserver
10import threading
11import asyncio
12import websockets
13import logging, json
14import urllib
15import posixpath
16import time
17import pythoncom
18import pystray
19import tkinter as tk
20from tkinter import ttk
21from PIL import Image, ImageDraw
22
23logging.basicConfig()
24
25global STATE
26global STATE_DEFAULT
27global current_slideshow
28current_slideshow = None
29CACHEDIR = r'''C:\Windows\Temp\ppt-cache'''
30CACHE_TIMEOUT = 2*60*60
31
32class Handler(server.SimpleHTTPRequestHandler):
33 def __init__(self, *args, **kwargs):
34 super().__init__(*args, directory=os.path.dirname(os.path.realpath(__file__)))
35
36 def translate_path(self, path):
37 """Translate a /-separated PATH to the local filename syntax.
38
39 Components that mean special things to the local file system
40 (e.g. drive or directory names) are ignored. (XXX They should
41 probably be diagnosed.)
42
43 """
44 # abandon query parameters
45 path = path.split('?',1)[0]
46 path = path.split('#',1)[0]
47 # Don't forget explicit trailing slash when normalizing. Issue17324
48 trailing_slash = path.rstrip().endswith('/')
49 try:
50 path = urllib.parse.unquote(path, errors='surrogatepass')
51 except UnicodeDecodeError:
52 path = urllib.parse.unquote(path)
53 path = posixpath.normpath(path)
54 words = path.split('/')
55 words = list(filter(None, words))
56 if len(words) > 0 and words[0] == "cache":
57 if current_slideshow:
58 path = CACHEDIR + "\\" + current_slideshow.name()
59 else:
60 path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "black.jpg") + '/'
61 return path
62 words.pop(0)
63 else:
64 path = self.directory
65 for word in words:
66 if os.path.dirname(word) or word in (os.curdir, os.pardir):
67 # Ignore components that are not a simple file/directory name
68 continue
69 path = os.path.join(path, word)
70 if trailing_slash:
71 path += '/'
72 return path
73
74
75def run_http():
76 http_server = server.HTTPServer(("", 80), Handler)
77 http_server.serve_forever()
78
79STATE_DEFAULT = {"connected": 0, "current": 0, "total": 0, "visible": 0, "name": ""}
80STATE = STATE_DEFAULT
81USERS = set()
82
83
84def state_event():
85 print("Running state event")
86 return json.dumps({"type": "state", **STATE})
87
88
89def users_event():
90 return json.dumps({"type": "users", "count": len(USERS)})
91
92
93async def notify_state():
94 print("Notifying state to " + str(len(USERS)) + " users")
95 global STATE
96 if current_slideshow and STATE["connected"] == 1:
97 STATE["current"] = current_slideshow.current_slide()
98 STATE["total"] = current_slideshow.total_slides()
99 STATE["visible"] = current_slideshow.visible()
100 STATE["name"] = current_slideshow.name()
101 else:
102 STATE = STATE_DEFAULT
103 if USERS: # asyncio.wait doesn't accept an empty list
104 message = state_event()
105 await asyncio.wait([user.send(message) for user in USERS])
106
107
108async def notify_users():
109 if USERS: # asyncio.wait doesn't accept an empty list
110 message = users_event()
111 await asyncio.wait([user.send(message) for user in USERS])
112
113
114async def register(websocket):
115 USERS.add(websocket)
116 await notify_users()
117
118
119async def unregister(websocket):
120 USERS.remove(websocket)
121 await notify_users()
122
123
124async def ws_handle(websocket, path):
125 print("Received command")
126 global current_slideshow
127 # register(websocket) sends user_event() to websocket
128 await register(websocket)
129 try:
130 await websocket.send(state_event())
131 async for message in websocket:
132 data = json.loads(message)
133 if data["action"] == "prev":
134 if current_slideshow:
135 current_slideshow.prev()
136 await notify_state()
137 elif data["action"] == "next":
138 if current_slideshow:
139 current_slideshow.next()
140 await notify_state()
141 elif data["action"] == "first":
142 if current_slideshow:
143 current_slideshow.first()
144 await notify_state()
145 elif data["action"] == "last":
146 if current_slideshow:
147 current_slideshow.last()
148 await notify_state()
149 elif data["action"] == "black":
150 if current_slideshow:
151 if current_slideshow.visible() == 3:
152 current_slideshow.normal()
153 else:
154 current_slideshow.black()
155 await notify_state()
156 elif data["action"] == "white":
157 if current_slideshow:
158 if current_slideshow.visible() == 4:
159 current_slideshow.normal()
160 else:
161 current_slideshow.white()
162 await notify_state()
163 elif data["action"] == "goto":
164 if current_slideshow:
165 current_slideshow.goto(int(data["value"]))
166 await notify_state()
167 elif data["action"] == "refresh":
168 print("Received refresh command")
169 await notify_state()
170 if current_slideshow:
171 current_slideshow.export_current_next()
172 current_slideshow.refresh()
173 else:
174 logging.error("unsupported event: {}", data)
175 finally:
176 await unregister(websocket)
177
178def run_ws():
179 # https://stackoverflow.com/questions/21141217/how-to-launch-win32-applications-in-separate-threads-in-python/22619084#22619084
180 # https://www.reddit.com/r/learnpython/comments/mwt4qi/pywintypescom_error_2147417842_the_application/
181 pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED)
182 asyncio.set_event_loop(asyncio.new_event_loop())
183 start_server = websockets.serve(ws_handle, "0.0.0.0", 5678, ping_interval=None)
184 asyncio.get_event_loop().run_until_complete(start_server)
185 asyncio.get_event_loop().run_forever()
186
187def start_server():
188 http_daemon = threading.Thread(name="http_daemon", target=run_http)
189 http_daemon.setDaemon(True)
190 http_daemon.start()
191 print("Started HTTP server")
192
193 ws_daemon = threading.Thread(name="ws_daemon", target=run_ws)
194 ws_daemon.setDaemon(True)
195 ws_daemon.start()
196 print("Started websocket server")
197
198class Slideshow:
199 def __init__(self, instance):
200 self.instance = instance
201 if self.instance is None:
202 raise ValueError("PPT instance cannot be None")
203
204 if self.instance.SlideShowWindows.Count == 0:
205 raise ValueError("PPT instance has no slideshow windows")
206 self.view = self.instance.SlideShowWindows[0].View
207
208 if self.instance.ActivePresentation is None:
209 raise ValueError("PPT instance has no active presentation")
210 self.presentation = self.instance.ActivePresentation
211
212 def unload(self):
213 connect_ppt()
214
215 def refresh(self):
216 try:
217 if self.instance is None:
218 raise ValueError("PPT instance cannot be None")
219
220 if self.instance.SlideShowWindows.Count == 0:
221 raise ValueError("PPT instance has no slideshow windows")
222 self.view = self.instance.SlideShowWindows[0].View
223
224 if self.instance.ActivePresentation is None:
225 raise ValueError("PPT instance has no active presentation")
226 except:
227 self.unload()
228
229 def total_slides(self):
230 try:
231 self.refresh()
232 return len(self.presentation.Slides)
233 except ValueError or pywintypes.com_error:
234 self.unload()
235
236 def current_slide(self):
237 try:
238 self.refresh()
239 return self.view.CurrentShowPosition
240 except ValueError or pywintypes.com_error:
241 self.unload()
242
243 def visible(self):
244 try:
245 self.refresh()
246 return self.view.State
247 except ValueError or pywintypes.com_error:
248 self.unload()
249
250 def prev(self):
251 try:
252 self.refresh()
253 self.view.Previous()
254 self.export_current_next()
255 except ValueError or pywintypes.com_error:
256 self.unload()
257
258 def next(self):
259 try:
260 self.refresh()
261 self.view.Next()
262 self.export_current_next()
263 except ValueError or pywintypes.com_error:
264 self.unload()
265
266 def first(self):
267 try:
268 self.refresh()
269 self.view.First()
270 self.export_current_next()
271 except ValueError or pywintypes.com_error:
272 self.unload()
273
274 def last(self):
275 try:
276 self.refresh()
277 self.view.Last()
278 self.export_current_next()
279 except ValueError or pywintypes.com_error:
280 self.unload()
281
282 def goto(self, slide):
283 try:
284 self.refresh()
285 if slide <= self.total_slides():
286 self.view.GotoSlide(slide)
287 else:
288 self.last()
289 self.next()
290 self.export_current_next()
291 except ValueError or pywintypes.com_error:
292 self.unload()
293
294 def black(self):
295 try:
296 self.refresh()
297 self.view.State = 3
298 self.export_current_next()
299 except ValueError or pywintypes.com_error:
300 self.unload()
301
302 def white(self):
303 try:
304 self.refresh()
305 self.view.State = 4
306 self.export_current_next()
307 except ValueError or pywintypes.com_error:
308 self.unload()
309
310 def normal(self):
311 try:
312 self.refresh()
313 self.view.State = 1
314 self.export_current_next()
315 except ValueError or pywintypes.com_error:
316 self.unload()
317
318 def name(self):
319 try:
320 self.refresh()
321 return self.presentation.Name
322 except ValueError or pywintypes.com_error:
323 self.unload()
324
325
326 def export_current_next(self):
327 self.export(self.current_slide())
328 self.export(self.current_slide() + 1)
329
330 def export(self, slide):
331 destination = CACHEDIR + "\\" + self.name() + "\\" + str(slide) + ".jpg"
332 os.makedirs(os.path.dirname(destination), exist_ok=True)
333 if not os.path.exists(destination) or time.time() - os.path.getmtime(destination) > CACHE_TIMEOUT:
334 if slide <= self.total_slides():
335 attempts = 0
336 while attempts < 3:
337 try:
338 self.presentation.Slides(slide).Export(destination, "JPG")
339 break
340 except:
341 pass
342 attempts += 1
343 elif slide == self.total_slides() + 1:
344 shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)) + r'''\black.jpg''', 'rb'), open(destination, 'wb'))
345 else:
346 pass
347
348 def export_all(self):
349 for i in range(1, self.total_slides()):
350 self.export(i)
351
352def get_ppt_instance():
353 instance = win32com.client.Dispatch('Powerpoint.Application')
354 if instance is None or instance.SlideShowWindows.Count == 0:
355 return None
356 return instance
357
358def get_current_slideshow():
359 return current_slideshow
360
361def connect_ppt():
362 global STATE
363 if STATE["connected"] == 1:
364 print("Disconnected from PowerPoint instance")
365 STATE = STATE_DEFAULT
366 while True:
367 try:
368 instance = get_ppt_instance()
369 global current_slideshow
370 current_slideshow = Slideshow(instance)
371 STATE["connected"] = 1
372 STATE["current"] = current_slideshow.current_slide()
373 STATE["total"] = current_slideshow.total_slides()
374 print("Connected to PowerPoint instance")
375 current_slideshow.export_all()
376 break
377 except ValueError as e:
378 current_slideshow = None
379 pass
380 time.sleep(1)
381
382def start(_=None):
383 #root = tk.Tk()
384 #root.iconphoto(False, tk.PhotoImage(file="icons/ppt.png"))
385 #root.geometry("250x150+300+300")
386 #app = Interface(root)
387 #interface_thread = threading.Thread(target=root.mainloop())
388 #interface_thread.setDaemon(True)
389 #interface_thread.start()
390 start_server()
391 connect_ppt()
392
393
394def null_action():
395 pass
396
397class Interface(ttk.Frame):
398
399 def __init__(self, parent):
400 ttk.Frame.__init__(self, parent)
401
402 self.parent = parent
403
404 self.initUI()
405
406 def initUI(self):
407
408 self.parent.title("ppt-control")
409 self.style = ttk.Style()
410 #self.style.theme_use("default")
411
412 self.pack(fill=tk.BOTH, expand=1)
413
414 quitButton = ttk.Button(self, text="Close",
415 command=self.quit)
416 quitButton.place(x=50, y=50)
417 status_label = ttk.Label(self, text="PowerPoint status: not detected")
418 status_label.place(x=10,y=10)
419
420
421
422def show_icon():
423 menu = (pystray.MenuItem("Status", lambda: null_action(), enabled=False),
424 pystray.MenuItem("Restart", lambda: start()),
425 pystray.MenuItem("Settings", lambda: open_settings()))
426 icon = pystray.Icon("ppt-control", Image.open("icons/ppt.ico"), "ppt-control", menu)
427 icon.visible = True
428 icon.run(setup=start)
429
430if __name__ == "__main__":
431 show_icon()