Merge branch 'fixes-for-beta1' into 'master'

Various fixes for beta1

Closes #660

See merge request openlp/openlp!232
This commit is contained in:
Raoul Snyman 2020-09-26 23:40:29 +00:00
commit 793a801235
6 changed files with 27 additions and 324 deletions

View File

@ -54,12 +54,6 @@ after_test:
# This is where we create a package using PyInstaller
# Install PyInstaller
python -m pip install --no-warn-script-location pyinstaller
# Patch pyinstaller to fix a webengine bundle issue
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/pyinstaller/pyinstaller/d08a42c612a91cc320c54f9e812fe967b9c8594a/PyInstaller/hooks/hook-PyQt5.QtWebEngineWidgets.py" -OutFile "hook-PyQt5.QtWebEngineWidgets.py"
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/pyinstaller/pyinstaller/91481570517707fc70aa70dca9eb986c61eac35d/PyInstaller/hooks/hook-pkg_resources.py" -OutFile hook-pkg_resources.py
$sitePkgFolder = python -c "import sys; site_pkg_folder = next(p for p in sys.path if 'site-packages' in p); print(site_pkg_folder.replace('\\', '/'))"
cp hook-PyQt5.QtWebEngineWidgets.py "${sitePkgFolder}/PyInstaller/hooks/hook-PyQt5.QtWebEngineWidgets.py"
cp hook-pkg_resources.py "${sitePkgFolder}/PyInstaller/hooks/hook-pkg_resources.py"
# Some windows only stuff...
If ($isWindows) {
# Disabled portable installers - can't figure out how to make them silent

View File

@ -280,8 +280,6 @@ class Settings(QtCore.QSettings):
'planningcenter/secret': '',
'presentations/status': PluginStatus.Inactive,
'presentations/override app': QtCore.Qt.Unchecked,
'presentations/enable_pdf_program': QtCore.Qt.Unchecked,
'presentations/pdf_program': None,
'presentations/maclo': QtCore.Qt.Checked,
'presentations/Impress': QtCore.Qt.Checked,
'presentations/Powerpoint': QtCore.Qt.Checked,
@ -406,7 +404,8 @@ class Settings(QtCore.QSettings):
('projector/last directory import', 'projector/last directory import', [(str_to_path, None)]),
('projector/last directory export', 'projector/last directory export', [(str_to_path, None)]),
('bibles/last directory import', 'bibles/last directory import', [(str_to_path, None)]),
('presentations/pdf_program', 'presentations/pdf_program', [(str_to_path, None)]),
('presentations/enable_pdf_program', '', []),
('presentations/pdf_program', '', []),
('songs/last directory import', 'songs/last directory import', [(str_to_path, None)]),
('songs/last directory export', 'songs/last directory export', [(str_to_path, None)]),
('songusage/last directory export', 'songusage/last directory export', [(str_to_path, None)]),

View File

@ -340,7 +340,6 @@ class MessageListener(object):
item.footer = item_cpy.footer
item.from_service = item_cpy.from_service
item.iconic_representation = item_cpy.icon
item.image_border = item_cpy.image_border
item.main = item_cpy.main
item.theme = item_cpy.theme
# When presenting PDF/XPS/OXPS, we are using the image presentation code,

View File

@ -19,13 +19,8 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
import logging
import re
from shutil import which
from subprocess import CalledProcessError, check_output
from openlp.core.common import check_binary_exists, is_win
from openlp.core.common.registry import Registry
from openlp.core.common.applocation import AppLocation
from openlp.core.common import is_win
from openlp.core.display.screens import ScreenList
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
@ -64,38 +59,6 @@ class PdfController(PresentationController):
# Determine whether mudraw or ghostscript is used
self.check_installed()
@staticmethod
def process_check_binary(program_path):
"""
Function that checks whether a binary is either ghostscript or mudraw or neither.
Is also used from presentationtab.py
:param pathlib.Path program_path: The full path to the binary to check.
:return: Type of the binary, 'gs' if ghostscript, 'mudraw' if mudraw, None if invalid.
:rtype: str | None
"""
program_type = None
runlog = check_binary_exists(program_path)
# 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, re.IGNORECASE)
if found_mudraw:
program_type = 'mudraw'
break
found_mutool = re.search('usage: mutool.*', decoded_line, re.IGNORECASE)
if found_mutool:
# Test that mutool contains mudraw
if re.search(r'draw\s+--\s+convert document.*', runlog.decode(), re.IGNORECASE | re.MULTILINE):
program_type = 'mutool'
break
found_gs = re.search('GPL Ghostscript.*', decoded_line, re.IGNORECASE)
if found_gs:
program_type = 'gs'
break
log.debug('in check_binary, found: {text}'.format(text=program_type))
return program_type
def check_available(self):
"""
PdfController is able to run on this machine.
@ -112,53 +75,10 @@ class PdfController(PresentationController):
:return: True if program to open PDF-files was found, otherwise False.
"""
log.debug('check_installed Pdf')
self.mudrawbin = None
self.mutoolbin = None
self.gsbin = None
self.also_supports = []
# Use the user defined program if given
if Registry().get('settings').value('presentations/enable_pdf_program'):
program_path = Registry().get('settings').value('presentations/pdf_program')
program_type = self.process_check_binary(program_path)
if program_type == 'gs':
self.gsbin = program_path
elif program_type == 'mudraw':
self.mudrawbin = program_path
elif program_type == 'mutool':
self.mutoolbin = program_path
elif PYMUPDF_AVAILABLE:
if PYMUPDF_AVAILABLE:
self.also_supports = ['xps', 'oxps', 'epub', 'cbz', 'fb2']
return True
else:
# Fallback to autodetection
application_path = AppLocation.get_directory(AppLocation.AppDir)
if is_win():
# for windows we only accept mudraw.exe or mutool.exe in the base folder
if (application_path / 'mudraw.exe').is_file():
self.mudrawbin = application_path / 'mudraw.exe'
elif (application_path / 'mutool.exe').is_file():
self.mutoolbin = application_path / 'mutool.exe'
else:
# First try to find mudraw
self.mudrawbin = which('mudraw')
# if mudraw isn't installed, try mutool
if not self.mudrawbin:
self.mutoolbin = which('mutool')
# Check we got a working mutool
if not self.mutoolbin or self.process_check_binary(self.mutoolbin) != 'mutool':
self.gsbin = which('gs')
# Last option: check if mudraw or mutool is placed in OpenLP base folder
if not self.mudrawbin and not self.mutoolbin and not self.gsbin:
application_path = AppLocation.get_directory(AppLocation.AppDir)
if (application_path / 'mudraw').is_file():
self.mudrawbin = application_path / 'mudraw'
elif (application_path / 'mutool').is_file():
self.mutoolbin = application_path / 'mutool'
if self.mudrawbin or self.mutoolbin:
self.also_supports = ['xps', 'oxps', 'epub', 'cbz', 'fb2']
return True
elif self.gsbin:
return True
return False
def kill(self):
@ -198,49 +118,6 @@ class PdfDocument(PresentationDocument):
else:
self.startupinfo = None
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([str(self.controller.gsbin), '-dNOPAUSE', '-dNODISPLAY', '-dBATCH',
'-sFile={file_path}'.format(file_path=self.file_path), str(gs_resolution_script)],
startupinfo=self.startupinfo)
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.0
height = 0.0
for line in runlog.splitlines():
try:
width = float(re.search(r'.*Size: x: (\d+\.?\d*), y: \d+.*', line.decode()).group(1))
height = float(re.search(r'.*Size: x: \d+\.?\d*, y: (\d+\.?\d*).*', line.decode()).group(1))
break
except AttributeError:
continue
# Calculate the ratio from pdf to screen
if width > 0 and height > 0:
width_ratio = size.width() / width
height_ratio = size.height() / 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.
@ -260,30 +137,9 @@ class PdfDocument(PresentationDocument):
size = ScreenList().current.display_geometry
# Generate images from PDF that will fit the frame.
runlog = ''
try:
if not temp_dir_path.is_dir():
temp_dir_path.mkdir(parents=True)
# The %03d in the file name is handled by each binary
if self.controller.mudrawbin:
log.debug('loading presentation using mudraw')
runlog = check_output([str(self.controller.mudrawbin), '-w', str(size.width()),
'-h', str(size.height()),
'-o', str(temp_dir_path / 'mainslide%03d.png'), str(self.file_path)],
startupinfo=self.startupinfo)
elif self.controller.mutoolbin:
log.debug('loading presentation using mutool')
runlog = check_output([str(self.controller.mutoolbin), 'draw', '-w', str(size.width()),
'-h', str(size.height()), '-o', str(temp_dir_path / 'mainslide%03d.png'),
str(self.file_path)],
startupinfo=self.startupinfo)
elif self.controller.gsbin:
log.debug('loading presentation using gs')
resolution = self.gs_get_resolution(size)
runlog = check_output([str(self.controller.gsbin), '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m',
'-r{res}'.format(res=resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
'-sOutputFile={output}'.format(output=temp_dir_path / 'mainslide%03d.png'),
str(self.file_path)], startupinfo=self.startupinfo)
elif PYMUPDF_AVAILABLE:
try:
log.debug('loading presentation using PyMuPDF')
pdf = fitz.open(str(self.file_path))
for i, page in enumerate(pdf, start=1):

View File

@ -23,9 +23,6 @@ from PyQt5 import QtWidgets
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.lib.settingstab import SettingsTab
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.widgets.edits import PathEdit
from openlp.plugins.presentations.lib.pdfcontroller import PdfController
class PresentationTab(SettingsTab):
@ -79,23 +76,10 @@ class PresentationTab(SettingsTab):
self.ppt_window_check_box.setObjectName('ppt_window_check_box')
self.powerpoint_layout.addWidget(self.ppt_window_check_box)
self.left_layout.addWidget(self.powerpoint_group_box)
# Pdf options
self.pdf_group_box = QtWidgets.QGroupBox(self.left_column)
self.pdf_group_box.setObjectName('pdf_group_box')
self.pdf_layout = QtWidgets.QFormLayout(self.pdf_group_box)
self.pdf_layout.setObjectName('pdf_layout')
self.pdf_program_check_box = QtWidgets.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.program_path_edit = PathEdit(self.pdf_group_box)
self.pdf_layout.addRow(self.program_path_edit)
self.left_layout.addWidget(self.pdf_group_box)
# setup layout
self.left_layout.addStretch()
self.right_column.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
self.right_layout.addStretch()
# Signals and slots
self.program_path_edit.pathChanged.connect(self.on_program_path_edit_path_changed)
self.pdf_program_check_box.clicked.connect(self.program_path_edit.setEnabled)
def retranslate_ui(self):
"""
@ -107,7 +91,6 @@ 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.powerpoint_group_box.setTitle(translate('PresentationPlugin.PresentationTab', 'PowerPoint options'))
self.override_app_check_box.setText(
translate('PresentationPlugin.PresentationTab', 'Allow presentation application to be overridden'))
@ -118,10 +101,6 @@ class PresentationTab(SettingsTab):
translate('PresentationPlugin.PresentationTab',
'Let PowerPoint control the size and monitor of the presentations\n'
'(This may fix PowerPoint scaling issues in Windows 8 and 10)'))
self.pdf_program_check_box.setText(
translate('PresentationPlugin.PresentationTab', 'Use given full path for mudraw or ghostscript binary:'))
self.program_path_edit.dialog_caption = translate('PresentationPlugin.PresentationTab',
'Select mudraw or ghostscript binary')
def set_controller_text(self, checkbox, controller):
if checkbox.isEnabled():
@ -147,11 +126,6 @@ class PresentationTab(SettingsTab):
self.ppt_slide_click_check_box.setEnabled(powerpoint_available)
self.ppt_window_check_box.setChecked(self.settings.value('presentations/powerpoint control window'))
self.ppt_window_check_box.setEnabled(powerpoint_available)
# load pdf-program settings
enable_pdf_program = self.settings.value('presentations/enable_pdf_program')
self.pdf_program_check_box.setChecked(enable_pdf_program)
self.program_path_edit.setEnabled(enable_pdf_program)
self.program_path_edit.path = self.settings.value('presentations/pdf_program')
def save(self):
"""
@ -186,18 +160,6 @@ class PresentationTab(SettingsTab):
if self.settings.value(setting_key) != self.ppt_window_check_box.checkState():
self.settings.setValue(setting_key, self.ppt_window_check_box.checkState())
changed = True
# Save pdf-settings
pdf_program_path = self.program_path_edit.path
enable_pdf_program = self.pdf_program_check_box.checkState()
# If the given program is blank disable using the program
if pdf_program_path is None:
enable_pdf_program = 0
if pdf_program_path != self.settings.value('presentations/pdf_program'):
self.settings.setValue('presentations/pdf_program', pdf_program_path)
changed = True
if enable_pdf_program != self.settings.value('presentations/enable_pdf_program'):
self.settings.setValue('presentations/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')
@ -213,16 +175,3 @@ class PresentationTab(SettingsTab):
checkbox = self.presenter_check_boxes[controller.name]
checkbox.setEnabled(controller.is_available())
self.set_controller_text(checkbox, controller)
def on_program_path_edit_path_changed(self, new_path):
"""
Handle the `pathEditChanged` signal from program_path_edit
:param pathlib.Path new_path: File path to the new program
:rtype: None
"""
if new_path:
if not PdfController.process_check_binary(new_path):
critical_error_message_box(UiStrings().Error,
translate('PresentationPlugin.PresentationTab',
'The program is not ghostscript or mudraw which is required.'))

View File

@ -24,10 +24,10 @@ This module contains tests for the PdfController
import os
import pytest
from pathlib import Path
from shutil import rmtree, which
from shutil import rmtree
from tempfile import mkdtemp
from unittest import skipIf
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock
from PyQt5 import QtCore, QtGui
@ -96,7 +96,7 @@ def test_constructor(settings, mock_plugin):
assert 'Pdf' == controller.name, 'The name of the presentation controller should be correct'
def load_pdf(exe_path, pdf_env):
def load_pdf(pdf_env):
"""
Test loading a Pdf using the PdfController
"""
@ -104,11 +104,9 @@ def load_pdf(exe_path, pdf_env):
test_file_path = RESOURCE_PATH / 'presentations' / 'pdf_test1.pdf'
# WHEN: The Pdf is loaded
settings = pdf_env[0]
mock_plugin = pdf_env[1]
temp_folder_path = pdf_env[2]
thumbnail_folder_path = pdf_env[3]
settings.setValue('presentations/pdf_program', exe_path)
controller = PdfController(plugin=mock_plugin)
controller.temp_folder = temp_folder_path
controller.thumbnail_folder = thumbnail_folder_path
@ -121,7 +119,7 @@ def load_pdf(exe_path, pdf_env):
@skipIf(IS_QT_QPA_PLATFORM_OFFSCREEN, 'This test fails when QT_QPA_PLATFORM is set to "offscreen".')
def load_pdf_pictures(exe_path, pdf_env):
def load_pdf_pictures(pdf_env):
"""
Test loading a Pdf and check the generated pictures' size
"""
@ -129,11 +127,9 @@ def load_pdf_pictures(exe_path, pdf_env):
test_file_path = RESOURCE_PATH / 'presentations' / 'pdf_test1.pdf'
# WHEN: The Pdf is loaded
settings = pdf_env[0]
mock_plugin = pdf_env[1]
temp_folder_path = pdf_env[2]
thumbnail_folder_path = pdf_env[3]
settings.setValue('presentations/pdf_program', exe_path)
controller = PdfController(plugin=mock_plugin)
controller.temp_folder = temp_folder_path
controller.thumbnail_folder = thumbnail_folder_path
@ -144,10 +140,6 @@ def load_pdf_pictures(exe_path, pdf_env):
assert loaded is True, 'The loading of the PDF should succeed.'
image = QtGui.QImage(os.path.join(str(temp_folder_path), 'pdf_test1.pdf', 'mainslide001.png'))
# Based on the converter used the resolution will differ a bit
if controller.gsbin:
assert 1076 == image.height(), 'The height should be 1076'
assert 760 == image.width(), 'The width should be 760'
else:
width, height = get_screen_resolution()
# Calculate the width of the PDF based on the aspect ratio of the PDF
width = int(round(height * 0.70703125, 0))
@ -155,97 +147,11 @@ def load_pdf_pictures(exe_path, pdf_env):
assert image.width() == width, 'The width should be {width}'.format(width=width)
def test_load_pdf(pdf_env):
"""
Test loading a Pdf with each of the installed backends
"""
for exe_name in ['gs', 'mutool', 'mudraw']:
exe_path = which(exe_name)
if exe_path:
load_pdf(exe_path, pdf_env)
load_pdf_pictures(exe_path, pdf_env)
def test_loading_pdf_using_pymupdf(pdf_env):
try:
import fitz # noqa: F401
except ImportError:
pytest.skip('PyMuPDF is not installed')
load_pdf(None, pdf_env)
load_pdf_pictures(None, pdf_env)
@patch('openlp.plugins.presentations.lib.pdfcontroller.check_binary_exists')
def test_process_check_binary_mudraw(mocked_check_binary_exists):
"""
Test that the correct output from mudraw is detected
"""
# GIVEN: A mocked check_binary_exists that returns mudraw output
mudraw_output = (b'usage: mudraw [options] input [pages]\n\t-o -\toutput filename (%d for page number)n\t\tsupp'
b'orted formats: pgm, ppm, pam, png, pbmn\t-p -\tpasswordn\t-r -\tresolution in dpi (default: '
b'72)n\t-w -\twidth (in pixels) (maximum width if -r is specified)n\t-h -\theight (in pixels) '
b'(maximum height if -r is specified)')
mocked_check_binary_exists.return_value = mudraw_output
# WHEN: Calling process_check_binary
ret = PdfController.process_check_binary('test')
# THEN: mudraw should be detected
assert 'mudraw' == ret, 'mudraw should have been detected'
@patch('openlp.plugins.presentations.lib.pdfcontroller.check_binary_exists')
def test_process_check_binary_new_motool(mocked_check_binary_exists):
"""
Test that the correct output from the new mutool is detected
"""
# GIVEN: A mocked check_binary_exists that returns new mutool output
new_mutool_output = (b'usage: mutool <command> [options]\n\tdraw\t-- convert document\n\trun\t-- run javascript'
b'\n\tclean\t-- rewrite pdf file\n\textract\t-- extract font and image resources\n\tinfo\t'
b'-- show information about pdf resources\n\tpages\t-- show information about pdf pages\n'
b'\tposter\t-- split large page into many tiles\n\tshow\t-- show internal pdf objects\n\t'
b'create\t-- create pdf document\n\tmerge\t-- merge pages from multiple pdf sources into a'
b'new pdf\n')
mocked_check_binary_exists.return_value = new_mutool_output
# WHEN: Calling process_check_binary
ret = PdfController.process_check_binary('test')
# THEN: mutool should be detected
assert 'mutool' == ret, 'mutool should have been detected'
@patch('openlp.plugins.presentations.lib.pdfcontroller.check_binary_exists')
def test_process_check_binary_old_motool(mocked_check_binary_exists):
"""
Test that the output from the old mutool is not accepted
"""
# GIVEN: A mocked check_binary_exists that returns old mutool output
old_mutool_output = (b'usage: mutool <command> [options]\n\tclean\t-- rewrite pdf file\n\textract\t-- extract '
b'font and image resources\n\tinfo\t-- show information about pdf resources\n\tposter\t-- '
b'split large page into many tiles\n\tshow\t-- show internal pdf objects')
mocked_check_binary_exists.return_value = old_mutool_output
# WHEN: Calling process_check_binary
ret = PdfController.process_check_binary('test')
# THEN: mutool should be detected
assert ret is None, 'old mutool should not be accepted!'
@patch('openlp.plugins.presentations.lib.pdfcontroller.check_binary_exists')
def test_process_check_binary_gs(mocked_check_binary_exists):
"""
Test that the correct output from gs is detected
"""
# GIVEN: A mocked check_binary_exists that returns gs output
gs_output = (b'GPL Ghostscript 9.19 (2016-03-23)\nCopyright (C) 2016 Artifex Software, Inc. All rights reserv'
b'ed.\nUsage: gs [switches] [file1.ps file2.ps ...]')
mocked_check_binary_exists.return_value = gs_output
# WHEN: Calling process_check_binary
ret = PdfController.process_check_binary('test')
# THEN: mutool should be detected
assert 'gs' == ret, 'mutool should have been detected'
load_pdf(pdf_env)
load_pdf_pictures(pdf_env)