Add PyMuPDF as additional PDF controller and missing mupdf file formats

bzr-revno: 2864
This commit is contained in:
Bastian Germann 2019-05-02 14:47:05 -07:00 committed by Raoul Snyman
commit db9cc8c310
6 changed files with 55 additions and 22 deletions

View File

@ -333,7 +333,8 @@ class MessageListener(object):
# the conversion has already been done at this point. # the conversion has already been done at this point.
file_type = file_path.suffix.lower()[1:] file_type = file_path.suffix.lower()[1:]
if file_type in PDF_CONTROLLER_FILETYPES: if file_type in PDF_CONTROLLER_FILETYPES:
log.debug('Converting from pdf/xps/oxps to images for serviceitem with file {name}'.format(name=file_path)) log.debug('Converting from pdf/xps/oxps/epub/cbz/fb2 to images for serviceitem with file {name}'
.format(name=file_path))
# Create a copy of the original item, and then clear the original item so it can be filled with images # 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_cpy = copy.copy(item)
item.__init__(None) item.__init__(None)

View File

@ -34,9 +34,15 @@ from openlp.plugins.presentations.lib.presentationcontroller import Presentation
if is_win(): if is_win():
from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW
try:
import fitz
PYMUPDF_AVAILABLE = True
except ImportError:
PYMUPDF_AVAILABLE = False
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
PDF_CONTROLLER_FILETYPES = ['pdf', 'xps', 'oxps'] PDF_CONTROLLER_FILETYPES = ['pdf', 'xps', 'oxps', 'epub', 'cbz', 'fb2']
class PdfController(PresentationController): class PdfController(PresentationController):
@ -121,6 +127,9 @@ class PdfController(PresentationController):
self.mudrawbin = program_path self.mudrawbin = program_path
elif program_type == 'mutool': elif program_type == 'mutool':
self.mutoolbin = program_path self.mutoolbin = program_path
elif PYMUPDF_AVAILABLE:
self.also_supports = ['xps', 'oxps', 'epub', 'cbz', 'fb2']
return True
else: else:
# Fallback to autodetection # Fallback to autodetection
application_path = AppLocation.get_directory(AppLocation.AppDir) application_path = AppLocation.get_directory(AppLocation.AppDir)
@ -147,11 +156,10 @@ class PdfController(PresentationController):
elif (application_path / 'mutool').is_file(): elif (application_path / 'mutool').is_file():
self.mutoolbin = application_path / 'mutool' self.mutoolbin = application_path / 'mutool'
if self.mudrawbin or self.mutoolbin: if self.mudrawbin or self.mutoolbin:
self.also_supports = ['xps', 'oxps'] self.also_supports = ['xps', 'oxps', 'epub', 'cbz', 'fb2']
return True return True
elif self.gsbin: elif self.gsbin:
return True return True
else:
return False return False
def kill(self): def kill(self):
@ -276,6 +284,16 @@ class PdfDocument(PresentationDocument):
'-r{res}'.format(res=resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', '-r{res}'.format(res=resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
'-sOutputFile={output}'.format(output=temp_dir_path / 'mainslide%03d.png'), '-sOutputFile={output}'.format(output=temp_dir_path / 'mainslide%03d.png'),
str(self.file_path)], startupinfo=self.startupinfo) str(self.file_path)], startupinfo=self.startupinfo)
elif PYMUPDF_AVAILABLE:
log.debug('loading presentation using PyMuPDF')
pdf = fitz.open(str(self.file_path))
for i, page in enumerate(pdf, start=1):
src_size = page.bound().round()
# keep aspect ratio
scale = min(size.width() / src_size.width, size.height() / src_size.height)
m = fitz.Matrix(scale, scale)
page.getPixmap(m, alpha=False).writeImage(str(temp_dir_path / 'mainslide{:03d}.png'.format(i)))
pdf.close()
created_files = sorted(temp_dir_path.glob('*')) created_files = sorted(temp_dir_path.glob('*'))
for image_path in created_files: for image_path in created_files:
if image_path.is_file(): if image_path.is_file():

View File

