forked from openlp/openlp
339 lines
14 KiB
Python
339 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
|
|
|
###############################################################################
|
|
# OpenLP - Open Source Lyrics Projection #
|
|
# --------------------------------------------------------------------------- #
|
|
# Copyright (c) 2008-2018 OpenLP Developers #
|
|
# --------------------------------------------------------------------------- #
|
|
# 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 logging
|
|
import re
|
|
from subprocess import CalledProcessError, check_output
|
|
|
|
from openlp.core.common import check_binary_exists, is_win
|
|
from openlp.core.common.applocation import AppLocation
|
|
from openlp.core.common.path import which
|
|
from openlp.core.common.settings import Settings
|
|
from openlp.core.display.screens import ScreenList
|
|
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
|
|
|
|
|
|
if is_win():
|
|
from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
PDF_CONTROLLER_FILETYPES = ['pdf', 'xps', 'oxps']
|
|
|
|
|
|
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')
|
|
super(PdfController, self).__init__(plugin, 'Pdf', PdfDocument)
|
|
self.process = None
|
|
self.supports = ['pdf']
|
|
self.also_supports = []
|
|
# 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 openlp.core.common.path.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.
|
|
|
|
: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 = None
|
|
self.mutoolbin = None
|
|
self.gsbin = None
|
|
self.also_supports = []
|
|
# Use the user defined program if given
|
|
if Settings().value('presentations/enable_pdf_program'):
|
|
program_path = 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
|
|
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']
|
|
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, document_path):
|
|
"""
|
|
Constructor, store information about the file and initialise.
|
|
|
|
:param openlp.core.common.path.Path document_path: Path to the document to load
|
|
:rtype: None
|
|
"""
|
|
log.debug('Init Presentation Pdf')
|
|
super().__init__(controller, document_path)
|
|
self.presentation = None
|
|
self.blanked = False
|
|
self.hidden = False
|
|
self.image_files = []
|
|
self.num_pages = -1
|
|
# Setup startupinfo options for check_output to avoid console popping up on windows
|
|
if is_win():
|
|
self.startupinfo = STARTUPINFO()
|
|
self.startupinfo.dwFlags |= STARTF_USESHOWWINDOW
|
|
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.
|
|
|
|
:return: True is loading succeeded, otherwise False.
|
|
"""
|
|
log.debug('load_presentation pdf')
|
|
temp_dir_path = self.get_temp_folder()
|
|
# Check if the images has already been created, and if yes load them
|
|
if (temp_dir_path / 'mainslide001.png').is_file():
|
|
created_files = sorted(temp_dir_path.glob('*'))
|
|
for image_path in created_files:
|
|
if image_path.is_file():
|
|
self.image_files.append(image_path)
|
|
self.num_pages = len(self.image_files)
|
|
return True
|
|
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)
|
|
created_files = sorted(temp_dir_path.glob('*'))
|
|
for image_path in created_files:
|
|
if image_path.is_file():
|
|
self.image_files.append(image_path)
|
|
except Exception:
|
|
log.exception(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
|