← Back to team overview

openlp-core team mailing list archive

[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/200161

Support for presenting PDF using mupdf or ghostscript.
-- 
https://code.launchpad.net/~tomasgroth/openlp/mupdf/+merge/200161
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/lib/serviceitem.py'
--- openlp/core/lib/serviceitem.py	2013-12-28 21:33:38 +0000
+++ openlp/core/lib/serviceitem.py	2013-12-29 22:24:56 +0000
@@ -420,7 +420,11 @@
                 self._raw_frames.append(slide)
         elif self.service_item_type == ServiceItemType.Image:
             settings_section = serviceitem['serviceitem']['header']['name']
-            background = QtGui.QColor(Settings().value(settings_section + '/background color'))
+            background = None
+            try:
+                background = QtGui.QColor(Settings().value(settings_section + '/background color'))
+            except Exception:
+                pass
             if path:
                 self.has_original_files = False
                 for text_image in serviceitem['serviceitem']['data']:

=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py	2013-12-28 21:33:38 +0000
+++ openlp/core/ui/slidecontroller.py	2013-12-29 22:24:56 +0000
@@ -436,7 +436,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):
@@ -730,8 +730,13 @@
         if old_item and self.is_live and old_item.is_capable(ItemCapabilities.ProvidesOwnDisplay):
             self._reset_blank()
         Registry().execute(
+<<<<<<< TREE
             '%s_start' % service_item.name.lower(), [service_item, self.is_live, self.hide_mode(), slide_no])
         self.slide_list = {}
+=======
+            '%s_start' % service_item.name.lower(), [self.service_item, self.is_live, self.hide_mode(), slideno])
+        self.slideList = {}
+>>>>>>> MERGE-SOURCE
         if self.is_live:
             self.song_menu.menu().clear()
             self.display.audio_player.reset()

=== modified file 'openlp/plugins/presentations/lib/mediaitem.py'
--- openlp/plugins/presentations/lib/mediaitem.py	2013-12-28 21:33:38 +0000
+++ openlp/plugins/presentations/lib/mediaitem.py	2013-12-29 22:24:56 +0000
@@ -238,7 +238,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 +249,89 @@
             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	2013-12-29 22:24:56 +0000
@@ -33,6 +33,7 @@
 
 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 +70,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 +318,26 @@
         hide_mode = message[2]
         file = item.get_frame_path()
         self.handler = item.processor
+        # When starting presentation from the servicemanager/slidecontroller we convert 
+        # PDF/XPS-serviceitems into image-serviceitems. When started from the mediamanager
+        # the conversion has already been done.
+        if file.endswith('.pdf') or file.endswith('.xps'):
+            log.debug('Converting from pdf/xps to images for serviceitem with file %s', file)
+            # Create a new image-serviceitem which will overwrite the old one
+            new_item = ServiceItem()
+            new_item.name = 'images'
+            if is_live:
+                self.media_item.generate_slide_data(new_item, item, False, False, ServiceItemContext.Live, file)
+            else:
+                self.media_item.generate_slide_data(new_item, item, False, False, ServiceItemContext.Preview, file)
+            # We need to overwrite the current serviceitem to make the slidecontroller
+            # present the images, so we do some copying
+            service_repr = {'serviceitem': new_item.get_service_repr(True) }
+            item.set_from_service(service_repr)
+            item._raw_frames = new_item._raw_frames
+            # 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 +346,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 == 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	2013-12-29 22:24:56 +0000
@@ -0,0 +1,301 @@
+# -*- 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
+        """
+        log.debug('Initialising')
+        self.process = None
+        PresentationController.__init__(self, plugin, 'Pdf', PdfDocument)
+        self.supports = ['pdf']
+        self.mudrawbin = ''
+        self.gsbin = ''
+        if self.check_installed() and self.mudrawbin:
+            self.also_supports = ['xps']
+
+    @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
+        """
+        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.
+        """
+        log.debug('check_available Pdf')
+        return self.check_installed()
+
+    def check_installed(self):
+        """
+        Check the viewer is installed.
+        """
+        log.debug('check_installed Pdf')
+        # 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
+                return True
+            elif program_type == 'mudraw':
+                self.mudrawbin = pdf_program
+                return True
+        # 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:
+            # First try to find mupdf
+            try:
+                self.mudrawbin = check_output(['which', 'mudraw']).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']).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 not self.mudrawbin and not self.gsbin:
+            return False
+        else:
+            return True
+        
+    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 automaticly while keeping aspect like mupdf, so we need
+        to get the ratio bewteen the screen size and the PDF to scale
+        """
+        # Use a postscript script to get size of the pdf. It is assumed that all pages have same size
+        postscript = '%!PS \n\
+() = \n\
+File dup (r) file runpdfbegin \n\
+1 pdfgetpage dup \n\
+/MediaBox pget { \n\
+aload pop exch 4 1 roll exch sub 3 1 roll sub \n\
+( Size: x: ) print =print (, y: ) print =print (\n) print \n\
+} if \n\
+flush \n\
+quit \n\
+'
+        # Put postscript into tempfile
+        tmpfile = NamedTemporaryFile(delete=False)
+        tmpfile.write(postscript)
+        tmpfile.close()
+        # Run the script on the pdf to get the size
+        runlog = []
+        try:
+            runlog = check_output([self.controller.gsbin, '-dNOPAUSE', '-dNODISPLAY', '-dBATCH', '-sFile=' + self.filepath, tmpfile.name])
+        except CalledProcessError as e:
+            log.debug(' '.join(e.cmd))
+            log.debug(e.output)
+        os.unlink(tmpfile.name)
+        # 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 = re.search('.*Size: x: (\d+\.?\d*), y: \d+.*', line).group(1)
+                height = re.search('.*Size: x: \d+\.?\d*, y: (\d+\.?\d*).*', line).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.
+        """
+        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.
+        """
+        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.
+        """
+        log.debug('is_active pdf')
+        return self.is_loaded() and not self.hidden
+
+    def get_slide_count(self):
+        """
+        Returns total number of slides
+        """
+        return self.num_pages
\ No newline at end of file

=== 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	2013-12-29 22:24:56 +0000
@@ -30,8 +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 +65,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 +74,35 @@
         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_path.editingFinished.connect(self.on_pdf_program_path_edit_finished)
+        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,12 @@
             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 +136,15 @@
             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.setReadOnly(not 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 +170,20 @@
         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 +199,44 @@
             checkbox = self.presenter_check_boxes[controller.name]
             checkbox.setEnabled(controller.is_available())
             self.set_controller_text(checkbox, controller)
+        
+    def on_pdf_program_path_edit_finished(self):
+        """
+        After selecting/typing in a program it is validated that it is a actually ghostscript or mudraw
+        """
+        program_type = None 
+        if self.pdf_program_path.text() != '':
+            program_type = PdfController.check_binary(self.pdf_program_path.text())
+            if not program_type:
+                critical_error_message_box(UiStrings().Error, 
+                        translate('PresentationPlugin.PresentationTab', 'The program is not ghostscript or mudraw which is required.'))
+                self.pdf_program_path.setFocus()
+
+    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.'))
+        if filename:
+            self.pdf_program_path.setText(filename)
+        self.pdf_program_path.setFocus()
+
+    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.
+        """
+        self.pdf_program_path.setReadOnly(not checked)
+        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.
+        """
+        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	2013-12-29 22:24:56 +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	2013-12-29 22:24:56 +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	2013-12-29 22:24:56 +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	2013-12-29 22:24:56 +0000
@@ -0,0 +1,108 @@
+# -*- 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	2013-12-29 22:24:56 +0000 differ

Follow ups