Fix Image Backgounds and remove Image Manager

Resolves ##480
This commit is contained in:
Tim Bentley 2020-06-20 06:06:31 +00:00
parent d117430175
commit f8d747e236
17 changed files with 26 additions and 791 deletions

View File

@ -60,7 +60,6 @@ def controller_text_api():
full_thumbnail_path = AppLocation.get_data_path() / thumbnail_path
if not full_thumbnail_path.exists():
create_thumb(Path(current_item.get_frame_path(index)), full_thumbnail_path, False)
# Registry().get('image_manager').add_image(str(full_thumbnail_path), frame['title'], None, 88, 88)
item['img'] = urllib.request.pathname2url(os.path.sep + str(thumbnail_path))
item['text'] = str(frame['title'])
item['html'] = str(frame['title'])
@ -76,7 +75,6 @@ def controller_text_api():
data_path = str(AppLocation.get_data_path())
if frame['image'][0:len(data_path)] == data_path:
item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):])
# Registry().get('image_manager').add_image(frame['image'], frame['title'], None, 88, 88)
item['text'] = str(frame['title'])
item['html'] = str(frame['title'])
data.append(item)

View File

@ -60,7 +60,6 @@ def controller_text_api():
full_thumbnail_path = AppLocation.get_data_path() / thumbnail_path
if not full_thumbnail_path.exists():
create_thumb(Path(current_item.get_frame_path(index)), full_thumbnail_path, False)
# Registry().get('image_manager').add_image(str(full_thumbnail_path), frame['title'], None, 88, 88)
item['img'] = urllib.request.pathname2url(os.path.sep + str(thumbnail_path))
item['text'] = str(frame['title'])
item['html'] = str(frame['title'])
@ -76,7 +75,6 @@ def controller_text_api():
data_path = str(AppLocation.get_data_path())
if frame['image'][0:len(data_path)] == data_path:
item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):])
# Registry().get('image_manager').add_image(frame['image'], frame['title'], None, 88, 88)
item['text'] = str(frame['title'])
item['html'] = str(frame['title'])
data.append(item)

View File

@ -114,7 +114,6 @@ class RegistryProperties(object):
"""
_application = None
_plugin_manager = None
_image_manager = None
_media_controller = None
_service_manager = None
_preview_controller = None
@ -149,15 +148,6 @@ class RegistryProperties(object):
self._plugin_manager = Registry().get('plugin_manager')
return self._plugin_manager
@property
def image_manager(self):
"""
Adds the image manager to the class dynamically
"""
if not hasattr(self, '_image_manager') or not self._image_manager:
self._image_manager = Registry().get('image_manager')
return self._image_manager
@property
def media_controller(self):
"""

View File

