← 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/201684

Support for presenting PDF using mupdf or ghostscript.
-- 
https://code.launchpad.net/~tomasgroth/openlp/mupdf/+merge/201684
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py	2014-01-04 11:50:27 +0000
+++ openlp/core/ui/slidecontroller.py	2014-01-14 21:58:55 +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):
@@ -763,7 +763,7 @@
         if old_item and self.is_live and old_item.is_capable(ItemCapabilities.ProvidesOwnDisplay):
             self._reset_blank()
         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()

=== 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-01-14 21:58:55 +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	2013-12-28 21:33:38 +0000
+++ openlp/plugins/presentations/lib/mediaitem.py	2014-01-14 21:58:55 +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-01-14 21:58:55 +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-01-14 21:58:55 +0000
@@ -0,0 +1,315 @@
+# -*- 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:
+                # 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 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-01-14 21:58:55 +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-01-14 21:58:55 +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-01-14 21:58:55 +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-01-14 21:58:55 +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-01-14 21:58:55 +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-01-14 21:58:55 +0000 differ

Follow ups