diff --git a/openlp/plugins/presentations/lib/libreofficeserver.py b/openlp/plugins/presentations/lib/libreofficeserver.py new file mode 100644 index 000000000..6d2b5752c --- /dev/null +++ b/openlp/plugins/presentations/lib/libreofficeserver.py @@ -0,0 +1,361 @@ +""" +This module runs a Pyro4 server using LibreOffice's version of Python +""" +from subprocess import Popen +import sys +import os +import logging + +# Add the vendor directory to sys.path so that we can load Pyro4 +sys.path.append(os.path.join(os.path.dirname(__file__), 'vendor')) + +from Pyro4 import Daemon, expose, locateNS +from com.sun.star.beans import PropertyValue +from com.sun.star.task import ErrorCodeIOException +import uno + +logging.basicConfig(filename=os.path.dirname(__file__) + '/libreofficeserver.log', level=logging.INFO) +log = logging.getLogger(__name__) + + +class TextType(object): + """ + Type Enumeration for Types of Text to request + """ + Title = 0 + SlideText = 1 + Notes = 2 + + +@expose +class LibreOfficeServer(object): + """ + A Pyro4 server which controls LibreOffice + """ + def __init__(self): + """ + Set up the LibreOffice server + """ + self._init_impress() + + def _init_impress(self) + """ + Initialise Impress + """ + uno_command = [ + '/Applications/LibreOffice.app/Contents/MacOS/soffice', + '--nologo', + '--norestore', + '--minimized', + '--nodefault', + '--nofirststartwizard', + '"--accept=pipe,name=openlp_pipe;urp;"' + ] + self._process = Popen(uno_command) + uno_instance = None + context = uno.getComponentContext() + resolver = context.ServiceManager.createInstanceWithContext('com.sun.star.bridge.UnoUrlResolver', context) + while uno_instance is None and loop < 3: + try: + uno_instance = resolver.resolve('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext') + except: + log.warning('Unable to find running instance ') + loop += 1 + try: + self._manager = uno_instance.ServiceManager + log.debug('get UNO Desktop Openoffice - createInstanceWithContext - Desktop') + self._desktop = self.manager.createInstanceWithContext("com.sun.star.frame.Desktop", uno_instance) + except: + log.warning('Failed to get UNO desktop') + + def _create_property(self, name, value): + """ + Create an OOo style property object which are passed into some Uno methods. + """ + log.debug('create property') + property_object = PropertyValue() + property_object.Name = name + property_object.Value = value + return property_object + + def _get_text_from_page(self, slide_no, text_type=TextType.SlideText): + """ + Return any text extracted from the presentation page. + + :param slide_no: The slide the notes are required for, starting at 1 + :param notes: A boolean. If set the method searches the notes of the slide. + :param text_type: A TextType. Enumeration of the types of supported text. + """ + text = '' + if TextType.Title <= text_type <= TextType.Notes: + pages = self._document.getDrawPages() + if 0 < slide_no <= pages.getCount(): + page = pages.getByIndex(slide_no - 1) + if text_type == TextType.Notes: + page = page.getNotesPage() + for index in range(page.getCount()): + shape = page.getByIndex(index) + shape_type = shape.getShapeType() + if shape.supportsService("com.sun.star.drawing.Text"): + # if they requested title, make sure it is the title + if text_type != TextType.Title or shape_type == "com.sun.star.presentation.TitleTextShape": + text += shape.getString() + '\n' + return text + + def has_desktop(self): + """ + Say if we have a desktop object + """ + return self._desktop is not None + + def shutdown(self): + """ + Shut down the server + """ + while self._docs: + self._docs[0].close_presentation() + if not self._desktop: + return + docs = self._desktop.getComponents() + count = 0 + if docs.hasElements(): + list_elements = docs.createEnumeration() + while list_elements.hasMoreElements(): + doc = list_elements.nextElement() + if doc.getImplementationName() != 'com.sun.star.comp.framework.BackingComp': + count += 1 + if count > 0: + log.debug('LibreOffice not terminated as docs are still open') + else: + try: + self._desktop.terminate() + log.debug('LibreOffice killed') + except: + log.warning('Failed to terminate LibreOffice') + + def load_presentation(self, file_path, screen_number): + """ + Load a presentation + """ + self._file_path = file_path + url = uno.systemPathToFileUrl(file_path) + properties = (self._create_property('Hidden', True),) + try: + self._document = self._desktop.loadComponentFromURL(url, '_blank', 0, properties) + except: + log.warning('Failed to load presentation {url}'.format(url=url)) + return False + self._presentation = self._document.getPresentation() + self._presentation.Display = screen_number + self._control = None + self.create_thumbnails() + self.create_titles_and_notes() + return True + + def create_thumbnails(self, temp_folder): + """ + Create thumbnails for the presentation + """ + thumb_dir_url = uno.systemPathToFileUrl(temp_folder) + properties = (self._create_property('FilterName', 'impress_png_Export'),) + doc = self.document + pages = doc.getDrawPages() + if not pages: + return + if not os.path.isdir(temp_folder): + os.makedirs(temp_folder) + for index in range(pages.getCount()): + page = pages.getByIndex(index) + doc.getCurrentController().setCurrentPage(page) + url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1)) + path = os.path.join(self.get_temp_folder(), str(index + 1) + '.png') + try: + doc.storeToURL(url_path, properties) + self.convert_thumbnail(path, index + 1) + delete_file(path) + except ErrorCodeIOException as exception: + log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode)) + except: + log.exception('{path} - Unable to store openoffice preview'.format(path=path)) + + def get_title_and_notes(self): + """ + Writes the list of titles (one per slide) to 'titles.txt' and the notes to 'slideNotes[x].txt' + in the thumbnails directory + """ + titles = [] + notes = [] + pages = self._document.getDrawPages() + for slide_no in range(1, pages.getCount() + 1): + titles.append(self._get_text_from_page(slide_no, TextType.Title).replace('\n', ' ') + '\n') + note = self._get_text_from_page(slide_no, TextType.Notes) + if len(note) == 0: + note = ' ' + notes.append(note) + return titles, notes + + def close_presentation(self): + """ + Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being + shutdown. + """ + log.debug('close Presentation LibreOffice') + if self._document: + if self._presentation: + try: + self._presentation.end() + self._presentation = None + self._document.dispose() + except: + log.warning("Closing presentation failed") + self._document = None + + def is_loaded(self): + """ + Returns true if a presentation is loaded. + """ + log.debug('is loaded LibreOffice') + if self._presentation is None or self._document is None: + log.debug("is_loaded: no presentation or document") + return False + try: + if self._document.getPresentation() is None: + log.debug("getPresentation failed to find a presentation") + return False + except: + log.warning("getPresentation failed to find a presentation") + return False + return True + + def is_active(self): + """ + Returns true if a presentation is active and running. + """ + log.debug('is active LibreOffice') + if not self.is_loaded(): + return False + return self._control.isRunning() if self._control else False + + def unblank_screen(self): + """ + Unblanks the screen. + """ + log.debug('unblank screen LibreOffice') + return self._control.resume() + + def blank_screen(self): + """ + Blanks the screen. + """ + log.debug('blank screen LibreOffice') + self._control.blankScreen(0) + + def is_blank(self): + """ + Returns true if screen is blank. + """ + log.debug('is blank LibreOffice') + if self._control and self._control.isRunning(): + return self._control.isPaused() + else: + return False + + def stop_presentation(self): + """ + Stop the presentation, remove from screen. + """ + log.debug('stop presentation LibreOffice') + self._presentation.end() + self._control = None + + def start_presentation(self): + """ + Start the presentation from the beginning. + """ + log.debug('start presentation LibreOffice') + if self._control is None or not self._control.isRunning(): + window = self._document.getCurrentController().getFrame().getContainerWindow() + window.setVisible(True) + self._presentation.start() + self._control = self._presentation.getController() + # start() returns before the Component is ready. Try for 15 seconds. + sleep_count = 1 + while not self._control and sleep_count < 150: + time.sleep(0.1) + sleep_count += 1 + self._control = self._presentation.getController() + window.setVisible(False) + else: + self._control.activate() + self.goto_slide(1) + + def get_slide_number(self): + """ + Return the current slide number on the screen, from 1. + """ + return self._control.getCurrentSlideIndex() + 1 + + def get_slide_count(self): + """ + Return the total number of slides. + """ + return self._document.getDrawPages().getCount() + + def goto_slide(self, slide_no): + """ + Go to a specific slide (from 1). + + :param slide_no: The slide the text is required for, starting at 1 + """ + self._control.gotoSlideIndex(slide_no - 1) + + def next_step(self): + """ + Triggers the next effect of slide on the running presentation. + """ + is_paused = self._control.isPaused() + self._control.gotoNextEffect() + time.sleep(0.1) + if not is_paused and self._control.isPaused(): + self._control.gotoPreviousEffect() + + def previous_step(self): + """ + Triggers the previous slide on the running presentation. + """ + self._control.gotoPreviousEffect() + + def get_slide_text(self, slide_no): + """ + Returns the text on the slide. + + :param slide_no: The slide the text is required for, starting at 1 + """ + return self._get_text_from_page(slide_no) + + def get_slide_notes(self, slide_no): + """ + Returns the text in the slide notes. + + :param slide_no: The slide the notes are required for, starting at 1 + """ + return self._get_text_from_page(slide_no, TextType.Notes) + + +def main(): + """ + The main function which runs the server + """ + daemon = Daemon() + ns = locateNS() + uri = daemon.register(LibreOfficeServer) + ns.register('openlp.libreofficeserver', uri) + try: + daemon.requestLoop() + finally: + daemon.close() + + +if __name__ == '__main__': + main() + diff --git a/openlp/plugins/presentations/lib/maclocontroller.py b/openlp/plugins/presentations/lib/maclocontroller.py new file mode 100644 index 000000000..8083d379d --- /dev/null +++ b/openlp/plugins/presentations/lib/maclocontroller.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### + +# OOo API documentation: +# http://api.openoffice.org/docs/common/ref/com/sun/star/presentation/XSlideShowController.html +# http://wiki.services.openoffice.org/wiki/Documentation/DevGuide/ProUNO/Basic +# /Getting_Information_about_UNO_Objects#Inspecting_interfaces_during_debugging +# http://docs.go-oo.org/sd/html/classsd_1_1SlideShow.html +# http://www.oooforum.org/forum/viewtopic.phtml?t=5252 +# http://wiki.services.openoffice.org/wiki/Documentation/DevGuide/Working_with_Presentations +# http://mail.python.org/pipermail/python-win32/2008-January/006676.html +# http://www.linuxjournal.com/content/starting-stopping-and-connecting-openoffice-python +# http://nxsy.org/comparing-documents-with-openoffice-and-python + +import logging +import os +import time +from subprocess import Popen +from multiprocessing import Process + +from openlp.core.common import is_mac, Registry, delete_file + +if is_mac() and os.path.exists('/Applications/LibreOffice.app'): + macuno_available = True +else: + macuno_available = False + +from PyQt5 import QtCore +from Pyro4 import Proxy +from Pyro4.naming import NameServerDaemon + +from openlp.core.lib import ScreenList +from .presentationcontroller import PresentationController, PresentationDocument, TextType + + +log = logging.getLogger(__name__) + + +def run_nameserver(): + """ + Run a Pyro4 nameserver + """ + ns_daemon = NameServerDaemon() + ns_daemon._pyroHmacKey = 'openlp-libreoffice' + try: + ns_daemon.requestLoop() + finally: + ns_daemon.close() + + +class MacLOController(PresentationController): + """ + Class to control interactions with MacLO presentations on Mac OS X via Pyro4. It starts the Pyro4 nameserver, + starts the LibreOfficeServer, and then controls MacLO via Pyro4. + """ + log.info('MacLOController loaded') + + def __init__(self, plugin): + """ + Initialise the class + """ + log.debug('Initialising') + super(MacLOController, self).__init__(plugin, 'MacLO', MacLODocument) + self.supports = ['odp'] + self.also_supports = ['ppt', 'pps', 'pptx', 'ppsx', 'pptm'] + self.server_process = None + self.nameserver_process = None + self.client = None + self._start_nameserver() + self._start_server() + self._setup_client() + + def _start_nameserver(self): + """ + Start the Pyro4 nameserver + """ + self.nameserver_process = Process(run_nameserver) + self.nameserver_process.start() + + def _start_server(self): + """ + Start a LibreOfficeServer + """ + libreoffice_python = '/Applications/LibreOffice.app/Contents/Resources/python' + libreoffice_server = os.path.join(os.path.dirname(__file__), 'libreofficeserver.py') + self.server_process = Popen([libreoffice_python, libreoffice_server]) + + def _setup_client(self): + """ + Set up a Pyro4 client so that we can talk to the LibreOfficeServer + """ + self.client = Proxy('PYRONAME:openlp.libreofficeserver') + + def check_available(self): + """ + MacLO is able to run on this machine. + """ + log.debug('check_available') + return macuno_available + + def start_process(self): + """ + Loads a running version of LibreOffice in the background. It is not displayed to the user but is available to the + UNO interface when required. + """ + log.debug('start process Openoffice') + if is_win(): + self.manager = self.get_com_servicemanager() + self.manager._FlagAsMethod('Bridge_GetStruct') + self.manager._FlagAsMethod('Bridge_GetValueObject') + else: + # -headless + cmd = get_uno_command() + self.process = QtCore.QProcess() + self.process.startDetached(cmd) + + def kill(self): + """ + Called at system exit to clean up any running presentations. + """ + log.debug('Kill LibreOffice') + self.client.shutdown() + self.server_process.terminate() + self.nameserver_process.terminate() + + +class MacLODocument(PresentationDocument): + """ + Class which holds information and controls a single presentation. + """ + + def __init__(self, controller, presentation): + """ + Constructor, store information about the file and initialise. + """ + log.debug('Init Presentation LibreOffice') + super(MacLODocument, self).__init__(controller, presentation) + self.client = controller.client + self.document = None + self.presentation = None + self.control = None + + def load_presentation(self): + """ + Called when a presentation is added to the SlideController. It builds the environment, starts communcations with + the background LibreOffice task started earlier. If LibreOffice is not present is is started. Once the environment + is available the presentation is loaded and started. + """ + log.debug('Load Presentation LibreOffice') + if not self.client.has_desktop(): + return False + if not self.client.load_presentation(self.file_path, ScreenList().current['number'] + 1): + return False + self.create_thumbnails() + self.client.create_titles_and_notes() + return True + + def create_thumbnails(self): + """ + Create thumbnail images for presentation. + """ + log.debug('create thumbnails LibreOffice') + if self.check_thumbnails(): + return + self.client.create_thumbnails(self.get_temp_folder()) + + def create_titles_and_notes(self): + """ + Writes the list of titles (one per slide) to 'titles.txt' and the notes to 'slideNotes[x].txt' + in the thumbnails directory + """ + titles, notes = self.client.get_titles_and_notes() + self.save_titles_and_notes(titles, notes) + + def close_presentation(self): + """ + Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being + shutdown. + """ + log.debug('close Presentation LibreOffice') + self.client.close_presentation() + self.controller.remove_doc(self) + + def is_loaded(self): + """ + Returns true if a presentation is loaded. + """ + log.debug('is loaded LibreOffice') + return self.client.is_loaded() + + def is_active(self): + """ + Returns true if a presentation is active and running. + """ + log.debug('is active LibreOffice') + return self.client.is_active() + + def unblank_screen(self): + """ + Unblanks the screen. + """ + log.debug('unblank screen LibreOffice') + return self.client.unblank_screen() + + def blank_screen(self): + """ + Blanks the screen. + """ + log.debug('blank screen LibreOffice') + self.client.blank_screen() + + def is_blank(self): + """ + Returns true if screen is blank. + """ + log.debug('is blank LibreOffice') + return self.client.is_blank() + + def stop_presentation(self): + """ + Stop the presentation, remove from screen. + """ + log.debug('stop presentation LibreOffice') + self.client.stop_presentation() + + def start_presentation(self): + """ + Start the presentation from the beginning. + """ + log.debug('start presentation LibreOffice') + self.client.start_presentation() + # Make sure impress doesn't steal focus, unless we're on a single screen setup + if len(ScreenList().screen_list) > 1: + Registry().get('main_window').activateWindow() + + def get_slide_number(self): + """ + Return the current slide number on the screen, from 1. + """ + return self.client.get_slide_number() + + def get_slide_count(self): + """ + Return the total number of slides. + """ + return self.client.get_slide_count() + + def goto_slide(self, slide_no): + """ + Go to a specific slide (from 1). + + :param slide_no: The slide the text is required for, starting at 1 + """ + self.client.goto_slide(slide_no) + + def next_step(self): + """ + Triggers the next effect of slide on the running presentation. + """ + self.client.next_step() + + def previous_step(self): + """ + Triggers the previous slide on the running presentation. + """ + self.client.previous_step() + + def get_slide_text(self, slide_no): + """ + Returns the text on the slide. + + :param slide_no: The slide the text is required for, starting at 1 + """ + return self.client.get_slide_text(slide_no) + + def get_slide_notes(self, slide_no): + """ + Returns the text in the slide notes. + + :param slide_no: The slide the notes are required for, starting at 1 + """ + return self.client.get_slide_notes(slide_no) +