ppt_control.pyon commit add keyboard shortcuts, icons, preloading (d403fb6)
   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
  18
  19logging.basicConfig()
  20
  21global current_slideshow
  22current_slideshow = None
  23CACHEDIR = r'''C:\Windows\Temp\ppt-cache'''
  24CACHE_TIMEOUT = 2*60*60
  25
  26class Handler(server.SimpleHTTPRequestHandler):
  27    def __init__(self, *args, **kwargs):
  28        super().__init__(*args, directory=os.path.dirname(os.path.realpath(__file__)))
  29        
  30    def translate_path(self, path):
  31        """Translate a /-separated PATH to the local filename syntax.
  32
  33        Components that mean special things to the local file system
  34        (e.g. drive or directory names) are ignored.  (XXX They should
  35        probably be diagnosed.)
  36
  37        """
  38        # abandon query parameters
  39        path = path.split('?',1)[0]
  40        path = path.split('#',1)[0]
  41        # Don't forget explicit trailing slash when normalizing. Issue17324
  42        trailing_slash = path.rstrip().endswith('/')
  43        try:
  44            path = urllib.parse.unquote(path, errors='surrogatepass')
  45        except UnicodeDecodeError:
  46            path = urllib.parse.unquote(path)
  47        path = posixpath.normpath(path)
  48        words = path.split('/')
  49        words = list(filter(None, words))
  50        if len(words) > 0 and words[0] == "cache":
  51            if current_slideshow:
  52                path = CACHEDIR + "\\" + current_slideshow.name()
  53            words.pop(0)
  54        else:
  55            path = self.directory
  56        for word in words:
  57            if os.path.dirname(word) or word in (os.curdir, os.pardir):
  58                # Ignore components that are not a simple file/directory name
  59                continue
  60            path = os.path.join(path, word)
  61        if trailing_slash:
  62            path += '/'
  63        return path
  64
  65
  66def run_http():
  67    http_server = server.HTTPServer(("", 80), Handler)
  68    http_server.serve_forever()
  69
  70STATE = {"connected": 0, "current": 0, "total": 0, "visible": 0, "name": ""}
  71USERS = set()
  72
  73
  74def state_event():
  75    return json.dumps({"type": "state", **STATE})
  76
  77
  78def users_event():
  79    return json.dumps({"type": "users", "count": len(USERS)})
  80
  81
  82async def notify_state():
  83    global current_slideshow
  84    if current_slideshow:
  85        STATE["current"] = current_slideshow.current_slide()
  86        STATE["total"] = current_slideshow.total_slides()
  87        STATE["visible"] = current_slideshow.visible()
  88        STATE["name"] = current_slideshow.name()
  89    if USERS:  # asyncio.wait doesn't accept an empty list
  90        message = state_event()
  91        await asyncio.wait([user.send(message) for user in USERS])
  92
  93
  94async def notify_users():
  95    if USERS:  # asyncio.wait doesn't accept an empty list
  96        message = users_event()
  97        await asyncio.wait([user.send(message) for user in USERS])
  98
  99
 100async def register(websocket):
 101    USERS.add(websocket)
 102    await notify_users()
 103
 104
 105async def unregister(websocket):
 106    USERS.remove(websocket)
 107    await notify_users()
 108
 109
 110async def ws_handle(websocket, path):
 111    global current_slideshow
 112    # register(websocket) sends user_event() to websocket
 113    await register(websocket)
 114    try:
 115        await websocket.send(state_event())
 116        async for message in websocket:
 117            data = json.loads(message)
 118            if data["action"] == "prev":
 119                if current_slideshow:
 120                    current_slideshow.prev()
 121                await notify_state()
 122            elif data["action"] == "next":
 123                if current_slideshow:
 124                    current_slideshow.next()
 125                await notify_state()
 126            elif data["action"] == "first":
 127                if current_slideshow:
 128                    current_slideshow.first()
 129                await notify_state()
 130            elif data["action"] == "last":
 131                if current_slideshow:
 132                    current_slideshow.last()
 133                await notify_state()
 134            elif data["action"] == "black":
 135                if current_slideshow:
 136                    if current_slideshow.visible() == 3:
 137                        current_slideshow.normal()
 138                    else:
 139                        current_slideshow.black()
 140                await notify_state()
 141            elif data["action"] == "white":
 142                if current_slideshow:
 143                    if current_slideshow.visible() == 4:
 144                        current_slideshow.normal()
 145                    else:
 146                        current_slideshow.white()
 147                await notify_state()
 148            elif data["action"] == "goto":
 149                if current_slideshow:
 150                    current_slideshow.goto(int(data["value"]))
 151                await notify_state()
 152            elif data["action"] == "refresh":
 153                if current_slideshow:
 154                    current_slideshow.export_current_next()
 155                    current_slideshow.refresh()
 156                await notify_state()
 157            else:
 158                logging.error("unsupported event: {}", data)
 159    finally:
 160        await unregister(websocket)
 161
 162def run_ws():
 163    pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED)
 164    asyncio.set_event_loop(asyncio.new_event_loop())
 165    start_server = websockets.serve(ws_handle, "0.0.0.0", 5678)
 166    asyncio.get_event_loop().run_until_complete(start_server)
 167    asyncio.get_event_loop().run_forever()
 168
 169def start_server():
 170    #STATE["current"] = current_slide()
 171    http_daemon = threading.Thread(name="http_daemon", target=run_http)
 172    http_daemon.setDaemon(True)
 173    http_daemon.start()
 174    print("Started HTTP server")
 175
 176    #run_ws()
 177    
 178    ws_daemon = threading.Thread(name="ws_daemon", target=run_ws)
 179    ws_daemon.setDaemon(True)
 180    ws_daemon.start()
 181    print("Started websocket server")
 182
 183    #try:
 184    #    ws_daemon.start()
 185    #    http_daemon.start()
 186    #except (KeyboardInterrupt, SystemExit):
 187    #    cleanup_stop_thread()
 188    #    sys.exit()
 189
 190class Slideshow:
 191    def __init__(self, instance):
 192        self.instance = instance
 193        if self.instance is None:
 194            raise ValueError("PPT instance cannot be None")
 195
 196        if self.instance.SlideShowWindows.Count == 0:
 197            raise ValueError("PPT instance has no slideshow windows")
 198        self.view = self.instance.SlideShowWindows[0].View
 199
 200        if self.instance.ActivePresentation is None:
 201            raise ValueError("PPT instance has no  active presentation")
 202        self.presentation = self.instance.ActivePresentation
 203        
 204        self.export_all()
 205
 206    def refresh(self):
 207        if self.instance is None:
 208            raise ValueError("PPT instance cannot be None")
 209
 210        #if self.instance.SlideShowWindows.Count == 0:
 211        #    raise ValueError("PPT instance has no slideshow windows")
 212        self.view = self.instance.SlideShowWindows[0].View
 213
 214        if self.instance.ActivePresentation is None:
 215            raise ValueError("PPT instance has no  active presentation")
 216
 217    def total_slides(self):
 218        return len(self.presentation.Slides)
 219
 220    def current_slide(self):
 221        return self.view.CurrentShowPosition
 222
 223    def visible(self):
 224        return self.view.State
 225
 226    def prev(self):
 227        self.refresh()
 228        self.view.Previous()
 229
 230    def next(self):
 231        self.refresh()
 232        self.view.Next()
 233        self.export_current_next()
 234
 235    def first(self):
 236        self.refresh()
 237        self.view.First()
 238        self.export_current_next()
 239                
 240    def last(self):
 241        self.refresh()
 242        self.view.Last()
 243        self.export_current_next()
 244
 245    def goto(self, slide):
 246        self.refresh()
 247        if slide <= self.total_slides():
 248            self.view.GotoSlide(slide)
 249        else:
 250            self.last()
 251            self.next()
 252        self.export_current_next()
 253
 254    def black(self):
 255        self.refresh()
 256        self.view.State = 3
 257
 258    def white(self):
 259        self.refresh()
 260        self.view.State = 4
 261
 262    def normal(self):
 263        self.refresh()
 264        self.view.State = 1
 265
 266    def name(self):
 267        return self.presentation.Name
 268
 269    def export_current_next(self):
 270        self.export(self.current_slide())
 271        self.export(self.current_slide() + 1)
 272
 273    def export(self, slide):
 274        destination = CACHEDIR + "\\" + self.name() + "\\" + str(slide) + ".jpg"
 275        os.makedirs(os.path.dirname(destination), exist_ok=True)
 276        if not os.path.exists(destination) or time.time() - os.path.getmtime(destination) > CACHE_TIMEOUT:
 277            if slide <= self.total_slides():
 278                attempts = 0
 279                while attempts < 3:
 280                    try:
 281                        self.presentation.Slides(slide).Export(destination, "JPG")
 282                        time.sleep(0.5)
 283                        break
 284                    except:
 285                        pass
 286                    attempts += 1
 287            elif slide == self.total_slides() + 1:
 288                shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)) + r'''\black.jpg''', 'rb'), open(destination, 'wb'))
 289            else:
 290                pass
 291
 292    def export_all(self):
 293        for i in range(1, self.total_slides()):
 294            self.export(i)
 295
 296def get_ppt_instance():
 297    instance = win32com.client.Dispatch('Powerpoint.Application')
 298    if instance is None or instance.SlideShowWindows.Count == 0:
 299        return None
 300    return instance
 301
 302def get_current_slideshow():
 303    return current_slideshow
 304
 305
 306if __name__ == "__main__":
 307
 308    start_server()
 309    
 310    while True:
 311        # Check if PowerPoint is running
 312        instance = get_ppt_instance()
 313        try:
 314            current_slideshow = Slideshow(instance)
 315            STATE["connected"] = 1
 316            STATE["current"] = current_slideshow.current_slide()
 317            STATE["total"] = current_slideshow.total_slides()
 318            print("Connected to PowerPoint instance")
 319            break
 320        except ValueError as e:
 321            current_slideshow = None
 322            pass
 323        time.sleep(1)