forked from openlp/openlp
Add PyMuPDF as additional PDF controller and missing mupdf file formats
bzr-revno: 2864
This commit is contained in:
commit
db9cc8c310
@ -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)
|
||||||
|
@ -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():
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
2
setup.py
2
setup.py
@ -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"'
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user