diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 956eedf6a..34a2df909 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -444,7 +444,7 @@ class SlideController(DisplayController): # "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 @@ class SlideController(DisplayController): 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 @@ class LiveController(RegistryMixin, OpenLPMixin, SlideController): """ process the bootstrap post setup request """ - self.post_set_up() \ No newline at end of file + self.post_set_up() diff --git a/openlp/plugins/presentations/lib/ghostscript_get_resolution.ps b/openlp/plugins/presentations/lib/ghostscript_get_resolution.ps new file mode 100644 index 000000000..67c14e1b2 --- /dev/null +++ b/openlp/plugins/presentations/lib/ghostscript_get_resolution.ps @@ -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 diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index 2e3b6c964..931a01bfd 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -116,7 +116,7 @@ class PresentationMediaItem(MediaManagerItem): 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 @@ class PresentationMediaItem(MediaManagerItem): """ 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 @@ class PresentationMediaItem(MediaManagerItem): 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 @@ class PresentationMediaItem(MediaManagerItem): 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 @@ class PresentationMediaItem(MediaManagerItem): 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 @@ class PresentationMediaItem(MediaManagerItem): 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) + 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 - 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): """ diff --git a/openlp/plugins/presentations/lib/messagelistener.py b/openlp/plugins/presentations/lib/messagelistener.py index fdb495c30..d734add72 100644 --- a/openlp/plugins/presentations/lib/messagelistener.py +++ b/openlp/plugins/presentations/lib/messagelistener.py @@ -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 @@ class Controller(object): 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 @@ class MessageListener(object): 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 @@ class MessageListener(object): 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): """ diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py new file mode 100644 index 000000000..36eddac2f --- /dev/null +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -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: , y: " + 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 diff --git a/openlp/plugins/presentations/lib/presentationtab.py b/openlp/plugins/presentations/lib/presentationtab.py index ba490f11b..7d5c81366 100644 --- a/openlp/plugins/presentations/lib/presentationtab.py +++ b/openlp/plugins/presentations/lib/presentationtab.py @@ -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 @@ class PresentationTab(SettingsTab): 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 @@ class PresentationTab(SettingsTab): 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 @@ class PresentationTab(SettingsTab): 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 @@ class PresentationTab(SettingsTab): 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 @@ class PresentationTab(SettingsTab): 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 @@ class PresentationTab(SettingsTab): 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 diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index ca583244c..c101f0437 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -45,9 +45,12 @@ log = logging.getLogger(__name__) __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': [] } diff --git a/resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py b/resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py index 16f96cff9..6ffb416fa 100644 --- a/resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py +++ b/resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py @@ -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'] diff --git a/tests/functional/openlp_plugins/presentations/test_mediaitem.py b/tests/functional/openlp_plugins/presentations/test_mediaitem.py index c6bc274bc..fb2c17e05 100644 --- a/tests/functional/openlp_plugins/presentations/test_mediaitem.py +++ b/tests/functional/openlp_plugins/presentations/test_mediaitem.py @@ -75,11 +75,16 @@ class TestMediaItem(TestCase): 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 @@ class TestMediaItem(TestCase): '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') diff --git a/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py b/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py new file mode 100644 index 000000000..cd2e4f744 --- /dev/null +++ b/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py @@ -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.') diff --git a/tests/resources/presentations/pdf_test1.pdf b/tests/resources/presentations/pdf_test1.pdf new file mode 100644 index 000000000..012dc9548 Binary files /dev/null and b/tests/resources/presentations/pdf_test1.pdf differ