openlp-core team mailing list archive
-
openlp-core team
-
Mailing list archive
-
Message #22656
[Merge] lp:~tomasgroth/openlp/mupdf into lp:openlp
Tomas Groth has proposed merging lp:~tomasgroth/openlp/mupdf into lp:openlp.
Requested reviews:
Raoul Snyman (raoul-snyman)
Tim Bentley (trb143)
For more details, see:
https://code.launchpad.net/~tomasgroth/openlp/mupdf/+merge/208230
Support for presenting PDF using mupdf or ghostscript.
--
https://code.launchpad.net/~tomasgroth/openlp/mupdf/+merge/208230
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py 2014-01-11 17:52:01 +0000
+++ openlp/core/ui/slidecontroller.py 2014-02-25 21:06:14 +0000
@@ -444,7 +444,7 @@
# "V1" was the slide we wanted to go.
self.preview_widget.change_slide(self.slide_list[self.current_shortcut])
self.slide_selected()
- # Reset the shortcut.
+ # Reset the shortcut.
self.current_shortcut = ''
def set_live_hot_keys(self, parent=None):
@@ -774,7 +774,7 @@
self._reset_blank()
if service_item.is_command():
Registry().execute(
- '%s_start' % service_item.name.lower(), [service_item, self.is_live, self.hide_mode(), slide_no])
+ '%s_start' % service_item.name.lower(), [self.service_item, self.is_live, self.hide_mode(), slide_no])
self.slide_list = {}
if self.is_live:
self.song_menu.menu().clear()
@@ -1440,4 +1440,4 @@
"""
process the bootstrap post setup request
"""
- self.post_set_up()
\ No newline at end of file
+ self.post_set_up()
=== added file 'openlp/plugins/presentations/lib/ghostscript_get_resolution.ps'
--- openlp/plugins/presentations/lib/ghostscript_get_resolution.ps 1970-01-01 00:00:00 +0000
+++ openlp/plugins/presentations/lib/ghostscript_get_resolution.ps 2014-02-25 21:06:14 +0000
@@ -0,0 +1,10 @@
+%!PS
+() =
+File dup (r) file runpdfbegin
+1 pdfgetpage dup
+/MediaBox pget {
+aload pop exch 4 1 roll exch sub 3 1 roll sub
+( Size: x: ) print =print (, y: ) print =print (\n) print
+} if
+flush
+quit
=== modified file 'openlp/plugins/presentations/lib/mediaitem.py'
--- openlp/plugins/presentations/lib/mediaitem.py 2014-02-11 20:29:57 +0000
+++ openlp/plugins/presentations/lib/mediaitem.py 2014-02-25 21:06:14 +0000
@@ -116,7 +116,7 @@
self.display_type_label = QtGui.QLabel(self.presentation_widget)
self.display_type_label.setObjectName('display_type_label')
self.display_type_combo_box = create_horizontal_adjusting_combo_box(self.presentation_widget,
- 'display_type_combo_box')
+ 'display_type_combo_box')
self.display_type_label.setBuddy(self.display_type_combo_box)
self.display_layout.addRow(self.display_type_label, self.display_type_combo_box)
# Add the Presentation widget to the page layout.
@@ -138,6 +138,9 @@
"""
self.display_type_combo_box.clear()
for item in self.controllers:
+ # For PDF reload backend, since it can have changed
+ if self.controllers[item].name == 'Pdf':
+ self.controllers[item].check_available()
# load the drop down selection
if self.controllers[item].enabled():
self.display_type_combo_box.addItem(item)
@@ -177,9 +180,8 @@
if titles.count(filename) > 0:
if not initial_load:
critical_error_message_box(translate('PresentationPlugin.MediaItem', 'File Exists'),
- translate('PresentationPlugin.MediaItem',
- 'A presentation with that filename already exists.')
- )
+ translate('PresentationPlugin.MediaItem',
+ 'A presentation with that filename already exists.'))
continue
controller_name = self.findControllerByType(filename)
if controller_name:
@@ -203,7 +205,8 @@
icon = build_icon(':/general/general_delete.png')
else:
critical_error_message_box(UiStrings().UnsupportedFile,
- translate('PresentationPlugin.MediaItem', 'This type of presentation is not supported.'))
+ translate('PresentationPlugin.MediaItem',
+ 'This type of presentation is not supported.'))
continue
item_name = QtGui.QListWidgetItem(filename)
item_name.setData(QtCore.Qt.UserRole, file)
@@ -238,7 +241,7 @@
Settings().setValue(self.settings_section + '/presentations files', self.get_file_list())
def generate_slide_data(self, service_item, item=None, xml_version=False,
- remote=False, context=ServiceItemContext.Service):
+ remote=False, context=ServiceItemContext.Service, presentation_file=None):
"""
Load the relevant information for displaying the presentation in the slidecontroller. In the case of
powerpoints, an image for each slide.
@@ -249,45 +252,93 @@
items = self.list_view.selectedItems()
if len(items) > 1:
return False
- service_item.processor = self.display_type_combo_box.currentText()
- service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
+ filename = presentation_file
+ if filename is None:
+ filename = items[0].data(QtCore.Qt.UserRole)
+ file_type = os.path.splitext(filename)[1][1:]
if not self.display_type_combo_box.currentText():
return False
- for bitem in items:
- filename = bitem.data(QtCore.Qt.UserRole)
- (path, name) = os.path.split(filename)
- service_item.title = name
- if os.path.exists(filename):
- if service_item.processor == self.automatic:
- service_item.processor = self.findControllerByType(filename)
- if not service_item.processor:
+ if (file_type == 'pdf' or file_type == 'xps') and context != ServiceItemContext.Service:
+ service_item.add_capability(ItemCapabilities.CanMaintain)
+ service_item.add_capability(ItemCapabilities.CanPreview)
+ service_item.add_capability(ItemCapabilities.CanLoop)
+ service_item.add_capability(ItemCapabilities.CanAppend)
+ # force a nonexistent theme
+ service_item.theme = -1
+ for bitem in items:
+ filename = presentation_file
+ if filename is None:
+ filename = bitem.data(QtCore.Qt.UserRole)
+ (path, name) = os.path.split(filename)
+ service_item.title = name
+ if os.path.exists(filename):
+ processor = self.findControllerByType(filename)
+ if not processor:
return False
- controller = self.controllers[service_item.processor]
- doc = controller.add_document(filename)
- if doc.get_thumbnail_path(1, True) is None:
- doc.load_presentation()
- i = 1
- img = doc.get_thumbnail_path(i, True)
- if img:
- while img:
- service_item.add_from_command(path, name, img)
+ controller = self.controllers[processor]
+ service_item.processor = None
+ doc = controller.add_document(filename)
+ if doc.get_thumbnail_path(1, True) is None or not os.path.isfile(
+ os.path.join(doc.get_temp_folder(), 'mainslide001.png')):
+ doc.load_presentation()
+ i = 1
+ imagefile = 'mainslide%03d.png' % i
+ image = os.path.join(doc.get_temp_folder(), imagefile)
+ while os.path.isfile(image):
+ service_item.add_from_image(image, name)
i += 1
- img = doc.get_thumbnail_path(i, True)
+ imagefile = 'mainslide%03d.png' % i
+ image = os.path.join(doc.get_temp_folder(), imagefile)
doc.close_presentation()
return True
else:
# File is no longer present
if not remote:
critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),
- translate('PresentationPlugin.MediaItem',
- 'The presentation %s is incomplete, please reload.') % filename)
- return False
- else:
- # File is no longer present
- if not remote:
- critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),
- translate('PresentationPlugin.MediaItem', 'The presentation %s no longer exists.') % filename)
- return False
+ translate('PresentationPlugin.MediaItem',
+ 'The presentation %s no longer exists.') % filename)
+ return False
+ else:
+ service_item.processor = self.display_type_combo_box.currentText()
+ service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
+ for bitem in items:
+ filename = bitem.data(QtCore.Qt.UserRole)
+ (path, name) = os.path.split(filename)
+ service_item.title = name
+ if os.path.exists(filename):
+ if service_item.processor == self.automatic:
+ service_item.processor = self.findControllerByType(filename)
+ if not service_item.processor:
+ return False
+ controller = self.controllers[service_item.processor]
+ doc = controller.add_document(filename)
+ if doc.get_thumbnail_path(1, True) is None:
+ doc.load_presentation()
+ i = 1
+ img = doc.get_thumbnail_path(i, True)
+ if img:
+ while img:
+ service_item.add_from_command(path, name, img)
+ i += 1
+ img = doc.get_thumbnail_path(i, True)
+ doc.close_presentation()
+ return True
+ else:
+ # File is no longer present
+ if not remote:
+ critical_error_message_box(translate('PresentationPlugin.MediaItem',
+ 'Missing Presentation'),
+ translate('PresentationPlugin.MediaItem',
+ 'The presentation %s is incomplete, please reload.')
+ % filename)
+ return False
+ else:
+ # File is no longer present
+ if not remote:
+ critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),
+ translate('PresentationPlugin.MediaItem',
+ 'The presentation %s no longer exists.') % filename)
+ return False
def findControllerByType(self, filename):
"""
=== modified file 'openlp/plugins/presentations/lib/messagelistener.py'
--- openlp/plugins/presentations/lib/messagelistener.py 2013-12-28 21:33:38 +0000
+++ openlp/plugins/presentations/lib/messagelistener.py 2014-02-25 21:06:14 +0000
@@ -28,11 +28,13 @@
###############################################################################
import logging
+import copy
from PyQt4 import QtCore
from openlp.core.common import Registry
from openlp.core.ui import HideMode
+from openlp.core.lib import ServiceItemContext, ServiceItem
log = logging.getLogger(__name__)
@@ -69,6 +71,7 @@
return
self.doc.slidenumber = slide_no
self.hide_mode = hide_mode
+ log.debug('add_handler, slidenumber: %d' % slide_no)
if self.is_live:
if hide_mode == HideMode.Screen:
Registry().execute('live_display_hide', HideMode.Screen)
@@ -316,6 +319,28 @@
hide_mode = message[2]
file = item.get_frame_path()
self.handler = item.processor
+ # When starting presentation from the servicemanager we convert
+ # PDF/XPS-serviceitems into image-serviceitems. When started from the mediamanager
+ # the conversion has already been done at this point.
+ if file.endswith('.pdf') or file.endswith('.xps'):
+ log.debug('Converting from pdf/xps to images for serviceitem with file %s', file)
+ # Create a copy of the original item, and then clear the original item so it can be filled with images
+ item_cpy = copy.copy(item)
+ item.__init__(None)
+ if is_live:
+ self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Live, file)
+ else:
+ self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Preview, file)
+ # Some of the original serviceitem attributes is needed in the new serviceitem
+ item.footer = item_cpy.footer
+ item.from_service = item_cpy.from_service
+ item.iconic_representation = item_cpy.iconic_representation
+ item.image_border = item_cpy.image_border
+ item.main = item_cpy.main
+ item.theme_data = item_cpy.theme_data
+ # When presenting PDF or XPS, we are using the image presentation code,
+ # so handler & processor is set to None, and we skip adding the handler.
+ self.handler = None
if self.handler == self.media_item.automatic:
self.handler = self.media_item.findControllerByType(file)
if not self.handler:
@@ -324,7 +349,12 @@
controller = self.live_handler
else:
controller = self.preview_handler
- controller.add_handler(self.controllers[self.handler], file, hide_mode, message[3])
+ # When presenting PDF or XPS, we are using the image presentation code,
+ # so handler & processor is set to None, and we skip adding the handler.
+ if self.handler is None:
+ self.controller = controller
+ else:
+ controller.add_handler(self.controllers[self.handler], file, hide_mode, message[3])
def slide(self, message):
"""
=== added file 'openlp/plugins/presentations/lib/pdfcontroller.py'
--- openlp/plugins/presentations/lib/pdfcontroller.py 1970-01-01 00:00:00 +0000
+++ openlp/plugins/presentations/lib/pdfcontroller.py 2014-02-25 21:06:14 +0000
@@ -0,0 +1,316 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
+# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
+# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
+# --------------------------------------------------------------------------- #
+# 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 #
+###############################################################################
+
+import os
+import logging
+from tempfile import NamedTemporaryFile
+import re
+from subprocess import check_output, CalledProcessError, STDOUT
+
+from openlp.core.utils import AppLocation
+from openlp.core.common import Settings
+from openlp.core.lib import ScreenList
+from .presentationcontroller import PresentationController, PresentationDocument
+
+log = logging.getLogger(__name__)
+
+
+class PdfController(PresentationController):
+ """
+ Class to control PDF presentations
+ """
+ log.info('PdfController loaded')
+
+ def __init__(self, plugin):
+ """
+ Initialise the class
+
+ :param plugin: The plugin that creates the controller.
+ """
+ log.debug('Initialising')
+ self.process = None
+ PresentationController.__init__(self, plugin, 'Pdf', PdfDocument)
+ self.supports = ['pdf']
+ self.also_supports = []
+ # Determine whether mudraw or ghostscript is used
+ self.check_installed()
+
+ @staticmethod
+ def check_binary(program_path):
+ """
+ Function that checks whether a binary is either ghostscript or mudraw or neither.
+ Is also used from presentationtab.py
+
+ :param program_path:The full path to the binary to check.
+ :return: Type of the binary, 'gs' if ghostscript, 'mudraw' if mudraw, None if invalid.
+ """
+ program_type = None
+ runlog = ''
+ log.debug('testing program_path: %s', program_path)
+ try:
+ runlog = check_output([program_path, '--help'], stderr=STDOUT)
+ except CalledProcessError as e:
+ runlog = e.output
+ except Exception:
+ runlog = ''
+ # Analyse the output to see it the program is mudraw, ghostscript or neither
+ for line in runlog.splitlines():
+ decoded_line = line.decode()
+ found_mudraw = re.search('usage: mudraw.*', decoded_line)
+ if found_mudraw:
+ program_type = 'mudraw'
+ break
+ found_gs = re.search('GPL Ghostscript.*', decoded_line)
+ if found_gs:
+ program_type = 'gs'
+ break
+ log.debug('in check_binary, found: %s', program_type)
+ return program_type
+
+ def check_available(self):
+ """
+ PdfController is able to run on this machine.
+
+ :return: True if program to open PDF-files was found, otherwise False.
+ """
+ log.debug('check_available Pdf')
+ return self.check_installed()
+
+ def check_installed(self):
+ """
+ Check the viewer is installed.
+
+ :return: True if program to open PDF-files was found, otherwise False.
+ """
+ log.debug('check_installed Pdf')
+ self.mudrawbin = ''
+ self.gsbin = ''
+ self.also_supports = []
+ # Use the user defined program if given
+ if (Settings().value('presentations/enable_pdf_program')):
+ pdf_program = Settings().value('presentations/pdf_program')
+ program_type = self.check_binary(pdf_program)
+ if program_type == 'gs':
+ self.gsbin = pdf_program
+ elif program_type == 'mudraw':
+ self.mudrawbin = pdf_program
+ else:
+ # Fallback to autodetection
+ application_path = AppLocation.get_directory(AppLocation.AppDir)
+ if os.name == 'nt':
+ # for windows we only accept mudraw.exe in the base folder
+ application_path = AppLocation.get_directory(AppLocation.AppDir)
+ if os.path.isfile(application_path + '/../mudraw.exe'):
+ self.mudrawbin = application_path + '/../mudraw.exe'
+ else:
+ DEVNULL = open(os.devnull, 'wb')
+ # First try to find mupdf
+ try:
+ self.mudrawbin = check_output(['which', 'mudraw'], stderr=DEVNULL).decode(encoding='UTF-8').rstrip('\n')
+ except CalledProcessError:
+ self.mudrawbin = ''
+ # if mupdf isn't installed, fallback to ghostscript
+ if not self.mudrawbin:
+ try:
+ self.gsbin = check_output(['which', 'gs'], stderr=DEVNULL).decode(encoding='UTF-8').rstrip('\n')
+ except CalledProcessError:
+ self.gsbin = ''
+ # Last option: check if mudraw is placed in OpenLP base folder
+ if not self.mudrawbin and not self.gsbin:
+ application_path = AppLocation.get_directory(AppLocation.AppDir)
+ if os.path.isfile(application_path + '/../mudraw'):
+ self.mudrawbin = application_path + '/../mudraw'
+ if self.mudrawbin:
+ self.also_supports = ['xps']
+ return True
+ elif self.gsbin:
+ return True
+ else:
+ return False
+
+ def kill(self):
+ """
+ Called at system exit to clean up any running presentations
+ """
+ log.debug('Kill pdfviewer')
+ while self.docs:
+ self.docs[0].close_presentation()
+
+
+class PdfDocument(PresentationDocument):
+ """
+ Class which holds information of a single presentation.
+ This class is not actually used to present the PDF, instead we convert to
+ image-serviceitem on the fly and present as such. Therefore some of the 'playback'
+ functions is not implemented.
+ """
+ def __init__(self, controller, presentation):
+ """
+ Constructor, store information about the file and initialise.
+ """
+ log.debug('Init Presentation Pdf')
+ PresentationDocument.__init__(self, controller, presentation)
+ self.presentation = None
+ self.blanked = False
+ self.hidden = False
+ self.image_files = []
+ self.num_pages = -1
+
+ def gs_get_resolution(self, size):
+ """
+ Only used when using ghostscript
+ Ghostscript can't scale automatically while keeping aspect like mupdf, so we need
+ to get the ratio between the screen size and the PDF to scale
+
+ :param size: Size struct containing the screen size.
+ :return: The resolution dpi to be used.
+ """
+ # Use a postscript script to get size of the pdf. It is assumed that all pages have same size
+ gs_resolution_script = AppLocation.get_directory(AppLocation.PluginsDir) + '/presentations/lib/ghostscript_get_resolution.ps'
+ # Run the script on the pdf to get the size
+ runlog = []
+ try:
+ runlog = check_output([self.controller.gsbin, '-dNOPAUSE', '-dNODISPLAY', '-dBATCH',
+ '-sFile=' + self.filepath, gs_resolution_script])
+ except CalledProcessError as e:
+ log.debug(' '.join(e.cmd))
+ log.debug(e.output)
+ # Extract the pdf resolution from output, the format is " Size: x: <width>, y: <height>"
+ width = 0
+ height = 0
+ for line in runlog.splitlines():
+ try:
+ width = int(re.search('.*Size: x: (\d+\.?\d*), y: \d+.*', line.decode()).group(1))
+ height = int(re.search('.*Size: x: \d+\.?\d*, y: (\d+\.?\d*).*', line.decode()).group(1))
+ break
+ except AttributeError:
+ pass
+ # Calculate the ratio from pdf to screen
+ if width > 0 and height > 0:
+ width_ratio = size.right() / float(width)
+ height_ratio = size.bottom() / float(height)
+ # return the resolution that should be used. 72 is default.
+ if width_ratio > height_ratio:
+ return int(height_ratio * 72)
+ else:
+ return int(width_ratio * 72)
+ else:
+ return 72
+
+ def load_presentation(self):
+ """
+ Called when a presentation is added to the SlideController. It generates images from the PDF.
+
+ :return: True is loading succeeded, otherwise False.
+ """
+ log.debug('load_presentation pdf')
+ # Check if the images has already been created, and if yes load them
+ if os.path.isfile(os.path.join(self.get_temp_folder(), 'mainslide001.png')):
+ created_files = sorted(os.listdir(self.get_temp_folder()))
+ for fn in created_files:
+ if os.path.isfile(os.path.join(self.get_temp_folder(), fn)):
+ self.image_files.append(os.path.join(self.get_temp_folder(), fn))
+ self.num_pages = len(self.image_files)
+ return True
+ size = ScreenList().current['size']
+ # Generate images from PDF that will fit the frame.
+ runlog = ''
+ try:
+ if not os.path.isdir(self.get_temp_folder()):
+ os.makedirs(self.get_temp_folder())
+ if self.controller.mudrawbin:
+ runlog = check_output([self.controller.mudrawbin, '-w', str(size.right()), '-h', str(size.bottom()),
+ '-o', os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), self.filepath])
+ elif self.controller.gsbin:
+ resolution = self.gs_get_resolution(size)
+ runlog = check_output([self.controller.gsbin, '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m',
+ '-r' + str(resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
+ '-sOutputFile=' + os.path.join(self.get_temp_folder(), 'mainslide%03d.png'),
+ self.filepath])
+ created_files = sorted(os.listdir(self.get_temp_folder()))
+ for fn in created_files:
+ if os.path.isfile(os.path.join(self.get_temp_folder(), fn)):
+ self.image_files.append(os.path.join(self.get_temp_folder(), fn))
+ except Exception as e:
+ log.debug(e)
+ log.debug(runlog)
+ return False
+ self.num_pages = len(self.image_files)
+ # Create thumbnails
+ self.create_thumbnails()
+ return True
+
+ def create_thumbnails(self):
+ """
+ Generates thumbnails
+ """
+ log.debug('create_thumbnails pdf')
+ if self.check_thumbnails():
+ return
+ # use builtin function to create thumbnails from generated images
+ index = 1
+ for image in self.image_files:
+ self.convert_thumbnail(image, index)
+ index += 1
+
+ def close_presentation(self):
+ """
+ Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being
+ shut down.
+ """
+ log.debug('close_presentation pdf')
+ self.controller.remove_doc(self)
+
+ def is_loaded(self):
+ """
+ Returns true if a presentation is loaded.
+
+ :return: True if loaded, False if not.
+ """
+ log.debug('is_loaded pdf')
+ if self.num_pages < 0:
+ return False
+ return True
+
+ def is_active(self):
+ """
+ Returns true if a presentation is currently active.
+
+ :return: True if active, False if not.
+ """
+ log.debug('is_active pdf')
+ return self.is_loaded() and not self.hidden
+
+ def get_slide_count(self):
+ """
+ Returns total number of slides
+
+ :return: The number of pages in the presentation..
+ """
+ return self.num_pages
=== modified file 'openlp/plugins/presentations/lib/presentationtab.py'
--- openlp/plugins/presentations/lib/presentationtab.py 2013-12-24 08:56:50 +0000
+++ openlp/plugins/presentations/lib/presentationtab.py 2014-02-25 21:06:14 +0000
@@ -30,7 +30,9 @@
from PyQt4 import QtGui
from openlp.core.common import Settings, UiStrings, translate
-from openlp.core.lib import SettingsTab
+from openlp.core.lib import SettingsTab, build_icon
+from openlp.core.lib.ui import critical_error_message_box
+from .pdfcontroller import PdfController
class PresentationTab(SettingsTab):
@@ -64,6 +66,7 @@
self.presenter_check_boxes[controller.name] = checkbox
self.controllers_layout.addWidget(checkbox)
self.left_layout.addWidget(self.controllers_group_box)
+ # Advanced
self.advanced_group_box = QtGui.QGroupBox(self.left_column)
self.advanced_group_box.setObjectName('advanced_group_box')
self.advanced_layout = QtGui.QVBoxLayout(self.advanced_group_box)
@@ -72,8 +75,34 @@
self.override_app_check_box.setObjectName('override_app_check_box')
self.advanced_layout.addWidget(self.override_app_check_box)
self.left_layout.addWidget(self.advanced_group_box)
+ # Pdf options
+ self.pdf_group_box = QtGui.QGroupBox(self.left_column)
+ self.pdf_group_box.setObjectName('pdf_group_box')
+ self.pdf_layout = QtGui.QFormLayout(self.pdf_group_box)
+ self.pdf_layout.setObjectName('pdf_layout')
+ self.pdf_program_check_box = QtGui.QCheckBox(self.pdf_group_box)
+ self.pdf_program_check_box.setObjectName('pdf_program_check_box')
+ self.pdf_layout.addRow(self.pdf_program_check_box)
+ self.pdf_program_path_layout = QtGui.QHBoxLayout()
+ self.pdf_program_path_layout.setObjectName('pdf_program_path_layout')
+ self.pdf_program_path = QtGui.QLineEdit(self.pdf_group_box)
+ self.pdf_program_path.setObjectName('pdf_program_path')
+ self.pdf_program_path.setReadOnly(True)
+ self.pdf_program_path.setPalette(self.get_grey_text_palette(True))
+ self.pdf_program_path_layout.addWidget(self.pdf_program_path)
+ self.pdf_program_browse_button = QtGui.QToolButton(self.pdf_group_box)
+ self.pdf_program_browse_button.setObjectName('pdf_program_browse_button')
+ self.pdf_program_browse_button.setIcon(build_icon(':/general/general_open.png'))
+ self.pdf_program_browse_button.setEnabled(False)
+ self.pdf_program_path_layout.addWidget(self.pdf_program_browse_button)
+ self.pdf_layout.addRow(self.pdf_program_path_layout)
+ self.left_layout.addWidget(self.pdf_group_box)
self.left_layout.addStretch()
+ self.right_column.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred)
self.right_layout.addStretch()
+ # Signals and slots
+ self.pdf_program_browse_button.clicked.connect(self.on_pdf_program_browse_button_clicked)
+ self.pdf_program_check_box.clicked.connect(self.on_pdf_program_check_box_clicked)
def retranslateUi(self):
"""
@@ -85,8 +114,11 @@
checkbox = self.presenter_check_boxes[controller.name]
self.set_controller_text(checkbox, controller)
self.advanced_group_box.setTitle(UiStrings().Advanced)
+ self.pdf_group_box.setTitle(translate('PresentationPlugin.PresentationTab', 'PDF options'))
self.override_app_check_box.setText(
translate('PresentationPlugin.PresentationTab', 'Allow presentation application to be overridden'))
+ self.pdf_program_check_box.setText(
+ translate('PresentationPlugin.PresentationTab', 'Use given full path for mudraw or ghostscript binary:'))
def set_controller_text(self, checkbox, controller):
if checkbox.isEnabled():
@@ -103,6 +135,14 @@
checkbox = self.presenter_check_boxes[controller.name]
checkbox.setChecked(Settings().value(self.settings_section + '/' + controller.name))
self.override_app_check_box.setChecked(Settings().value(self.settings_section + '/override app'))
+ # load pdf-program settings
+ enable_pdf_program = Settings().value(self.settings_section + '/enable_pdf_program')
+ self.pdf_program_check_box.setChecked(enable_pdf_program)
+ self.pdf_program_path.setPalette(self.get_grey_text_palette(not enable_pdf_program))
+ self.pdf_program_browse_button.setEnabled(enable_pdf_program)
+ pdf_program = Settings().value(self.settings_section + '/pdf_program')
+ if pdf_program:
+ self.pdf_program_path.setText(pdf_program)
def save(self):
"""
@@ -128,6 +168,18 @@
if Settings().value(setting_key) != self.override_app_check_box.checkState():
Settings().setValue(setting_key, self.override_app_check_box.checkState())
changed = True
+ # Save pdf-settings
+ pdf_program = self.pdf_program_path.text()
+ enable_pdf_program = self.pdf_program_check_box.checkState()
+ # If the given program is blank disable using the program
+ if pdf_program == '':
+ enable_pdf_program = 0
+ if pdf_program != Settings().value(self.settings_section + '/pdf_program'):
+ Settings().setValue(self.settings_section + '/pdf_program', pdf_program)
+ changed = True
+ if enable_pdf_program != Settings().value(self.settings_section + '/enable_pdf_program'):
+ Settings().setValue(self.settings_section + '/enable_pdf_program', enable_pdf_program)
+ changed = True
if changed:
self.settings_form.register_post_process('mediaitem_suffix_reset')
self.settings_form.register_post_process('mediaitem_presentation_rebuild')
@@ -143,3 +195,43 @@
checkbox = self.presenter_check_boxes[controller.name]
checkbox.setEnabled(controller.is_available())
self.set_controller_text(checkbox, controller)
+
+ def on_pdf_program_browse_button_clicked(self):
+ """
+ Select the mudraw or ghostscript binary that should be used.
+ """
+ filename = QtGui.QFileDialog.getOpenFileName(self, translate('PresentationPlugin.PresentationTab',
+ 'Select mudraw or ghostscript binary.'),
+ self.pdf_program_path.text())
+ if filename:
+ program_type = PdfController.check_binary(filename)
+ if not program_type:
+ critical_error_message_box(UiStrings().Error,
+ translate('PresentationPlugin.PresentationTab',
+ 'The program is not ghostscript or mudraw which is required.'))
+ else:
+ self.pdf_program_path.setText(filename)
+
+ def on_pdf_program_check_box_clicked(self, checked):
+ """
+ When checkbox for manual entering pdf-program is clicked,
+ enable or disable the textbox for the programpath and the browse-button.
+
+ :param checked: If the box is checked or not.
+ """
+ self.pdf_program_path.setPalette(self.get_grey_text_palette(not checked))
+ self.pdf_program_browse_button.setEnabled(checked)
+
+ def get_grey_text_palette(self, greyed):
+ """
+ Returns a QPalette with greyed out text as used for placeholderText.
+
+ :param greyed: Determines whether the palette should be grayed.
+ :return: The created palette.
+ """
+ palette = QtGui.QPalette()
+ color = self.palette().color(QtGui.QPalette.Active, QtGui.QPalette.Text)
+ if greyed:
+ color.setAlpha(128)
+ palette.setColor(QtGui.QPalette.Active, QtGui.QPalette.Text, color)
+ return palette
=== modified file 'openlp/plugins/presentations/presentationplugin.py'
--- openlp/plugins/presentations/presentationplugin.py 2013-12-24 08:56:50 +0000
+++ openlp/plugins/presentations/presentationplugin.py 2014-02-25 21:06:14 +0000
@@ -45,9 +45,12 @@
__default_settings__ = {
'presentations/override app': QtCore.Qt.Unchecked,
+ 'presentations/enable_pdf_program': QtCore.Qt.Unchecked,
+ 'presentations/pdf_program': '',
'presentations/Impress': QtCore.Qt.Checked,
'presentations/Powerpoint': QtCore.Qt.Checked,
'presentations/Powerpoint Viewer': QtCore.Qt.Checked,
+ 'presentations/Pdf': QtCore.Qt.Checked,
'presentations/presentations files': []
}
=== modified file 'resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py'
--- resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py 2013-12-24 08:56:50 +0000
+++ resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py 2014-02-25 21:06:14 +0000
@@ -29,4 +29,5 @@
hiddenimports = ['openlp.plugins.presentations.lib.impresscontroller',
'openlp.plugins.presentations.lib.powerpointcontroller',
- 'openlp.plugins.presentations.lib.pptviewcontroller']
+ 'openlp.plugins.presentations.lib.pptviewcontroller',
+ 'openlp.plugins.presentations.lib.pdfcontroller']
=== modified file 'tests/functional/openlp_plugins/presentations/test_mediaitem.py'
--- tests/functional/openlp_plugins/presentations/test_mediaitem.py 2013-12-28 21:33:38 +0000
+++ tests/functional/openlp_plugins/presentations/test_mediaitem.py 2014-02-25 21:06:14 +0000
@@ -75,11 +75,16 @@
presentation_controller.also_supports = []
presentation_viewer_controller = MagicMock()
presentation_viewer_controller.enabled.return_value = False
+ pdf_controller = MagicMock()
+ pdf_controller.enabled.return_value = True
+ pdf_controller.supports = ['pdf']
+ pdf_controller.also_supports = ['xps']
# Mock the controllers.
self.media_item.controllers = {
'Impress': impress_controller,
'Powerpoint': presentation_controller,
- 'Powerpoint Viewer': presentation_viewer_controller
+ 'Powerpoint Viewer': presentation_viewer_controller,
+ 'Pdf': pdf_controller
}
# WHEN: Build the file mask.
@@ -92,3 +97,7 @@
'The file mask should contain the odp extension')
self.assertIn('*.ppt', self.media_item.on_new_file_masks,
'The file mask should contain the ppt extension')
+ self.assertIn('*.pdf', self.media_item.on_new_file_masks,
+ 'The file mask should contain the pdf extension')
+ self.assertIn('*.xps', self.media_item.on_new_file_masks,
+ 'The file mask should contain the xps extension')
=== added file 'tests/functional/openlp_plugins/presentations/test_pdfcontroller.py'
--- tests/functional/openlp_plugins/presentations/test_pdfcontroller.py 1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/presentations/test_pdfcontroller.py 2014-02-25 21:06:14 +0000
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
+# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
+# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
+# --------------------------------------------------------------------------- #
+# 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 #
+###############################################################################
+"""
+This module contains tests for the PdfController
+"""
+import os
+import shutil
+from unittest import TestCase, SkipTest
+from tempfile import mkstemp, mkdtemp
+
+from PyQt4 import QtGui
+
+from openlp.plugins.presentations.lib.pdfcontroller import PdfController, PdfDocument
+from tests.functional import MagicMock
+from openlp.core.common import Settings
+from openlp.core.lib import ScreenList
+from tests.utils.constants import TEST_RESOURCES_PATH
+
+__default_settings__ = {
+ 'presentations/enable_pdf_program': False
+}
+
+
+class TestPdfController(TestCase):
+ """
+ Test the PdfController.
+ """
+ def setUp(self):
+ """
+ Set up the components need for all tests.
+ """
+ self.fd, self.ini_file = mkstemp('.ini')
+ Settings().set_filename(self.ini_file)
+ self.application = QtGui.QApplication.instance()
+ ScreenList.create(self.application.desktop())
+ Settings().extend_default_settings(__default_settings__)
+ self.temp_folder = mkdtemp()
+ self.thumbnail_folder = mkdtemp()
+
+ def tearDown(self):
+ """
+ Delete all the C++ objects at the end so that we don't have a segfault
+ """
+ del self.application
+ try:
+ os.unlink(self.ini_file)
+ shutil.rmtree(self.thumbnail_folder)
+ shutil.rmtree(self.temp_folder)
+ except OSError:
+ pass
+
+ def constructor_test(self):
+ """
+ Test the Constructor
+ """
+ # GIVEN: No presentation controller
+ controller = None
+
+ # WHEN: The presentation controller object is created
+ controller = PdfController(plugin=MagicMock())
+
+ # THEN: The name of the presentation controller should be correct
+ self.assertEqual('Pdf', controller.name, 'The name of the presentation controller should be correct')
+
+ def load_pdf_test(self):
+ """
+ Test loading of a Pdf
+ """
+ # GIVEN: A Pdf-file
+ test_file = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf')
+
+ # WHEN: The Pdf is loaded
+ controller = PdfController(plugin=MagicMock())
+ if not controller.check_available():
+ raise SkipTest('Could not detect mudraw or ghostscript, so skipping PDF test')
+ controller.temp_folder = self.temp_folder
+ controller.thumbnail_folder = self.thumbnail_folder
+ document = PdfDocument(controller, test_file)
+ loaded = document.load_presentation()
+
+ # THEN: The load should succeed and we should be able to get a pagecount
+ self.assertTrue(loaded, 'The loading of the PDF should succeed.')
+ self.assertEqual(3, document.get_slide_count(), 'The pagecount of the PDF should be 3.')
=== added directory 'tests/resources/presentations'
=== added file 'tests/resources/presentations/pdf_test1.pdf'
Binary files tests/resources/presentations/pdf_test1.pdf 1970-01-01 00:00:00 +0000 and tests/resources/presentations/pdf_test1.pdf 2014-02-25 21:06:14 +0000 differ
Follow ups