openlp/openlp/core/lib/imagemanager.py

346 lines
13 KiB
Python
Raw Normal View History

2010-10-22 18:56:58 +00:00
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
2011-12-27 10:33:55 +00:00
# Copyright (c) 2008-2012 Raoul Snyman #
# Portions copyright (c) 2008-2012 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
# Meinert Jordan, Armin Köhler, Edwin Lunando, Joshua Miller, Stevan Pettit, #
# Andreas Preikschat, Mattias Põldaru, Christian Richter, Philip Ridout, #
# Simon Scudder, Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon #
# Tibble, Dave Warnock, Frode Woldsund #
2010-10-22 18:56:58 +00:00
# --------------------------------------------------------------------------- #
# 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 #
###############################################################################
"""
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
2010-10-23 07:23:49 +00:00
import time
import Queue
2010-10-22 18:56:58 +00:00
2010-10-23 17:37:10 +00:00
from PyQt4 import QtCore
2010-10-22 18:56:58 +00:00
2011-08-20 11:45:06 +00:00
from openlp.core.lib import resize_image, image_to_byte, Receiver
2011-05-05 11:51:47 +00:00
from openlp.core.ui import ScreenList
2010-10-22 18:56:58 +00:00
log = logging.getLogger(__name__)
class ImageThread(QtCore.QThread):
"""
2011-06-25 05:34:07 +00:00
A special Qt thread class to speed up the display of images. This is
threaded so it loads the frames and generates byte stream in background.
2010-10-22 18:56:58 +00:00
"""
def __init__(self, manager):
QtCore.QThread.__init__(self, None)
2010-12-02 14:37:38 +00:00
self.imageManager = manager
2010-10-22 18:56:58 +00:00
def run(self):
"""
Run the thread.
"""
self.imageManager._process()
class Priority(object):
"""
Enumeration class for different priorities.
2011-06-25 05:34:07 +00:00
``Lowest``
Only the image's byte stream has to be generated. But neither the
``QImage`` nor the byte stream has been requested yet.
``Low``
2011-06-25 05:34:07 +00:00
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
2011-06-25 06:20:02 +00:00
whose ``QImage`` were not generated due to a request.
``Normal``
The image's byte stream as well as the image has to be generated.
2011-06-25 05:34:07 +00:00
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
2011-06-25 05:34:07 +00:00
``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.
2011-06-25 05:34:07 +00:00
**Note**, this priority is only set when the byte stream has not been
generated yet.
"""
2011-06-25 05:34:07 +00:00
Lowest = 4
Low = 3
Normal = 2
High = 1
Urgent = 0
2010-10-22 18:56:58 +00:00
class Image(object):
2011-06-25 05:34:07 +00:00
"""
This class represents an image. To mark an image as *dirty* call the
:class:`ImageManager`'s ``_resetImage`` method with the Image instance as
argument.
2011-06-25 05:34:07 +00:00
"""
2012-05-05 18:34:26 +00:00
secondary_priority = 0
2011-08-20 11:45:06 +00:00
def __init__(self, name, path, source, background):
"""
Create an image for the :class:`ImageManager`'s cache.
``name``
The image name. This does not have to be part of the ``path``. It
can be of any value. It can be considered an ID.
``path``
The image's file path. This should be an existing file path.
``source``
The source describes the image's origin. Possible values are
``image`` and ``theme``.
``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.
"""
self.name = name
self.path = path
self.image = None
self.image_bytes = None
2011-08-20 11:45:06 +00:00
self.source = source
self.background = background
self.timestamp = os.stat(path).st_mtime
self.priority = Priority.Normal
2012-05-05 18:34:26 +00:00
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
2012-05-05 18:34:26 +00:00
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::
2012-05-05 18:34:26 +00:00
(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.
"""
2011-07-07 14:53:05 +00:00
def modify_priority(self, image, new_priority):
"""
2011-07-07 14:53:05 +00:00
Modifies the priority of the given ``image``.
2011-07-07 14:53:05 +00:00
``image``
2012-05-05 12:40:31 +00:00
The image to remove. This should be an :class:`Image` instance.
2011-07-07 14:53:05 +00:00
``new_priority``
2012-05-05 12:40:31 +00:00
The image's new priority. See the :class:`Priority` class for
priorities.
"""
2011-07-07 14:53:05 +00:00
self.remove(image)
image.priority = new_priority
2012-05-05 18:34:26 +00:00
self.put((image.priority, image.secondary_priority, image))
2011-07-07 14:53:05 +00:00
def remove(self, image):
"""
Removes the given ``image`` from the queue.
``image``
The image to remove. This should be an ``Image`` instance.
"""
2012-05-05 18:34:26 +00:00
if (image.priority, image.secondary_priority, image) in self.queue:
self.queue.remove((image.priority, image.secondary_priority, image))
2010-10-22 18:56:58 +00:00
2011-02-07 15:55:02 +00:00
2010-10-22 18:56:58 +00:00
class ImageManager(QtCore.QObject):
"""
Image Manager handles the conversion and sizing of images.
"""
log.info(u'Image Manager loaded')
2011-05-05 11:51:47 +00:00
def __init__(self):
2011-02-20 00:05:50 +00:00
QtCore.QObject.__init__(self)
currentScreen = ScreenList().current
self.width = currentScreen[u'size'].width()
self.height = currentScreen[u'size'].height()
2010-10-22 18:56:58 +00:00
self._cache = {}
2012-05-26 17:51:27 +00:00
self.imageThread = ImageThread(self)
self._conversionQueue = PriorityQueue()
self.stopManager = False
2011-08-20 11:45:06 +00:00
QtCore.QObject.connect(Receiver.get_receiver(),
QtCore.SIGNAL(u'config_updated'), self.processUpdates)
2010-10-22 18:56:58 +00:00
def updateDisplay(self):
2010-10-22 18:56:58 +00:00
"""
Screen has changed size so rebuild the cache to new size.
2010-10-22 18:56:58 +00:00
"""
log.debug(u'updateDisplay')
currentScreen = ScreenList().current
self.width = currentScreen[u'size'].width()
self.height = currentScreen[u'size'].height()
# Mark the images as dirty for a rebuild by setting the image and byte
# stream to None.
2012-03-12 22:12:16 +00:00
for image in self._cache.values():
self._resetImage(image)
2011-08-20 11:45:06 +00:00
def updateImagesBorder(self, source, background):
2011-08-20 13:21:25 +00:00
"""
2011-08-21 05:33:07 +00:00
Border has changed so update all the images affected.
2011-08-20 13:21:25 +00:00
"""
log.debug(u'updateImages')
2011-08-20 13:21:25 +00:00
# Mark the images as dirty for a rebuild by setting the image and byte
# stream to None.
2012-03-12 22:12:16 +00:00
for image in self._cache.values():
if image.source == source:
2011-08-20 13:21:25 +00:00
image.background = background
self._resetImage(image)
2011-08-21 05:33:07 +00:00
def updateImageBorder(self, name, source, background):
2011-08-21 05:33:07 +00:00
"""
Border has changed so update the image affected.
"""
log.debug(u'updateImage')
# Mark the image as dirty for a rebuild by setting the image and byte
2011-08-21 05:33:07 +00:00
# stream to None.
image = self._cache[name]
if image.source == source:
image.background = background
self._resetImage(image)
2011-08-21 16:13:45 +00:00
def _resetImage(self, image):
"""
Mark the given :class:`Image` instance as dirt by setting its ``image``
and ``image_bytes`` attributes to None.
"""
2011-08-21 16:13:45 +00:00
image.image = None
image.image_bytes = None
self._conversionQueue.modify_priority(image, Priority.Normal)
2011-08-20 13:21:25 +00:00
def processUpdates(self):
2011-08-20 11:45:06 +00:00
"""
Flush the queue to updated any data to update
"""
# We want only one thread.
2012-05-26 17:51:27 +00:00
if not self.imageThread.isRunning():
self.imageThread.start()
2010-10-22 18:56:58 +00:00
def getImage(self, name):
2010-10-23 07:23:49 +00:00
"""
Return the ``QImage`` from the cache. If not present wait for the
background thread to process it.
2010-10-23 07:23:49 +00:00
"""
log.debug(u'getImage %s' % name)
image = self._cache[name]
if image.image is None:
self._conversionQueue.modify_priority(image, Priority.High)
# make sure we are running and if not give it a kick
self.processUpdates()
while image.image is None:
log.debug(u'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._conversionQueue.modify_priority(image, Priority.Low)
return image.image
2010-10-22 18:56:58 +00:00
def getImageBytes(self, name):
2010-10-23 07:23:49 +00:00
"""
Returns the byte string for an image. If not present wait for the
background thread to process it.
2010-10-23 07:23:49 +00:00
"""
log.debug(u'getImageBytes %s' % name)
image = self._cache[name]
if image.image_bytes is None:
self._conversionQueue.modify_priority(image, Priority.Urgent)
# make sure we are running and if not give it a kick
self.processUpdates()
while image.image_bytes is None:
log.debug(u'getImageBytes - waiting')
2010-10-23 07:23:49 +00:00
time.sleep(0.1)
return image.image_bytes
2010-10-22 18:56:58 +00:00
def addImage(self, name, path, source, background):
2010-10-22 18:56:58 +00:00
"""
Add image to cache if it is not already there.
2010-10-22 18:56:58 +00:00
"""
log.debug(u'addImage %s:%s' % (name, path))
2010-10-22 18:56:58 +00:00
if not name in self._cache:
2011-08-20 11:45:06 +00:00
image = Image(name, path, source, background)
2010-10-22 18:56:58 +00:00
self._cache[name] = image
self._conversionQueue.put(
2012-05-05 18:34:26 +00:00
(image.priority, image.secondary_priority, image))
else:
image = self._cache[name]
if os.path.isfile(path) and \
image.timestamp != os.stat(path).st_mtime:
image.path = path
image.timestamp = os.stat(path).st_mtime
self._resetImage(image)
log.debug(u'Image in cache %s:%s' % (name, path))
# We want only one thread.
2012-05-26 17:51:27 +00:00
if not self.imageThread.isRunning():
self.imageThread.start()
2010-10-22 18:56:58 +00:00
def _process(self):
2010-10-22 18:56:58 +00:00
"""
Controls the processing called from a ``QtCore.QThread``.
2010-10-22 18:56:58 +00:00
"""
log.debug(u'_process - started')
while not self._conversionQueue.empty() and not self.stopManager:
self._processCache()
log.debug(u'_process - ended')
2010-10-22 18:56:58 +00:00
def _processCache(self):
2010-10-22 18:56:58 +00:00
"""
Actually does the work.
"""
log.debug(u'_processCache')
image = self._conversionQueue.get()[2]
# Generate the QImage for the image.
if image.image is None:
2011-08-20 11:45:06 +00:00
image.image = resize_image(image.path, self.width, self.height,
image.background)
2011-06-25 05:34:07 +00:00
# Set the priority to Lowest and stop here as we need to process
# more important images first.
if image.priority == Priority.Normal:
self._conversionQueue.modify_priority(image, Priority.Lowest)
2011-06-25 05:34:07 +00:00
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._conversionQueue.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)