@ -16,11 +16,7 @@ environment:
install: install:
# Install dependencies from pypi # Install dependencies from pypi
- "%PYTHON%\\python.exe -m pip install sqlalchemy alembic appdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock pyodbc psycopg2 pypiwin32 websockets asyncio waitress six webob requests QtAwesome PyQt5 PyQtWebEngine pymediainfo" - "%PYTHON%\\python.exe -m pip install sqlalchemy alembic appdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock pyodbc psycopg2 pypiwin32 websockets asyncio waitress six webob requests QtAwesome PyQt5 PyQtWebEngine pymediainfo PyMuPDF"
# Download and unpack mupdf
- appveyor DownloadFile https://mupdf.com/downloads/archive/mupdf-1.14.0-windows.zip
- 7z x mupdf-1.14.0-windows.zip
- cp mupdf-1.14.0-windows/mutool.exe openlp-branch/mutool.exe
build: off build: off

View File

@ -187,6 +187,7 @@ using a computer and a data projector.""",
'websockets' 'websockets'
], ],
extras_require={ extras_require={
'agpl-pdf': ['PyMuPDF'],
'darkstyle': ['QDarkStyle'], 'darkstyle': ['QDarkStyle'],
'mysql': ['mysql-connector-python'], 'mysql': ['mysql-connector-python'],
'odbc': ['pyodbc'], 'odbc': ['pyodbc'],
@ -200,6 +201,7 @@ using a computer and a data projector.""",
tests_require=[ tests_require=[
'nose2', 'nose2',
'pylint', 'pylint',
'PyMuPDF',
'pyodbc', 'pyodbc',
'pysword', 'pysword',
'python-xlib; platform_system=="Linux"' 'python-xlib; platform_system=="Linux"'

View File

@ -65,7 +65,7 @@ class TestMediaItem(TestCase, TestMixin):
pdf_controller = MagicMock() pdf_controller = MagicMock()
pdf_controller.enabled.return_value = True pdf_controller.enabled.return_value = True
pdf_controller.supports = ['pdf'] pdf_controller.supports = ['pdf']
pdf_controller.also_supports = ['xps', 'oxps'] pdf_controller.also_supports = ['xps', 'oxps', 'epub', 'cbz', 'fb2']
# Mock the controllers. # Mock the controllers.
self.media_item.controllers = { self.media_item.controllers = {
'Impress': impress_controller, 'Impress': impress_controller,
@ -85,6 +85,9 @@ class TestMediaItem(TestCase, TestMixin):
assert '*.pdf' in self.media_item.on_new_file_masks, 'The file mask should contain the pdf extension' assert '*.pdf' in self.media_item.on_new_file_masks, 'The file mask should contain the pdf extension'
assert '*.xps' in self.media_item.on_new_file_masks, 'The file mask should contain the xps extension' assert '*.xps' in self.media_item.on_new_file_masks, 'The file mask should contain the xps extension'
assert '*.oxps' in self.media_item.on_new_file_masks, 'The file mask should contain the oxps extension' assert '*.oxps' in self.media_item.on_new_file_masks, 'The file mask should contain the oxps extension'
assert '*.epub' in self.media_item.on_new_file_masks, 'The file mask should contain the epub extension'
assert '*.cbz' in self.media_item.on_new_file_masks, 'The file mask should contain the cbz extension'
assert '*.fb2' in self.media_item.on_new_file_masks, 'The file mask should contain the fb2 extension'
def test_clean_up_thumbnails(self): def test_clean_up_thumbnails(self):
""" """

View File

@ -23,8 +23,9 @@
This module contains tests for the PdfController This module contains tests for the PdfController
""" """
import os import os
from shutil import which
from tempfile import mkdtemp from tempfile import mkdtemp
from unittest import SkipTest, TestCase from unittest import TestCase
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from PyQt5 import QtCore, QtGui from PyQt5 import QtCore, QtGui
@ -39,7 +40,8 @@ from tests.utils.constants import RESOURCE_PATH
__default_settings__ = { __default_settings__ = {
'presentations/enable_pdf_program': False, 'presentations/enable_pdf_program': True,
'presentations/pdf_program': None,
'presentations/thumbnail_scheme': '' 'presentations/thumbnail_scheme': ''
} }
@ -113,17 +115,16 @@ class TestPdfController(TestCase, TestMixin):
# THEN: The name of the presentation controller should be correct # THEN: The name of the presentation controller should be correct
assert 'Pdf' == controller.name, 'The name of the presentation controller should be correct' assert 'Pdf' == controller.name, 'The name of the presentation controller should be correct'
def test_load_pdf(self): def load_pdf(self, exe_path):
""" """
Test loading of a Pdf using the PdfController Test loading a Pdf using the PdfController
""" """
# GIVEN: A Pdf-file # GIVEN: A Pdf-file
test_file_path = RESOURCE_PATH / 'presentations' / 'pdf_test1.pdf' test_file_path = RESOURCE_PATH / 'presentations' / 'pdf_test1.pdf'
# WHEN: The Pdf is loaded # WHEN: The Pdf is loaded
Settings().setValue('presentations/pdf_program', exe_path)
controller = PdfController(plugin=self.mock_plugin) controller = PdfController(plugin=self.mock_plugin)
if not controller.check_available():
raise SkipTest('Could not detect mudraw or ghostscript, so skipping PDF test')
controller.temp_folder = self.temp_folder_path controller.temp_folder = self.temp_folder_path
controller.thumbnail_folder = self.thumbnail_folder_path controller.thumbnail_folder = self.thumbnail_folder_path
document = PdfDocument(controller, test_file_path) document = PdfDocument(controller, test_file_path)
@ -133,23 +134,22 @@ class TestPdfController(TestCase, TestMixin):
assert loaded is True, 'The loading of the PDF should succeed.' assert loaded is True, 'The loading of the PDF should succeed.'
assert 3 == document.get_slide_count(), 'The pagecount of the PDF should be 3.' assert 3 == document.get_slide_count(), 'The pagecount of the PDF should be 3.'
def test_load_pdf_pictures(self): def load_pdf_pictures(self, exe_path):
""" """
Test loading of a Pdf and check size of generate pictures Test loading a Pdf and check the generated pictures' size
""" """
# GIVEN: A Pdf-file # GIVEN: A Pdf-file
test_file_path = RESOURCE_PATH / 'presentations' / 'pdf_test1.pdf' test_file_path = RESOURCE_PATH / 'presentations' / 'pdf_test1.pdf'
# WHEN: The Pdf is loaded # WHEN: The Pdf is loaded
Settings().setValue('presentations/pdf_program', exe_path)
controller = PdfController(plugin=self.mock_plugin) controller = PdfController(plugin=self.mock_plugin)
if not controller.check_available():
raise SkipTest('Could not detect mudraw or ghostscript, so skipping PDF test')
controller.temp_folder = self.temp_folder_path controller.temp_folder = self.temp_folder_path
controller.thumbnail_folder = self.thumbnail_folder_path controller.thumbnail_folder = self.thumbnail_folder_path
document = PdfDocument(controller, test_file_path) document = PdfDocument(controller, test_file_path)
loaded = document.load_presentation() loaded = document.load_presentation()
# THEN: The load should succeed and pictures should be created and have been scales to fit the screen # THEN: The load should succeed and pictures should be created and have been scaled to fit the screen
assert loaded is True, 'The loading of the PDF should succeed.' assert loaded is True, 'The loading of the PDF should succeed.'
image = QtGui.QImage(os.path.join(str(self.temp_folder_path), 'pdf_test1.pdf', 'mainslide001.png')) image = QtGui.QImage(os.path.join(str(self.temp_folder_path), 'pdf_test1.pdf', 'mainslide001.png'))
# Based on the converter used the resolution will differ a bit # Based on the converter used the resolution will differ a bit
@ -163,6 +163,19 @@ class TestPdfController(TestCase, TestMixin):
assert image.height() == height, 'The height should be {height}'.format(height=height) assert image.height() == height, 'The height should be {height}'.format(height=height)
assert image.width() == width, 'The width should be {width}'.format(width=width) assert image.width() == width, 'The width should be {width}'.format(width=width)
def test_load_pdf(self):
"""
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:
self.load_pdf(exe_path)
self.load_pdf_pictures(exe_path)
# PyMuPDF
self.load_pdf(None)
self.load_pdf_pictures(None)
@patch('openlp.plugins.presentations.lib.pdfcontroller.check_binary_exists') @patch('openlp.plugins.presentations.lib.pdfcontroller.check_binary_exists')
def test_process_check_binary_mudraw(self, mocked_check_binary_exists): def test_process_check_binary_mudraw(self, mocked_check_binary_exists):
""" """