1
# -*- coding: utf-8 -*-
2
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
4
##########################################################################
5
# OpenLP - Open Source Lyrics Projection #
6
# ---------------------------------------------------------------------- #
7
# Copyright (c) 2008-2019 OpenLP Developers #
8
# ---------------------------------------------------------------------- #
9
# This program is free software: you can redistribute it and/or modify #
10
# it under the terms of the GNU General Public License as published by #
11
# the Free Software Foundation, either version 3 of the License, or #
12
# (at your option) any later version. #
14
# This program is distributed in the hope that it will be useful, #
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
17
# GNU General Public License for more details. #
19
# You should have received a copy of the GNU General Public License #
20
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
21
##########################################################################
23
This module runs a Pyro4 server using LibreOffice's version of Python
25
Please Note: This intentionally uses os.path over pathlib because we don't know which version of Python is shipped with
26
the version of LibreOffice on the user's computer.
28
from subprocess import Popen
35
if sys.platform.startswith('darwin'):
36
# Only make the log file on OS X when running as a server
37
logfile = os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp', 'libreofficeserver.log')
38
print('Setting up log file: {logfile}'.format(logfile=logfile))
39
logging.basicConfig(filename=logfile, level=logging.INFO)
42
# Add the current directory to sys.path so that we can load the serializers
43
sys.path.append(os.path.join(os.path.dirname(__file__)))
44
# Add the vendor directory to sys.path so that we can load Pyro4
45
sys.path.append(os.path.join(os.path.dirname(__file__), 'vendor'))
47
from serializers import register_classes
48
from Pyro4 import Daemon, expose
51
# Wrap these imports in a try so that we can run the tests on macOS
53
from com.sun.star.beans import PropertyValue
54
from com.sun.star.task import ErrorCodeIOException
56
# But they need to be defined for mocking
59
ErrorCodeIOException = Exception
62
log = logging.getLogger(__name__)
66
class TextType(object):
68
Type Enumeration for Types of Text to request
75
class LibreOfficeException(Exception):
77
A specific exception for LO
83
class LibreOfficeServer(object):
85
A Pyro4 server which controls LibreOffice
94
self._presentation = None
98
def _create_property(self, name, value):
100
Create an OOo style property object which are passed into some Uno methods.
102
log.debug('create property')
103
property_object = PropertyValue()
104
property_object.Name = name
105
property_object.Value = value
106
return property_object
108
def _get_text_from_page(self, slide_no, text_type=TextType.SlideText):
110
Return any text extracted from the presentation page.
112
:param slide_no: The slide the notes are required for, starting at 1
113
:param notes: A boolean. If set the method searches the notes of the slide.
114
:param text_type: A TextType. Enumeration of the types of supported text.
117
if TextType.Title <= text_type <= TextType.Notes:
118
pages = self._document.getDrawPages()
119
if 0 < slide_no <= pages.getCount():
120
page = pages.getByIndex(slide_no - 1)
121
if text_type == TextType.Notes:
122
page = page.getNotesPage()
123
for index in range(page.getCount()):
124
shape = page.getByIndex(index)
125
shape_type = shape.getShapeType()
126
if shape.supportsService('com.sun.star.drawing.Text'):
127
# if they requested title, make sure it is the title
128
if text_type != TextType.Title or shape_type == 'com.sun.star.presentation.TitleTextShape':
129
text += shape.getString() + '\n'
132
def start_process(self):
137
'/Applications/LibreOffice.app/Contents/MacOS/soffice',
142
'--nofirststartwizard',
143
'--accept=pipe,name=openlp_maclo;urp;StarOffice.ServiceManager'
145
self._process = Popen(uno_command)
150
Set up an UNO desktop instance
152
if self._desktop is not None:
155
context = uno.getComponentContext()
156
resolver = context.ServiceManager.createInstanceWithContext('com.sun.star.bridge.UnoUrlResolver', context)
158
while uno_instance is None and loop < 3:
160
uno_instance = resolver.resolve('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext')
162
log.exception('Unable to find running instance, retrying...')
165
self._manager = uno_instance.ServiceManager
166
log.debug('get UNO Desktop Openoffice - createInstanceWithContext - Desktop')
167
desktop = self._manager.createInstanceWithContext('com.sun.star.frame.Desktop', uno_instance)
169
raise Exception('Failed to get UNO desktop')
170
self._desktop = desktop
173
log.exception('Failed to get UNO desktop')
181
if hasattr(self, '_docs'):
183
self._docs[0].close_presentation()
184
docs = self.desktop.getComponents()
186
if docs.hasElements():
187
list_elements = docs.createEnumeration()
188
while list_elements.hasMoreElements():
189
doc = list_elements.nextElement()
190
if doc.getImplementationName() != 'com.sun.star.comp.framework.BackingComp':
193
log.debug('LibreOffice not terminated as docs are still open')
197
self.desktop.terminate()
198
log.debug('LibreOffice killed')
200
log.exception('Failed to terminate LibreOffice')
201
if getattr(self, '_process') and can_kill:
204
def load_presentation(self, file_path, screen_number):
208
self._file_path = file_path
209
url = uno.systemPathToFileUrl(file_path)
210
properties = (self._create_property('Hidden', True),)
211
self._document = None
213
while loop_count < 3:
215
self._document = self.desktop.loadComponentFromURL(url, '_blank', 0, properties)
217
log.exception('Failed to load presentation {url}'.format(url=url))
223
log.error('Looped too many times')
225
self._presentation = self._document.getPresentation()
226
self._presentation.Display = screen_number
230
def extract_thumbnails(self, temp_folder):
232
Create thumbnails for the presentation
235
thumb_dir_url = uno.systemPathToFileUrl(temp_folder)
236
properties = (self._create_property('FilterName', 'impress_png_Export'),)
237
pages = self._document.getDrawPages()
240
if not os.path.isdir(temp_folder):
241
os.makedirs(temp_folder)
242
for index in range(pages.getCount()):
243
page = pages.getByIndex(index)
244
self._document.getCurrentController().setCurrentPage(page)
245
url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1))
246
path = os.path.join(temp_folder, str(index + 1) + '.png')
248
self._document.storeToURL(url_path, properties)
249
thumbnails.append(path)
250
except ErrorCodeIOException as exception:
251
log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode))
253
log.exception('{path} - Unable to store openoffice preview'.format(path=path))
256
def get_titles_and_notes(self):
258
Extract the titles and the notes from the slides.
262
pages = self._document.getDrawPages()
263
for slide_no in range(1, pages.getCount() + 1):
264
titles.append(self._get_text_from_page(slide_no, TextType.Title).replace('\n', ' ') + '\n')
265
note = self._get_text_from_page(slide_no, TextType.Notes)
271
def close_presentation(self):
273
Close presentation and clean up objects.
275
log.debug('close Presentation LibreOffice')
277
if self._presentation:
279
self._presentation.end()
280
self._presentation = None
281
self._document.dispose()
283
log.exception("Closing presentation failed")
284
self._document = None
288
Returns true if a presentation is loaded.
290
log.debug('is loaded LibreOffice')
291
if self._presentation is None or self._document is None:
292
log.debug("is_loaded: no presentation or document")
295
if self._document.getPresentation() is None:
296
log.debug("getPresentation failed to find a presentation")
299
log.exception("getPresentation failed to find a presentation")
305
Returns true if a presentation is active and running.
307
log.debug('is active LibreOffice')
308
if not self.is_loaded():
310
return self._control.isRunning() if self._control else False
312
def unblank_screen(self):
316
log.debug('unblank screen LibreOffice')
317
return self._control.resume()
319
def blank_screen(self):
323
log.debug('blank screen LibreOffice')
324
self._control.blankScreen(0)
328
Returns true if screen is blank.
330
log.debug('is blank LibreOffice')
331
if self._control and self._control.isRunning():
332
return self._control.isPaused()
336
def stop_presentation(self):
338
Stop the presentation, remove from screen.
340
log.debug('stop presentation LibreOffice')
341
self._presentation.end()
344
def start_presentation(self):
346
Start the presentation from the beginning.
348
log.debug('start presentation LibreOffice')
349
if self._control is None or not self._control.isRunning():
350
window = self._document.getCurrentController().getFrame().getContainerWindow()
351
window.setVisible(True)
352
self._presentation.start()
353
self._control = self._presentation.getController()
354
# start() returns before the Component is ready. Try for 15 seconds.
356
while not self._control and sleep_count < 150:
359
self._control = self._presentation.getController()
360
window.setVisible(False)
362
self._control.activate()
365
def get_slide_number(self):
367
Return the current slide number on the screen, from 1.
369
return self._control.getCurrentSlideIndex() + 1
371
def get_slide_count(self):
373
Return the total number of slides.
375
return self._document.getDrawPages().getCount()
377
def goto_slide(self, slide_no):
379
Go to a specific slide (from 1).
381
:param slide_no: The slide the text is required for, starting at 1
383
self._control.gotoSlideIndex(slide_no - 1)
387
Triggers the next effect of slide on the running presentation.
389
is_paused = self._control.isPaused()
390
self._control.gotoNextEffect()
392
if not is_paused and self._control.isPaused():
393
self._control.gotoPreviousEffect()
395
def previous_step(self):
397
Triggers the previous slide on the running presentation.
399
self._control.gotoPreviousEffect()
401
def get_slide_text(self, slide_no):
403
Returns the text on the slide.
405
:param slide_no: The slide the text is required for, starting at 1
407
return self._get_text_from_page(slide_no)
409
def get_slide_notes(self, slide_no):
411
Returns the text in the slide notes.
413
:param slide_no: The slide the notes are required for, starting at 1
415
return self._get_text_from_page(slide_no, TextType.Notes)
420
The main function which runs the server
422
daemon = Daemon(host='localhost', port=4310)
423
daemon.register(LibreOfficeServer, 'openlp.libreofficeserver')
430
if __name__ == '__main__':