forked from openlp/openlp
479 lines
17 KiB
Python
479 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
##########################################################################
|
|
# OpenLP - Open Source Lyrics Projection #
|
|
# ---------------------------------------------------------------------- #
|
|
# Copyright (c) 2008-2021 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, either version 3 of the License, or #
|
|
# (at your option) any later version. #
|
|
# #
|
|
# 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, see <https://www.gnu.org/licenses/>. #
|
|
##########################################################################
|
|
"""
|
|
The :mod:`lib` module contains most of the components and libraries that make
|
|
OpenLP work.
|
|
"""
|
|
import logging
|
|
import os
|
|
import base64
|
|
from enum import IntEnum
|
|
from pathlib import Path
|
|
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
|
|
from openlp.core.common.i18n import UiStrings, translate
|
|
|
|
log = logging.getLogger(__name__ + '.__init__')
|
|
|
|
|
|
class DataType(IntEnum):
|
|
U8 = 1
|
|
U16 = 2
|
|
U32 = 4
|
|
|
|
|
|
class ServiceItemContext(object):
|
|
"""
|
|
The context in which a Service Item is being generated
|
|
"""
|
|
Preview = 0
|
|
Live = 1
|
|
Service = 2
|
|
|
|
|
|
class ImageSource(object):
|
|
"""
|
|
This enumeration class represents different image sources. An image sources states where an image is used. This
|
|
enumeration class is need in the context of the :class:~openlp.core.lib.imagemanager`.
|
|
|
|
``ImagePlugin``
|
|
This states that an image is being used by the image plugin.
|
|
|
|
``Theme``
|
|
This says, that the image is used by a theme.
|
|
|
|
``CommandPlugins``
|
|
This states that an image is being used by a command plugin.
|
|
"""
|
|
ImagePlugin = 1
|
|
Theme = 2
|
|
CommandPlugins = 3
|
|
|
|
|
|
class MediaType(object):
|
|
"""
|
|
An enumeration class for types of media.
|
|
"""
|
|
Audio = 1
|
|
Video = 2
|
|
|
|
|
|
class ServiceItemAction(object):
|
|
"""
|
|
Provides an enumeration for the required action moving between service items by left/right arrow keys
|
|
"""
|
|
Previous = 1
|
|
PreviousLastSlide = 2
|
|
Next = 3
|
|
|
|
|
|
class ItemCapabilities(object):
|
|
"""
|
|
Provides an enumeration of a service item's capabilities
|
|
|
|
``CanPreview``
|
|
The capability to allow the ServiceManager to add to the preview tab when making the previous item live.
|
|
|
|
``CanEdit``
|
|
The capability to allow the ServiceManager to allow the item to be edited
|
|
|
|
``CanMaintain``
|
|
The capability to allow the ServiceManager to allow the item to be reordered.
|
|
|
|
``RequiresMedia``
|
|
Determines is the service_item needs a Media Player
|
|
|
|
``CanLoop``
|
|
The capability to allow the SlideController to allow the loop processing.
|
|
|
|
``CanAppend``
|
|
The capability to allow the ServiceManager to add leaves to the
|
|
item
|
|
|
|
``NoLineBreaks``
|
|
The capability to remove lines breaks in the renderer
|
|
|
|
``OnLoadUpdate``
|
|
The capability to update MediaManager when a service Item is loaded.
|
|
|
|
``AddIfNewItem``
|
|
Not Used
|
|
|
|
``ProvidesOwnDisplay``
|
|
The capability to tell the SlideController the service Item has a different display.
|
|
|
|
``HasDetailedTitleDisplay``
|
|
Being Removed and decommissioned.
|
|
|
|
``HasVariableStartTime``
|
|
The capability to tell the ServiceManager that a change to start time is possible.
|
|
|
|
``CanSoftBreak``
|
|
The capability to tell the renderer that Soft Break is allowed
|
|
|
|
``CanWordSplit``
|
|
The capability to tell the renderer that it can split words is
|
|
allowed
|
|
|
|
``HasBackgroundAudio``
|
|
That a audio file is present with the text.
|
|
|
|
``CanAutoStartForLive``
|
|
The capability to ignore the do not play if display blank flag.
|
|
|
|
``CanEditTitle``
|
|
The capability to edit the title of the item
|
|
|
|
``IsOptical``
|
|
Determines is the service_item is based on an optical device
|
|
|
|
``HasDisplayTitle``
|
|
The item contains 'displaytitle' on every frame which should be
|
|
preferred over 'title' when displaying the item
|
|
|
|
``HasNotes``
|
|
The item contains 'notes'
|
|
|
|
``HasThumbnails``
|
|
The item has related thumbnails available
|
|
|
|
``HasMetaData``
|
|
The item has Meta Data about item
|
|
|
|
``CanStream``
|
|
The item requires to process a VLC Stream
|
|
|
|
``HasBackgroundVideo``
|
|
That a video file is present with the text
|
|
|
|
``HasBackgroundStream``
|
|
That a video stream is present with the text
|
|
|
|
``ProvidesOwnTheme``
|
|
The capability to tell the SlideController to force the use of the service item theme.
|
|
"""
|
|
CanPreview = 1
|
|
CanEdit = 2
|
|
CanMaintain = 3
|
|
RequiresMedia = 4
|
|
CanLoop = 5
|
|
CanAppend = 6
|
|
NoLineBreaks = 7
|
|
OnLoadUpdate = 8
|
|
AddIfNewItem = 9
|
|
ProvidesOwnDisplay = 10
|
|
# HasDetailedTitleDisplay = 11
|
|
HasVariableStartTime = 12
|
|
CanSoftBreak = 13
|
|
CanWordSplit = 14
|
|
HasBackgroundAudio = 15
|
|
CanAutoStartForLive = 16
|
|
CanEditTitle = 17
|
|
IsOptical = 18
|
|
HasDisplayTitle = 19
|
|
HasNotes = 20
|
|
HasThumbnails = 21
|
|
HasMetaData = 22
|
|
CanStream = 23
|
|
HasBackgroundVideo = 24
|
|
HasBackgroundStream = 25
|
|
ProvidesOwnTheme = 26
|
|
|
|
|
|
def get_text_file_string(text_file_path):
|
|
"""
|
|
Open a file and return its content as a string. If the supplied file path is not a file then the function
|
|
returns False. If there is an error loading the file or the content can't be decoded then the function will return
|
|
None.
|
|
|
|
:param Path text_file_path: The path to the file.
|
|
:return: The contents of the file, False if the file does not exist, or None if there is an Error reading or
|
|
decoding the file.
|
|
:rtype: str | False | None
|
|
"""
|
|
if not text_file_path.is_file():
|
|
return False
|
|
content = None
|
|
try:
|
|
with text_file_path.open('r', encoding='utf-8') as file_handle:
|
|
if file_handle.read(3) != '\xEF\xBB\xBF':
|
|
# no BOM was found
|
|
file_handle.seek(0)
|
|
content = file_handle.read()
|
|
except (OSError, UnicodeError):
|
|
log.exception('Failed to open text file {text}'.format(text=text_file_path))
|
|
return content
|
|
|
|
|
|
def str_to_bool(string_value):
|
|
"""
|
|
Convert a string version of a boolean into a real boolean.
|
|
|
|
:param string_value: The string value to examine and convert to a boolean type.
|
|
:return: The correct boolean value
|
|
"""
|
|
if isinstance(string_value, bool):
|
|
return string_value
|
|
return str(string_value).strip().lower() in ('true', 'yes', 'y')
|
|
|
|
|
|
def build_icon(icon):
|
|
"""
|
|
Build a QIcon instance from an existing QIcon, a resource location, or a physical file location. If the icon is a
|
|
QIcon instance, that icon is simply returned. If not, it builds a QIcon instance from the resource or file name.
|
|
|
|
:param QtGui.QIcon | Path | QtGui.QIcon | str icon:
|
|
The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file path
|
|
location like ``Path(/path/to/file.png)``. However, the **recommended** way is to specify a resource string.
|
|
:return: The build icon.
|
|
:rtype: QtGui.QIcon
|
|
"""
|
|
if isinstance(icon, QtGui.QIcon):
|
|
return icon
|
|
pix_map = None
|
|
button_icon = QtGui.QIcon()
|
|
if isinstance(icon, str):
|
|
pix_map = QtGui.QPixmap(icon)
|
|
elif isinstance(icon, Path):
|
|
pix_map = QtGui.QPixmap(str(icon))
|
|
elif isinstance(icon, QtGui.QImage): # pragma: no cover
|
|
pix_map = QtGui.QPixmap.fromImage(icon)
|
|
if pix_map:
|
|
button_icon.addPixmap(pix_map, QtGui.QIcon.Normal, QtGui.QIcon.Off)
|
|
return button_icon
|
|
|
|
|
|
def image_to_byte(image, base_64=True):
|
|
"""
|
|
Resize an image to fit on the current screen for the web and returns it as a byte stream.
|
|
|
|
:param image: The image to be converted.
|
|
:param base_64: If True returns the image as Base64 bytes, otherwise the image is returned as a byte array.
|
|
To preserve original intention, this defaults to True
|
|
"""
|
|
log.debug('image_to_byte - start')
|
|
byte_array = QtCore.QByteArray()
|
|
# use buffer to store pixmap into byteArray
|
|
buffer = QtCore.QBuffer(byte_array)
|
|
buffer.open(QtCore.QIODevice.WriteOnly)
|
|
image.save(buffer, "JPEG")
|
|
log.debug('image_to_byte - end')
|
|
if not base_64:
|
|
return byte_array
|
|
# convert to base64 encoding so does not get missed!
|
|
return base64.b64encode(byte_array).decode('utf-8')
|
|
|
|
|
|
def image_to_data_uri(image_path):
|
|
"""
|
|
Converts a image into a base64 data uri
|
|
"""
|
|
extension = image_path.suffix.replace('.', '')
|
|
with open(image_path, 'rb') as image_file:
|
|
image_bytes = image_file.read()
|
|
image_base64 = base64.b64encode(image_bytes).decode('utf-8')
|
|
return 'data:image/{extension};base64,{data}'.format(extension=extension, data=image_base64)
|
|
|
|
|
|
def create_thumb(image_path, thumb_path, return_icon=True, size=None):
|
|
"""
|
|
Create a thumbnail from the given image path and depending on ``return_icon`` it returns an icon from this thumb.
|
|
|
|
:param Path image_path: The image file to create the icon from.
|
|
:param Path thumb_path: The filename to save the thumbnail to.
|
|
:param return_icon: States if an icon should be build and returned from the thumb. Defaults to ``True``.
|
|
:param size: Allows to state a own size (QtCore.QSize) to use. Defaults to ``None``, which means that a default
|
|
height of 88 is used.
|
|
:return: The final icon.
|
|
"""
|
|
reader = QtGui.QImageReader(str(image_path))
|
|
if size is None:
|
|
# No size given; use default height of 88
|
|
if reader.size().isEmpty():
|
|
ratio = 1
|
|
else:
|
|
ratio = reader.size().width() / reader.size().height()
|
|
reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88))
|
|
elif size.isValid():
|
|
# Complete size given
|
|
reader.setScaledSize(size)
|
|
else:
|
|
# Invalid size given
|
|
if reader.size().isEmpty():
|
|
ratio = 1
|
|
else:
|
|
ratio = reader.size().width() / reader.size().height()
|
|
if size.width() >= 0:
|
|
# Valid width; scale height
|
|
reader.setScaledSize(QtCore.QSize(size.width(), int(size.width() / ratio)))
|
|
elif size.height() >= 0:
|
|
# Valid height; scale width
|
|
reader.setScaledSize(QtCore.QSize(int(ratio * size.height()), size.height()))
|
|
else:
|
|
# Invalid; use default height of 88
|
|
reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88))
|
|
thumb = reader.read()
|
|
thumb.save(str(thumb_path), thumb_path.suffix[1:].lower())
|
|
if not return_icon:
|
|
return
|
|
if thumb_path.exists():
|
|
return build_icon(thumb_path)
|
|
# Fallback for files with animation support.
|
|
return build_icon(image_path)
|
|
|
|
|
|
def validate_thumb(file_path, thumb_path):
|
|
"""
|
|
Validates whether an file's thumb still exists and if is up to date. **Note**, you must **not** call this function,
|
|
before checking the existence of the file.
|
|
|
|
:param Path file_path: The path to the file. The file **must** exist!
|
|
:param Path thumb_path: The path to the thumb.
|
|
:return: Has the image changed since the thumb was created?
|
|
:rtype: bool
|
|
"""
|
|
if not thumb_path.exists():
|
|
return False
|
|
image_date = file_path.stat().st_mtime
|
|
thumb_date = thumb_path.stat().st_mtime
|
|
return image_date <= thumb_date
|
|
|
|
|
|
def resize_image(image_path, width, height, background='#000000', ignore_aspect_ratio=False):
|
|
"""
|
|
Resize an image to fit on the current screen.
|
|
|
|
DO NOT REMOVE THE DEFAULT BACKGROUND VALUE!
|
|
|
|
:param image_path: The path to the image to resize.
|
|
:param width: The new image width.
|
|
:param height: The new image height.
|
|
:param background: The background colour. Defaults to black.
|
|
"""
|
|
log.debug('resize_image - start')
|
|
reader = QtGui.QImageReader(image_path)
|
|
# The image's ratio.
|
|
image_ratio = reader.size().width() / reader.size().height()
|
|
resize_ratio = width / height
|
|
# Figure out the size we want to resize the image to (keep aspect ratio).
|
|
if image_ratio == resize_ratio or ignore_aspect_ratio:
|
|
size = QtCore.QSize(width, height)
|
|
elif image_ratio < resize_ratio:
|
|
# Use the image's height as reference for the new size.
|
|
size = QtCore.QSize(int(image_ratio * height), height)
|
|
else:
|
|
# Use the image's width as reference for the new size.
|
|
size = QtCore.QSize(width, int(1 / (image_ratio / width)))
|
|
reader.setScaledSize(size)
|
|
preview = reader.read()
|
|
if image_ratio == resize_ratio:
|
|
# We neither need to centre the image nor add "bars" to the image.
|
|
return preview
|
|
real_width = preview.width()
|
|
real_height = preview.height()
|
|
# and move it to the centre of the preview space
|
|
new_image = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32_Premultiplied)
|
|
painter = QtGui.QPainter(new_image)
|
|
painter.fillRect(new_image.rect(), QtGui.QColor(background))
|
|
painter.drawImage((width - real_width) // 2, (height - real_height) // 2, preview)
|
|
return new_image
|
|
|
|
|
|
def check_item_selected(list_widget, message):
|
|
"""
|
|
Check if a list item is selected so an action may be performed on it
|
|
|
|
:param list_widget: The list to check for selected items
|
|
:param message: The message to give the user if no item is selected
|
|
"""
|
|
if not list_widget.selectedIndexes():
|
|
QtWidgets.QMessageBox.information(list_widget.parent(),
|
|
translate('OpenLP.MediaManagerItem', 'No Items Selected'), message)
|
|
return False
|
|
return True
|
|
|
|
|
|
def create_separated_list(string_list):
|
|
"""
|
|
Returns a string that represents a join of a list of strings with a localized separator.
|
|
Localized separation will be done via the translate() function by the translators.
|
|
|
|
:param string_list: List of unicode strings
|
|
:return: Formatted string
|
|
"""
|
|
list_length = len(string_list)
|
|
if list_length == 1:
|
|
list_to_string = string_list[0]
|
|
elif list_length == 2:
|
|
list_to_string = translate('OpenLP.core.lib', '{one} and {two}').format(one=string_list[0], two=string_list[1])
|
|
elif list_length > 2:
|
|
list_to_string = translate('OpenLP.core.lib', '{first} and {last}').format(first=', '.join(string_list[:-1]),
|
|
last=string_list[-1])
|
|
else:
|
|
list_to_string = ''
|
|
return list_to_string
|
|
|
|
|
|
def read_or_fail(file_object, length):
|
|
"""
|
|
Ensure that the data read is as the exact length requested. Otherwise raise an OSError.
|
|
|
|
:param io.IOBase file_object: The file-lke object ot read from.
|
|
:param int length: The length of the data to read.
|
|
:return: The data read.
|
|
"""
|
|
data = file_object.read(length)
|
|
if len(data) != length:
|
|
raise OSError(UiStrings().FileCorrupt)
|
|
return data
|
|
|
|
|
|
def read_int(file_object, data_type, endian='big'):
|
|
"""
|
|
Read the correct amount of data from a file-like object to decode it to the specified type.
|
|
|
|
:param io.IOBase file_object: The file-like object to read from.
|
|
:param DataType data_type: A member from the :enum:`DataType`
|
|
:param endian: The endianess of the data to be read
|
|
:return int: The decoded int
|
|
"""
|
|
data = read_or_fail(file_object, data_type)
|
|
return int.from_bytes(data, endian)
|
|
|
|
|
|
def seek_or_fail(file_object, offset, how=os.SEEK_SET):
|
|
"""
|
|
See to a set position and return an error if the cursor has not moved to that position.
|
|
|
|
:param io.IOBase file_object: The file-like object to attempt to seek.
|
|
:param int offset: The offset / position to seek by / to.
|
|
:param [os.SEEK_CUR | os.SEEK_SET how: Currently only supports os.SEEK_CUR (0) or os.SEEK_SET (1)
|
|
:return int: The new position in the file.
|
|
"""
|
|
if how not in (os.SEEK_CUR, os.SEEK_SET):
|
|
raise NotImplementedError
|
|
prev_pos = file_object.tell()
|
|
new_pos = file_object.seek(offset, how)
|
|
if how == os.SEEK_SET and new_pos != offset or how == os.SEEK_CUR and new_pos != prev_pos + offset:
|
|
raise OSError(UiStrings().FileCorrupt)
|
|
return new_pos
|