Support for presenting PDF using mupdf or ghostscript.

bzr-revno: 2328
This commit is contained in:
Tomas Groth 2014-02-28 07:53:29 +00:00 committed by Tim Bentley
commit 873286df9a
11 changed files with 662 additions and 41 deletions

View File

@ -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()
self.post_set_up()

View File

@ -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

View File

@ -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):
"""

View File

@ -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):
"""

View File

@ -0,0 +1,316 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2014 Raoul Snyman #
# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
import os
import logging
from tempfile import NamedTemporaryFile
import re
from subprocess import check_output, CalledProcessError, STDOUT
from openlp.core.utils import AppLocation
from openlp.core.common import Settings
from openlp.core.lib import ScreenList
from .presentationcontroller import PresentationController, PresentationDocument
log = logging.getLogger(__name__)
class PdfController(PresentationController):
"""
Class to control PDF presentations
"""
log.info('PdfController loaded')
def __init__(self, plugin):
"""
Initialise the class
:param plugin: The plugin that creates the controller.
"""
log.debug('Initialising')
self.process = None
PresentationController.__init__(self, plugin, 'Pdf', PdfDocument)
self.supports = ['pdf']
self.also_supports = []
# Determine whether mudraw or ghostscript is used
self.check_installed()
@staticmethod
def check_binary(program_path):
"""
Function that checks whether a binary is either ghostscript or mudraw or neither.
Is also used from presentationtab.py
:param program_path:The full path to the binary to check.
:return: Type of the binary, 'gs' if ghostscript, 'mudraw' if mudraw, None if invalid.
"""
program_type = None
runlog = ''
log.debug('testing program_path: %s', program_path)
try:
runlog = check_output([program_path, '--help'], stderr=STDOUT)
except CalledProcessError as e:
runlog = e.output
except Exception:
runlog = ''
# Analyse the output to see it the program is mudraw, ghostscript or neither
for line in runlog.splitlines():
decoded_line = line.decode()
found_mudraw = re.search('usage: mudraw.*', decoded_line)
if found_mudraw:
program_type = 'mudraw'
break
found_gs = re.search('GPL Ghostscript.*', decoded_line)
if found_gs:
program_type = 'gs'
break
log.debug('in check_binary, found: %s', program_type)
return program_type
def check_available(self):
"""
PdfController is able to run on this machine.
:return: True if program to open PDF-files was found, otherwise False.
"""
log.debug('check_available Pdf')
return self.check_installed()
def check_installed(self):
"""
Check the viewer is installed.
:return: True if program to open PDF-files was found, otherwise False.
"""
log.debug('check_installed Pdf')
self.mudrawbin = ''
self.gsbin = ''
self.also_supports = []
# Use the user defined program if given
if (Settings().value('presentations/enable_pdf_program')):
pdf_program = Settings().value('presentations/pdf_program')
program_type = self.check_binary(pdf_program)
if program_type == 'gs':
self.gsbin = pdf_program
elif program_type == 'mudraw':
self.mudrawbin = pdf_program
else:
# Fallback to autodetection
application_path = AppLocation.get_directory(AppLocation.AppDir)
if os.name == 'nt':
# for windows we only accept mudraw.exe in the base folder
application_path = AppLocation.get_directory(AppLocation.AppDir)
if os.path.isfile(application_path + '/../mudraw.exe'):
self.mudrawbin = application_path + '/../mudraw.exe'
else:
DEVNULL = open(os.devnull, 'wb')
# First try to find mupdf
try:
self.mudrawbin = check_output(['which', 'mudraw'], stderr=DEVNULL).decode(encoding='UTF-8').rstrip('\n')
except CalledProcessError:
self.mudrawbin = ''
# if mupdf isn't installed, fallback to ghostscript
if not self.mudrawbin:
try:
self.gsbin = check_output(['which', 'gs'], stderr=DEVNULL).decode(encoding='UTF-8').rstrip('\n')
except CalledProcessError:
self.gsbin = ''
# Last option: check if mudraw is placed in OpenLP base folder
if not self.mudrawbin and not self.gsbin:
application_path = AppLocation.get_directory(AppLocation.AppDir)
if os.path.isfile(application_path + '/../mudraw'):
self.mudrawbin = application_path + '/../mudraw'
if self.mudrawbin:
self.also_supports = ['xps']
return True
elif self.gsbin:
return True
else:
return False
def kill(self):
"""
Called at system exit to clean up any running presentations
"""
log.debug('Kill pdfviewer')
while self.docs:
self.docs[0].close_presentation()
class PdfDocument(PresentationDocument):
"""
Class which holds information of a single presentation.
This class is not actually used to present the PDF, instead we convert to
image-serviceitem on the fly and present as such. Therefore some of the 'playback'
functions is not implemented.
"""
def __init__(self, controller, presentation):
"""
Constructor, store information about the file and initialise.
"""
log.debug('Init Presentation Pdf')
PresentationDocument.__init__(self, controller, presentation)
self.presentation = None
self.blanked = False
self.hidden = False
self.image_files = []
self.num_pages = -1
def gs_get_resolution(self, size):
"""
Only used when using ghostscript
Ghostscript can't scale automatically while keeping aspect like mupdf, so we need
to get the ratio between the screen size and the PDF to scale
:param size: Size struct containing the screen size.
:return: The resolution dpi to be used.
"""
# Use a postscript script to get size of the pdf. It is assumed that all pages have same size
gs_resolution_script = AppLocation.get_directory(AppLocation.PluginsDir) + '/presentations/lib/ghostscript_get_resolution.ps'
# Run the script on the pdf to get the size
runlog = []
try:
runlog = check_output([self.controller.gsbin, '-dNOPAUSE', '-dNODISPLAY', '-dBATCH',
'-sFile=' + self.filepath, gs_resolution_script])
except CalledProcessError as e:
log.debug(' '.join(e.cmd))
log.debug(e.output)
# Extract the pdf resolution from output, the format is " Size: x: <width>, y: <height>"
width = 0
height = 0
for line in runlog.splitlines():
try:
width = int(re.search('.*Size: x: (\d+\.?\d*), y: \d+.*', line.decode()).group(1))
height = int(re.search('.*Size: x: \d+\.?\d*, y: (\d+\.?\d*).*', line.decode()).group(1))
break
except AttributeError:
pass
# Calculate the ratio from pdf to screen
if width > 0 and height > 0:
width_ratio = size.right() / float(width)
height_ratio = size.bottom() / float(height)
# return the resolution that should be used. 72 is default.
if width_ratio > height_ratio:
return int(height_ratio * 72)
else:
return int(width_ratio * 72)
else:
return 72
def load_presentation(self):
"""
Called when a presentation is added to the SlideController. It generates images from the PDF.
:return: True is loading succeeded, otherwise False.
"""
log.debug('load_presentation pdf')
# Check if the images has already been created, and if yes load them
if os.path.isfile(os.path.join(self.get_temp_folder(), 'mainslide001.png')):
created_files = sorted(os.listdir(self.get_temp_folder()))
for fn in created_files:
if os.path.isfile(os.path.join(self.get_temp_folder(), fn)):
self.image_files.append(os.path.join(self.get_temp_folder(), fn))
self.num_pages = len(self.image_files)
return True
size = ScreenList().current['size']
# Generate images from PDF that will fit the frame.
runlog = ''
try:
if not os.path.isdir(self.get_temp_folder()):
os.makedirs(self.get_temp_folder())
if self.controller.mudrawbin:
runlog = check_output([self.controller.mudrawbin, '-w', str(size.right()), '-h', str(size.bottom()),
'-o', os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), self.filepath])
elif self.controller.gsbin:
resolution = self.gs_get_resolution(size)
runlog = check_output([self.controller.gsbin, '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m',
'-r' + str(resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
'-sOutputFile=' + os.path.join(self.get_temp_folder(), 'mainslide%03d.png'),
self.filepath])
created_files = sorted(os.listdir(self.get_temp_folder()))
for fn in created_files:
if os.path.isfile(os.path.join(self.get_temp_folder(), fn)):
self.image_files.append(os.path.join(self.get_temp_folder(), fn))
except Exception as e:
log.debug(e)
log.debug(runlog)
return False
self.num_pages = len(self.image_files)
# Create thumbnails
self.create_thumbnails()
return True
def create_thumbnails(self):
"""
Generates thumbnails
"""
log.debug('create_thumbnails pdf')
if self.check_thumbnails():
return
# use builtin function to create thumbnails from generated images
index = 1
for image in self.image_files:
self.convert_thumbnail(image, index)
index += 1
def close_presentation(self):
"""
Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being
shut down.
"""
log.debug('close_presentation pdf')
self.controller.remove_doc(self)
def is_loaded(self):
"""
Returns true if a presentation is loaded.
:return: True if loaded, False if not.
"""
log.debug('is_loaded pdf')
if self.num_pages < 0:
return False
return True
def is_active(self):
"""
Returns true if a presentation is currently active.
:return: True if active, False if not.
"""
log.debug('is_active pdf')
return self.is_loaded() and not self.hidden
def get_slide_count(self):
"""
Returns total number of slides
:return: The number of pages in the presentation..
"""
return self.num_pages

View File

@ -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

View File

@ -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': []
}

View File

@ -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']

View File

@ -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')

View File

@ -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.')

Binary file not shown.