@ -756,13 +756,13 @@ var Display = {
* Set image slides
* @param {Object[]} slides - A list of images to add as JS objects [{"path": "url/to/file"}]
*/
setImageSlides: function (slides) {
setImageSlides: function (slides, background) {
Display._clearSlidesList();
var parentSection = document.createElement("section");
slides.forEach(function (slide, index) {
var section = document.createElement("section");
section.setAttribute("id", index);
section.setAttribute("data-background", "#000");
section.setAttribute("data-background", background);
section.setAttribute("style", "height: 100%; width: 100%;");
var img = document.createElement('img');
img.src = slide.path;

View File

@ -326,7 +326,9 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
else:
image['thumbnail'] = image['path']
json_images = json.dumps(imagesr)
self.run_javascript('Display.setImageSlides({images});'.format(images=json_images))
background = self.settings.value('images/background color')
self.run_javascript('Display.setImageSlides({images}, "{background}");'.format(images=json_images,
background=background))
def load_video(self, video):
"""

View File

@ -1,353 +0,0 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2020 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/>. #
##########################################################################
"""
Provides the store and management for Images automatically caching them and resizing them when needed. Only one copy of
each image is needed in the system. A Thread is used to convert the image to a byte array so the user does not need to
wait for the conversion to happen.
"""
import logging
import os
import queue
import time
from PyQt5 import QtCore
from openlp.core.common.registry import Registry
from openlp.core.display.screens import ScreenList
from openlp.core.lib import image_to_byte, resize_image
from openlp.core.threading import ThreadWorker, run_thread
log = logging.getLogger(__name__)
class ImageWorker(ThreadWorker):
"""
A thread worker class to speed up the display of images. This is threaded so it loads the frames and generates
byte stream in background.
"""
def __init__(self, manager):
"""
Constructor for the thread class.
``manager``
The image manager.
"""
super().__init__()
self.image_manager = manager
def start(self):
"""
Start the worker
"""
self.image_manager.process()
self.quit.emit()
def stop(self):
"""
Stop the worker
"""
self.image_manager.stop_manager = True
class Priority(object):
"""
Enumeration class for different priorities.
``Lowest``
Only the image's byte stream has to be generated. But neither the ``QImage`` nor the byte stream has been
requested yet.
``Low``
Only the image's byte stream has to be generated. Because the image's ``QImage`` has been requested previously
it is reasonable to assume that the byte stream will be needed before the byte stream of other images whose
``QImage`` were not generated due to a request.
``Normal``
The image's byte stream as well as the image has to be generated. Neither the ``QImage`` nor the byte stream has
been requested yet.
``High``
The image's byte stream as well as the image has to be generated. The ``QImage`` for this image has been
requested. **Note**, this priority is only set when the ``QImage`` has not been generated yet.
``Urgent``
The image's byte stream as well as the image has to be generated. The byte stream for this image has been
requested. **Note**, this priority is only set when the byte stream has not been generated yet.
"""
Lowest = 4
Low = 3
Normal = 2
High = 1
Urgent = 0
class Image(object):
"""
This class represents an image. To mark an image as *dirty* call the :class:`ImageManager`'s ``_reset_image`` method
with the Image instance as argument.
"""
secondary_priority = 0
def __init__(self, path, source, background, width=-1, height=-1):
"""
Create an image for the :class:`ImageManager`'s cache.
:param path: The image's file path. This should be an existing file path.
:param source: The source describes the image's origin. Possible values are described in the
:class:`~openlp.core.lib.ImageSource` class.
:param background: A ``QtGui.QColor`` object specifying the colour to be used to fill the gabs if the image's
ratio does not match with the display ratio.
:param width: The width of the image, defaults to -1 meaning that the screen width will be used.
:param height: The height of the image, defaults to -1 meaning that the screen height will be used.
"""
if not os.path.exists(path):
raise FileNotFoundError('{path} not found'.format(path=path))
self.path = path
self.image = None
self.image_bytes = None
self.priority = Priority.Normal
self.source = source
self.background = background
self.timestamp = 0
self.width = width
self.height = height
self.timestamp = os.stat(path).st_mtime
self.secondary_priority = Image.secondary_priority
Image.secondary_priority += 1
class PriorityQueue(queue.PriorityQueue):
"""
Customised ``queue.PriorityQueue``.
Each item in the queue must be a tuple with three values. The first value is the :class:`Image`'s ``priority``
attribute, the second value the :class:`Image`'s ``secondary_priority`` attribute. The last value the :class:`Image`
instance itself::
(image.priority, image.secondary_priority, image)
Doing this, the :class:`Queue.PriorityQueue` will sort the images according to their priorities, but also according
to there number. However, the number only has an impact on the result if there are more images with the same
priority. In such case the image which has been added earlier is privileged.
"""
def modify_priority(self, image, new_priority):
"""
Modifies the priority of the given ``image``.
:param image: The image to remove. This should be an :class:`Image` instance.
:param new_priority: The image's new priority. See the :class:`Priority` class for priorities.
"""
self.remove(image)
image.priority = new_priority
self.put((image.priority, image.secondary_priority, image))
def remove(self, image):
"""
Removes the given ``image`` from the queue.
:param image: The image to remove. This should be an ``Image`` instance.
"""
if (image.priority, image.secondary_priority, image) in self.queue:
self.queue.remove((image.priority, image.secondary_priority, image))
class ImageManager(QtCore.QObject):
"""
Image Manager handles the conversion and sizing of images.
"""
log.info('Image Manager loaded')
def __init__(self):
"""
Constructor for the image manager.
"""
super(ImageManager, self).__init__()
Registry().register('image_manager', self)
current_screen = ScreenList().current
self.width = current_screen.display_geometry.width()
self.height = current_screen.display_geometry.height()
self._cache = {}
self._conversion_queue = PriorityQueue()
self.stop_manager = False
Registry().register_function('images_regenerate', self.process_updates)
def update_display(self):
"""
Screen has changed size so rebuild the cache to new size.
"""
log.debug('update_display')
current_screen = ScreenList().current
self.width = current_screen.display_geometry.width()
self.height = current_screen.display_geometry.height()
# Mark the images as dirty for a rebuild by setting the image and byte stream to None.
for image in list(self._cache.values()):
self._reset_image(image)
def update_images_border(self, source, background):
"""
Border has changed so update all the images affected.
"""
log.debug('update_images_border')
# Mark the images as dirty for a rebuild by setting the image and byte stream to None.
for image in list(self._cache.values()):
if image.source == source:
image.background = background
self._reset_image(image)
def update_image_border(self, path, source, background, width=-1, height=-1):
"""
Border has changed so update the image affected.
"""
log.debug('update_image_border')
# Mark the image as dirty for a rebuild by setting the image and byte stream to None.
image = self._cache[(path, source, width, height)]
if image.source == source:
image.background = background
self._reset_image(image)
def _reset_image(self, image):
"""
Mark the given :class:`Image` instance as dirty by setting its ``image`` and ``image_bytes`` attributes to None.
"""
image.image = None
image.image_bytes = None
self._conversion_queue.modify_priority(image, Priority.Normal)
def process_updates(self):
"""
Flush the queue to updated any data to update
"""
try:
worker = ImageWorker(self)
run_thread(worker, 'image_manager')
except KeyError:
# run_thread() will throw a KeyError if this thread already exists, so ignore it so that we don't
# try to start another thread when one is already running
pass
def get_image(self, path, source, width=-1, height=-1):
"""
Return the ``QImage`` from the cache. If not present wait for the background thread to process it.
:param: path: The image path
:param: source: The source of the image
:param: background: The image background colour
:param: width: The processed image width
:param: height: The processed image height
"""
log.debug('get_image {path} {source} {width} {height}'.format(path=path, source=source,
width=width, height=height))
image = self._cache[(path, source, width, height)]
if image.image is None:
self._conversion_queue.modify_priority(image, Priority.High)
# make sure we are running and if not give it a kick
self.process_updates()
while image.image is None:
log.debug('getImage - waiting')
time.sleep(0.1)
elif image.image_bytes is None:
# Set the priority to Low, because the image was requested but the byte stream was not generated yet.
# However, we only need to do this, when the image was generated before it was requested (otherwise this is
# already taken care of).
self._conversion_queue.modify_priority(image, Priority.Low)
return image.image
def get_image_bytes(self, path, source, width=-1, height=-1):
"""
Returns the byte string for an image. If not present wait for the background thread to process it.
:param: path: The image path
:param: source: The source of the image
:param: background: The image background colour
:param: width: The processed image width
:param: height: The processed image height
"""
log.debug('get_image_bytes {path} {source} {width} {height}'.format(path=path, source=source,
width=width, height=height))
image = self._cache[(path, source, width, height)]
if image.image_bytes is None:
self._conversion_queue.modify_priority(image, Priority.Urgent)
# make sure we are running and if not give it a kick
self.process_updates()
while image.image_bytes is None:
log.debug('getImageBytes - waiting')
time.sleep(0.1)
return image.image_bytes
def add_image(self, path, source, background, width=-1, height=-1):
"""
Add image to cache if it is not already there.
:param: path: The image path
:param: source: The source of the image
:param: background: The image background colour
:param: width: The processed image width
:param: height: The processed image height
"""
log.debug('add_image {path} {source} {width} {height}'.format(path=path, source=source,
width=width, height=height))
if not (path, source, width, height) in self._cache:
image = Image(path, source, background, width, height)
self._cache[(path, source, width, height)] = image
self._conversion_queue.put((image.priority, image.secondary_priority, image))
# Check if the there are any images with the same path and check if the timestamp has changed.
for image in list(self._cache.values()):
if os.path.exists(path):
if image.path == path and image.timestamp != os.stat(path).st_mtime:
image.timestamp = os.stat(path).st_mtime
self._reset_image(image)
self.process_updates()
def process(self):
"""
Controls the processing called from a ``QtCore.QThread``.
"""
log.debug('process - started')
while not self._conversion_queue.empty() and not self.stop_manager:
self._process_cache()
log.debug('_process - ended')
def _process_cache(self):
"""
Actually does the work.
"""
log.debug('_processCache')
image = self._conversion_queue.get()[2]
# Generate the QImage for the image.
if image.image is None:
# Let's see if the image was requested with specific dimensions
width = self.width if image.width == -1 else image.width
height = self.height if image.height == -1 else image.height
image.image = resize_image(image.path, width, height, image.background,
Registry().get('settings').value('advanced/ignore aspect ratio'))
# Set the priority to Lowest and stop here as we need to process more important images first.
if image.priority == Priority.Normal:
self._conversion_queue.modify_priority(image, Priority.Lowest)
return
# For image with high priority we set the priority to Low, as the byte stream might be needed earlier the
# byte stream of image with Normal priority. We stop here as we need to process more important images first.
elif image.priority == Priority.High:
self._conversion_queue.modify_priority(image, Priority.Low)
return
# Generate the byte stream for the image.
if image.image_bytes is None:
image.image_bytes = image_to_byte(image.image)

View File

@ -47,7 +47,6 @@ class PluginManager(RegistryBase, LogMixin, RegistryProperties):
super(PluginManager, self).__init__(parent)
self.log_info('Plugin manager Initialising')
self.log_debug('Base path {path}'.format(path=AppLocation.get_directory(AppLocation.PluginsDir)))
self.plugins = []
self.log_info('Plugin manager Initialised')
def bootstrap_initialise(self):

View File

@ -96,7 +96,6 @@ class ServiceItem(RegistryProperties):
self.end_time = 0
self.media_length = 0
self.from_service = False
self.image_border = '#000000'
self.background_audio = []
self.theme_overwritten = False
self.temporary_edit = False
@ -279,17 +278,15 @@ class ServiceItem(RegistryProperties):
self._print_slides.append(slide)
return self._print_slides
def add_from_image(self, path, title, background=None, thumbnail=None, file_hash=None):
def add_from_image(self, path, title, thumbnail=None, file_hash=None):
"""
Add an image slide to the service item.
:param path: The directory in which the image file is located.
:param title: A title for the slide in the service item.
:param background: The background colour
:param thumbnail: Optional alternative thumbnail, used for remote thumbnails.
:param file_hash: Unique Reference to file .
"""
if background:
self.image_border = background
self.service_item_type = ServiceItemType.Image
if not file_hash:
file_hash = sha256_file_hash(path)
@ -297,7 +294,6 @@ class ServiceItem(RegistryProperties):
if thumbnail:
slide['thumbnail'] = thumbnail
self.slides.append(slide)
# self.image_manager.add_image(path, ImageSource.ImagePlugin, self.image_border)
self._new_item()
def add_from_text(self, text, verse_tag=None):
@ -346,8 +342,6 @@ class ServiceItem(RegistryProperties):
ntpath.basename(image)
self.slides.append({'title': file_name, 'image': image, 'path': path, 'display_title': display_title,
'notes': notes, 'thumbnail': image})
# if self.is_capable(ItemCapabilities.HasThumbnails):
# self.image_manager.add_image(image, ImageSource.CommandPlugins, '#000000')
self._new_item()
def get_service_repr(self, lite_save):
@ -496,8 +490,6 @@ class ServiceItem(RegistryProperties):
self.add_from_text(slide['raw_slide'], slide['verseTag'])
self._create_slides()
elif self.service_item_type == ServiceItemType.Image:
settings_section = service_item['serviceitem']['header']['name']
background = QtGui.QColor(self.settings.value(settings_section + '/background color'))
if path:
self.has_original_file_path = False
for text_image in service_item['serviceitem']['data']:
@ -522,7 +514,7 @@ class ServiceItem(RegistryProperties):
test_thumb = AppLocation.get_section_data_path(self.name) / 'thumbnails' / new_file
if test_thumb.exists():
thumbnail = test_thumb
self.add_from_image(file_path, text_image, background, thumbnail=thumbnail, file_hash=file_hash)
self.add_from_image(file_path, text_image, thumbnail=thumbnail, file_hash=file_hash)
else:
for text_image in service_item['serviceitem']['data']:
file_hash = None
@ -540,7 +532,7 @@ class ServiceItem(RegistryProperties):
test_thumb = AppLocation.get_section_data_path(self.name) / 'thumbnails' / new_file
if test_thumb.exists():
thumbnail = test_thumb
self.add_from_image(file_path, text, background, thumbnail=thumbnail, file_hash=file_hash)
self.add_from_image(file_path, text, thumbnail=thumbnail, file_hash=file_hash)
elif self.service_item_type == ServiceItemType.Command:
if version < 3:
# If this is an old servicefile with files included, we need to rename the bundled files to match

View File

@ -991,7 +991,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
The screen has changed so we have to update components such as the renderer.
"""
self.application.set_busy_cursor()
# self.image_manager.update_display()
self.renderer.resize(self.live_controller.screens.current.display_geometry.size())
self.preview_controller.screen_size_changed()
self.live_controller.setup_displays()

View File

@ -553,7 +553,6 @@ class ImageMediaItem(MediaManagerItem):
:param context: Why is it being generated
:param kwargs: Consume other unused args specified by the base implementation, but not use by this one.
"""
background = QtGui.QColor(self.settings.value('images/background color'))
if item:
items = [item]
else:
@ -611,7 +610,7 @@ class ImageMediaItem(MediaManagerItem):
for image in images:
name = image.file_path.name
thumbnail_path = self.generate_thumbnail_path(image)
service_item.add_from_image(image.file_path, name, background, thumbnail_path)
service_item.add_from_image(image.file_path, name, thumbnail_path)
return True
def check_group_exists(self, new_group):

View File

@ -231,13 +231,13 @@ class SongImportForm(OpenLPWizard, RegistryProperties):
filters += '{text} (*)'.format(text=UiStrings().AllFiles)
file_paths, filter_used = FileDialog.getOpenFileNames(
self, title,
self.settings.value('presentations/last directory import'), filters)
self.settings.value('songs/last directory import'), filters)
for file_path in file_paths:
list_item = QtWidgets.QListWidgetItem(str(file_path))
list_item.setData(QtCore.Qt.UserRole, file_path)
listbox.addItem(list_item)
if file_paths:
self.settings.setValue('song/last directory import', file_paths[0].parent)
self.settings.setValue('songs/last directory import', file_paths[0].parent)
def get_list_of_paths(self, list_box):
"""
@ -348,7 +348,7 @@ class SongImportForm(OpenLPWizard, RegistryProperties):
:rtype: None
"""
file_path, filter_used = FileDialog.getSaveFileName(
self, self.settings.value(self.plugin.settings_section + '/last directory import'))
self, self.settings.value('songs/last directory import'))
if file_path is None:
return
file_path.write_text(self.error_report_text_edit.toPlainText(), encoding='utf-8')
@ -399,7 +399,7 @@ class SongImportForm(OpenLPWizard, RegistryProperties):
path_edit.filters = filters + ';;' + path_edit.filters
else:
path_edit.filters = filters
path_edit.path = self.settings.value(self.plugin.settings_section + '/last directory import')
path_edit.path = self.settings.value('songs/last directory import')
file_path_layout.addWidget(path_edit)
import_layout.addLayout(file_path_layout)
import_layout.addSpacerItem(self.stack_spacer)

View File

@ -1,298 +0,0 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2020 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/>. #
##########################################################################
"""
Package to test the openlp.core.ui package.
"""
import os
import time
from threading import Lock
from unittest import TestCase, skip
from unittest.mock import MagicMock, patch
from PyQt5 import QtGui
from openlp.core.common.registry import Registry
from openlp.core.display.screens import ScreenList
from openlp.core.lib.imagemanager import ImageManager, ImageWorker, Priority, PriorityQueue
from tests.helpers.testmixin import TestMixin
from tests.utils.constants import RESOURCE_PATH
TEST_PATH = str(RESOURCE_PATH)
@skip('Probably not going to use ImageManager')
class TestImageWorker(TestCase, TestMixin):
"""
Test all the methods in the ImageWorker class
"""
def test_init(self):
"""
Test the constructor of the ImageWorker
"""
# GIVEN: An ImageWorker class and a mocked ImageManager
mocked_image_manager = MagicMock()
# WHEN: Creating the ImageWorker
worker = ImageWorker(mocked_image_manager)
# THEN: The image_manager attribute should be set correctly
assert worker.image_manager is mocked_image_manager, \
'worker.image_manager should have been the mocked_image_manager'
@patch('openlp.core.lib.imagemanager.ThreadWorker.quit')
def test_start(self, mocked_quit):
"""
Test that the start() method of the image worker calls the process method and then emits quit.
"""
# GIVEN: A mocked image_manager and a new image worker
mocked_image_manager = MagicMock()
worker = ImageWorker(mocked_image_manager)
# WHEN: start() is called
worker.start()
# THEN: process() should have been called and quit should have been emitted
mocked_image_manager.process.assert_called_once_with()
mocked_quit.emit.assert_called_once_with()
def test_stop(self):
"""
Test that the stop method does the right thing
"""
# GIVEN: A mocked image_manager and a worker
mocked_image_manager = MagicMock()
worker = ImageWorker(mocked_image_manager)
# WHEN: The stop() method is called
worker.stop()
# THEN: The stop_manager attrivute should have been set to True
assert mocked_image_manager.stop_manager is True, 'mocked_image_manager.stop_manager should have been True'
@skip('Probably not going to use ImageManager')
class TestPriorityQueue(TestCase, TestMixin):
"""
Test the PriorityQueue class
"""
@patch('openlp.core.lib.imagemanager.PriorityQueue.remove')
@patch('openlp.core.lib.imagemanager.PriorityQueue.put')
def test_modify_priority(self, mocked_put, mocked_remove):
"""
Test the modify_priority() method of PriorityQueue
"""
# GIVEN: An instance of a PriorityQueue and a mocked image
mocked_image = MagicMock()
mocked_image.priority = Priority.Normal
mocked_image.secondary_priority = Priority.Low
queue = PriorityQueue()
# WHEN: modify_priority is called with a mocked image and a new priority
queue.modify_priority(mocked_image, Priority.High)
# THEN: The remove() method should have been called, image priority updated and put() called
mocked_remove.assert_called_once_with(mocked_image)
assert mocked_image.priority == Priority.High, 'The priority should have been Priority.High'
mocked_put.assert_called_once_with((Priority.High, Priority.Low, mocked_image))
def test_remove(self):
"""
Test the remove() method of PriorityQueue
"""
# GIVEN: A PriorityQueue instance with a mocked image and queue
mocked_image = MagicMock()
mocked_image.priority = Priority.High
mocked_image.secondary_priority = Priority.Normal
queue = PriorityQueue()
# WHEN: An image is removed
with patch.object(queue, 'queue') as mocked_queue:
mocked_queue.__contains__.return_value = True
queue.remove(mocked_image)
# THEN: The mocked queue.remove() method should have been called
mocked_queue.remove.assert_called_once_with((Priority.High, Priority.Normal, mocked_image))
@skip('Probably not going to use ImageManager')
class TestImageManager(TestCase, TestMixin):
def setUp(self):
"""
Create the UI
"""
Registry.create()
self.setup_application()
ScreenList.create(self.app.desktop())
self.image_manager = ImageManager()
self.lock = Lock()
self.sleep_time = 0.1
def tearDown(self):
"""
Delete all the C++ objects at the end so that we don't have a segfault
"""
self.image_manager.stop_manager = True
del self.app
@patch('openlp.core.lib.imagemanager.run_thread')
def test_basic_image_manager(self, mocked_run_thread):
"""
Test the Image Manager setup basic functionality
"""
# GIVEN: the an image add to the image manager
full_path = os.path.normpath(os.path.join(TEST_PATH, 'church.jpg'))
self.image_manager.add_image(full_path, 'church.jpg', None)
# WHEN the image is retrieved
image = self.image_manager.get_image(full_path, 'church.jpg')
# THEN returned record is a type of image
assert isinstance(image, QtGui.QImage), 'The returned object should be a QImage'
# WHEN: The image bytes are requested.
byte_array = self.image_manager.get_image_bytes(full_path, 'church.jpg')
# THEN: Type should be a str.
assert isinstance(byte_array, str), 'The returned object should be a str'
# WHEN the image is retrieved has not been loaded
# THEN a KeyError is thrown
with self.assertRaises(KeyError) as context:
self.image_manager.get_image(TEST_PATH, 'church1.jpg')
assert context.exception != '', 'KeyError exception should have been thrown for missing image'
@patch('openlp.core.lib.imagemanager.run_thread')
def test_different_dimension_image(self, mocked_run_thread):
"""
Test the Image Manager with dimensions
"""
# GIVEN: add an image with specific dimensions
full_path = os.path.normpath(os.path.join(TEST_PATH, 'church.jpg'))
self.image_manager.add_image(full_path, 'church.jpg', None, 80, 80)
# WHEN: the image is retrieved
image = self.image_manager.get_image(full_path, 'church.jpg', 80, 80)
# THEN: The return should be of type image
assert isinstance(image, QtGui.QImage), 'The returned object should be a QImage'
# WHEN: adding the same image with different dimensions
self.image_manager.add_image(full_path, 'church.jpg', None, 100, 100)
# THEN: the cache should contain two pictures
assert len(self.image_manager._cache) == 2, \
'Image manager should consider two dimensions of the same picture as different'
# WHEN: adding the same image with first dimensions
self.image_manager.add_image(full_path, 'church.jpg', None, 80, 80)
# THEN: the cache should still contain only two pictures
assert len(self.image_manager._cache) == 2, 'Same dimensions should not be added again'
# WHEN: calling with correct image, but wrong dimensions
with self.assertRaises(KeyError) as context:
self.image_manager.get_image(full_path, 'church.jpg', 120, 120)
assert context.exception != '', 'KeyError exception should have been thrown for missing dimension'
@patch('openlp.core.lib.imagemanager.resize_image')
@patch('openlp.core.lib.imagemanager.image_to_byte')
@patch('openlp.core.lib.imagemanager.run_thread')
def test_process_cache(self, mocked_run_thread, mocked_image_to_byte, mocked_resize_image):
"""
Test the process_cache method
"""
# GIVEN: Mocked functions
mocked_resize_image.side_effect = self.mocked_resize_image
mocked_image_to_byte.side_effect = self.mocked_image_to_byte
image1 = 'church.jpg'
image2 = 'church2.jpg'
image3 = 'church3.jpg'
image4 = 'church4.jpg'
# WHEN: Add the images. Then get the lock (=queue can not be processed).
self.lock.acquire()
self.image_manager.add_image(TEST_PATH, image1, None)
self.image_manager.add_image(TEST_PATH, image2, None)
# THEN: All images have been added to the queue, and only the first image is not be in the list anymore, but
# is being processed (see mocked methods/functions).
# Note: Priority.Normal means, that the resize_image() was not completed yet (because afterwards the #
# priority is adjusted to Priority.Lowest).
assert self.get_image_priority(image1) == Priority.Normal, "image1's priority should be 'Priority.Normal'"
assert self.get_image_priority(image2) == Priority.Normal, "image2's priority should be 'Priority.Normal'"
# WHEN: Add more images.
self.image_manager.add_image(TEST_PATH, image3, None)
self.image_manager.add_image(TEST_PATH, image4, None)
# Allow the queue to process.
self.lock.release()
# Request some "data".
self.image_manager.get_image_bytes(TEST_PATH, image4)
self.image_manager.get_image(TEST_PATH, image3)
# Now the mocked methods/functions do not have to sleep anymore.
self.sleep_time = 0
# Wait for the queue to finish.
while not self.image_manager._conversion_queue.empty():
time.sleep(0.1)
# Because empty() is not reliable, wait a litte; just to make sure.
time.sleep(0.1)
# THEN: The images' priority reflect how they were processed.
assert self.image_manager._conversion_queue.qsize() == 0, "The queue should be empty."
assert self.get_image_priority(image1) == Priority.Lowest, \
"The image should have not been requested (=Lowest)"
assert self.get_image_priority(image2) == Priority.Lowest, \
"The image should have not been requested (=Lowest)"
assert self.get_image_priority(image3) == Priority.Low, \
"Only the QImage should have been requested (=Low)."
assert self.get_image_priority(image4) == Priority.Urgent, \
"The image bytes should have been requested (=Urgent)."
def get_image_priority(self, image):
"""
This is a help method to get the priority of the given image out of the image_manager's cache.
NOTE: This requires, that the image has been added to the image manager using the *TEST_PATH*.
:param image: The name of the image. E. g. ``image1``
"""
return self.image_manager._cache[(TEST_PATH, image, -1, -1)].priority
def mocked_resize_image(self, *args):
"""
This is a mocked method, so that we can control the work flow of the image manager.
"""
self.lock.acquire()
self.lock.release()
# The sleep time is adjusted in the test case.
time.sleep(self.sleep_time)
return QtGui.QImage()
def mocked_image_to_byte(self, *args):
"""
This is a mocked method, so that we can control the work flow of the image manager.
"""
self.lock.acquire()
self.lock.release()
# The sleep time is adjusted in the test case.
time.sleep(self.sleep_time)
return ''

View File

@ -150,8 +150,6 @@ def test_hook_settings_tabs_with_disabled_plugin_and_mocked_form(registry, state
# THEN: The create_settings_tab() method should not have been called, but the plugins lists should be the same
assert 0 == mocked_plugin.create_settings_tab.call_count, \
'The create_media_manager_item() method should not have been called.'
assert mocked_settings_form.plugin_manager.plugins == plugin_manager.plugins, \
'The plugins on the settings form should be the same as the plugins in the plugin manager'
def test_hook_settings_tabs_with_active_plugin_and_mocked_form(registry, state):
@ -175,8 +173,6 @@ def test_hook_settings_tabs_with_active_plugin_and_mocked_form(registry, state):
# THEN: The create_media_manager_item() method should have been called with the mocked settings form
assert 1 == mocked_plugin.create_settings_tab.call_count, \
'The create_media_manager_item() method should have been called once.'
assert plugin_manager.plugins == mocked_settings_form.plugin_manager.plugins, \
'The plugins on the settings form should be the same as the plugins in the plugin manager'
def test_hook_settings_tabs_with_active_plugin_and_no_form(plugin_manager_env):

View File

@ -72,7 +72,6 @@ def service_item_env(state):
mocked_slide_formater = MagicMock(side_effect=side_effect_return_arg)
mocked_renderer.format_slide = mocked_slide_formater
Registry().register('renderer', mocked_renderer)
Registry().register('image_manager', MagicMock())
def test_service_item_basic(settings):
@ -266,11 +265,10 @@ def test_add_from_command_without_display_title_and_notes():
assert service_item.get_frames()[0] == frame, 'Frames should match'
@patch('openlp.core.lib.serviceitem.ServiceItem.image_manager')
@patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path')
def test_add_from_command_for_a_presentation_thumb(mocked_get_section_data_path, mocked_image_manager):
def test_add_from_command_for_a_presentation_thumb(mocked_get_section_data_path):
"""
Test the Service Item - adding a presentation, updating the thumb path & adding the thumb to image_manager
Test the Service Item - adding a presentation, updating the thumb path & adding the thumb
"""
# GIVEN: A service item, a mocked AppLocation and presentation data
mocked_get_section_data_path.return_value = Path('mocked') / 'section' / 'path'
@ -294,7 +292,6 @@ def test_add_from_command_for_a_presentation_thumb(mocked_get_section_data_path,
# THEN: verify that it is setup as a Command and that the frame data matches
assert service_item.service_item_type == ServiceItemType.Command, 'It should be a Command'
assert service_item.get_frames()[0] == frame, 'Frames should match'
# assert 1 == mocked_image_manager.add_image.call_count, 'image_manager should be used'
def test_service_item_load_optical_media_from_service(state_media):

View File

@ -815,21 +815,18 @@ def test_on_preview_double_click_add_to_service(mock_settings):
assert 1 == slide_controller.on_preview_add_to_service.call_count, 'Should have been called once.'
@patch(u'openlp.core.ui.slidecontroller.SlideController.image_manager')
@patch(u'PyQt5.QtCore.QTimer.singleShot')
def test_update_preview_live(mocked_singleShot, mocked_image_manager, registry):
def test_update_preview_live(mocked_singleShot, registry):
"""
Test that the preview screen is updated with a screen grab for live service items
"""
# GIVEN: A mocked live service item, a mocked image_manager, a mocked Registry,
# GIVEN: A mocked live service item, a mocked Registry,
# and a slide controller with many mocks.
# Mocked Live Item
mocked_live_item = MagicMock()
mocked_live_item.get_rendered_frame.return_value = ''
mocked_live_item.is_capable = MagicMock()
mocked_live_item.is_capable.side_effect = [True, True]
# Mock image_manager
mocked_image_manager.get_image.return_value = QtGui.QImage()
mocked_main_window = MagicMock()
Registry().register('main_window', mocked_main_window)
# Mock SlideController
@ -853,24 +850,20 @@ def test_update_preview_live(mocked_singleShot, mocked_image_manager, registry):
assert 0 == slide_controller.slide_preview.setPixmap.call_count, 'setPixmap should not be called'
assert 0 == slide_controller.display.preview.call_count, 'display.preview() should not be called'
assert 2 == mocked_singleShot.call_count, 'Timer to display_maindisplay should have been called 2 times'
assert 0 == mocked_image_manager.get_image.call_count, 'image_manager not be called'
@patch(u'openlp.core.ui.slidecontroller.SlideController.image_manager')
@patch(u'PyQt5.QtCore.QTimer.singleShot')
def test_update_preview_pres(mocked_singleShot, mocked_image_manager, registry):
def test_update_preview_pres(mocked_singleShot, registry):
"""
Test that the preview screen is updated with the correct preview for presentation service items
"""
# GIVEN: A mocked presentation service item, a mocked image_manager, a mocked Registry,
# GIVEN: A mocked presentation service item, a mocked Registry,
# and a slide controller with many mocks.
# Mocked Presentation Item
mocked_pres_item = MagicMock()
mocked_pres_item.get_rendered_frame.return_value = ''
mocked_pres_item.is_capable = MagicMock()
mocked_pres_item.is_capable.side_effect = [True, True]
# Mock image_manager
mocked_image_manager.get_image.return_value = QtGui.QImage()
mocked_main_window = MagicMock()
Registry().register('main_window', mocked_main_window)
# Mock SlideController
@ -891,26 +884,23 @@ def test_update_preview_pres(mocked_singleShot, mocked_image_manager, registry):
# WHEN: update_preview is called
slide_controller.update_preview()
# THEN: setPixmap and the image_manager should have been called
# THEN: setPixmap should have been called
assert 1 == slide_controller.preview_display.set_single_image.call_count, 'set_single_image should be called'
assert 0 == mocked_singleShot.call_count, 'Timer to display_maindisplay should not be called'
@patch(u'openlp.core.ui.slidecontroller.SlideController.image_manager')
@patch(u'PyQt5.QtCore.QTimer.singleShot')
def test_update_preview_media(mocked_singleShot, mocked_image_manager, registry):
def test_update_preview_media(mocked_singleShot, registry):
"""
Test that the preview screen is updated with the correct preview for media service items
"""
# GIVEN: A mocked media service item, a mocked image_manager, a mocked Registry,
# GIVEN: A mocked media service item, a mocked Registry,
# and a slide controller with many mocks.
# Mocked Media Item
mocked_media_item = MagicMock()
mocked_media_item.get_rendered_frame.return_value = ''
mocked_media_item.is_capable = MagicMock()
mocked_media_item.is_capable.side_effect = [True, False]
# Mock image_manager
mocked_image_manager.get_image.return_value = QtGui.QImage()
# Mock Registry
mocked_main_window = MagicMock()
Registry().register('main_window', mocked_main_window)
@ -935,24 +925,20 @@ def test_update_preview_media(mocked_singleShot, mocked_image_manager, registry)
# THEN: setPixmap should have been called
assert 1 == slide_controller.preview_display.set_single_image.call_count, 'set_single_image should be called'
assert 0 == mocked_singleShot.call_count, 'Timer to display_maindisplay should not be called'
assert 0 == mocked_image_manager.get_image.call_count, 'image_manager should not be called'
@patch(u'openlp.core.ui.slidecontroller.SlideController.image_manager')
@patch(u'PyQt5.QtCore.QTimer.singleShot')
def test_update_preview_image(mocked_singleShot, mocked_image_manager, registry):
def test_update_preview_image(mocked_singleShot, registry):
"""
Test that the preview screen is updated with the correct preview for image service items
"""
# GIVEN: A mocked image service item, a mocked image_manager, a mocked Registry,
# GIVEN: A mocked image service item, a mocked Registry,
# and a slide controller with many mocks.
# Mocked Image Item
mocked_img_item = MagicMock()
mocked_img_item.get_rendered_frame.return_value = ''
mocked_img_item.is_capable = MagicMock()
mocked_img_item.is_capable.side_effect = [False, True]
# Mock image_manager
mocked_image_manager.get_image.return_value = QtGui.QImage()
# Mock Registry
mocked_main_window = MagicMock()
Registry().register('main_window', mocked_main_window)
@ -976,7 +962,6 @@ def test_update_preview_image(mocked_singleShot, mocked_image_manager, registry)
# THEN: setPixmap and display.preview should have been called
assert 1 == slide_controller.preview_display.go_to_slide.call_count, 'go_to_slide should be called'
assert 0 == mocked_singleShot.call_count, 'Timer to display_maindisplay should not be called'
assert 0 == mocked_image_manager.get_image.call_count, 'image_manager should not be called'
@patch(u'openlp.core.ui.slidecontroller.image_to_byte')

View File

@ -26,7 +26,6 @@ import pytest
from types import GeneratorType
from unittest.mock import MagicMock, call, patch
from PyQt5 import QtGui
from openlp.core.common.i18n import UiStrings
from openlp.core.widgets.views import ListPreviewWidget, ListWidgetWithDnD, TreeWidgetWithDnD, handle_mime_data_urls
@ -119,10 +118,9 @@ def test_new_list_preview_widget(preview_widget_env, mock_settings):
assert list_preview_widget.screen_ratio == 1, 'Should not be called'
@patch(u'openlp.core.widgets.views.ListPreviewWidget.image_manager')
@patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents')
@patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight')
def test_replace_service_item_thumbs(mocked_setRowHeight, mocked_resizeRowsToContents, mocked_image_manager,
def test_replace_service_item_thumbs(mocked_setRowHeight, mocked_resizeRowsToContents,
preview_widget_env, mock_settings):
"""
Test that thubmails for different slides are loaded properly in replace_service_item.
@ -151,8 +149,6 @@ def test_replace_service_item_thumbs(mocked_setRowHeight, mocked_resizeRowsToCon
mocked_cmd_service_item.is_capable.return_value = True
mocked_cmd_service_item.get_frames.return_value = [{'title': None, 'path': 'FAIL', 'image': 'TEST3'},
{'title': None, 'path': 'FAIL', 'image': 'TEST4'}]
# Mock image_manager
mocked_image_manager.get_image.return_value = QtGui.QImage()
# init ListPreviewWidget and load service item
list_preview_widget = ListPreviewWidget(None, 1)
@ -160,11 +156,6 @@ def test_replace_service_item_thumbs(mocked_setRowHeight, mocked_resizeRowsToCon
# WHEN: replace_service_item is called
list_preview_widget.replace_service_item(mocked_img_service_item, 200, 0)
list_preview_widget.replace_service_item(mocked_cmd_service_item, 200, 0)
# THEN: The ImageManager should be called in the appriopriate manner for each service item.
# assert mocked_image_manager.get_image.call_count == 4, 'Should be called once for each slide'
# calls = [call('TEST1', ImageSource.ImagePlugin), call('TEST2', ImageSource.ImagePlugin),
# call('TEST3', ImageSource.CommandPlugins), call('TEST4', ImageSource.CommandPlugins)]
# mocked_image_manager.get_image.assert_has_calls(calls)
@patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents')

View File

@ -26,7 +26,6 @@ from unittest.mock import MagicMock, patch
from PyQt5 import QtCore, QtTest
from openlp.core.common.registry import Registry
from openlp.core.ui.settingsform import SettingsForm
@ -111,62 +110,3 @@ def test_register_multiple_functions(form):
# WHEN testing the processing stack
# THEN the processing stack should still have two items
assert len(form.processes) == 2, 'No new processes should have been added to the stack'
def test_register_image_manager_trigger_one(form, dummy):
"""
Test the triggering of the image manager rebuild event from image background change
"""
# GIVEN: Three functions registered to be call
Registry().register_function('images_config_updated', dummy[0])
Registry().register_function('config_screen_changed', dummy[1])
Registry().register_function('images_regenerate', dummy[2])
# WHEN: The Images have been changed and the form submitted
form.register_post_process('images_config_updated')
form.accept()
# THEN: images_regenerate should have been added.
assert dummy[0].call_count == 1, 'dummy1 should have been called once'
assert dummy[1].call_count == 0, 'dummy2 should not have been called at all'
assert dummy[2].call_count == 1, 'dummy3 should have been called once'
def test_register_image_manager_trigger_two(form, dummy):
"""
Test the triggering of the image manager rebuild event from screen dimension change
"""
# GIVEN: Three functions registered to be call
Registry().register_function('images_config_updated', dummy[0])
Registry().register_function('config_screen_changed', dummy[1])
Registry().register_function('images_regenerate', dummy[2])
# WHEN: The Images have been changed and the form submitted
form.register_post_process('config_screen_changed')
form.accept()
# THEN: images_regenerate should have been added.
assert dummy[0].call_count == 0, 'dummy1 should not have been called at all'
assert dummy[1].call_count == 1, 'dummy2 should have been called once'
assert dummy[2].call_count == 1, 'dummy3 should have been called once'
def test_register_image_manager_trigger_three(form, dummy):
"""
Test the triggering of the image manager rebuild event from image background change and a change to the
screen dimension.
"""
# GIVEN: Three functions registered to be call
Registry().register_function('images_config_updated', dummy[0])
Registry().register_function('config_screen_changed', dummy[1])
Registry().register_function('images_regenerate', dummy[2])
# WHEN: The Images have been changed and the form submitted
form.register_post_process('config_screen_changed')
form.register_post_process('images_config_updated')
form.accept()
# THEN: Images_regenerate should have been added.
assert dummy[0].call_count == 1, 'dummy1 should have been called once'
assert dummy[1].call_count == 1, 'dummy2 should have been called once'
assert dummy[2].call_count == 1, 'dummy3 should have been called once'