# -*- coding: utf-8 -*- # vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 ############################################################################### # OpenLP - Open Source Lyrics Projection # # --------------------------------------------------------------------------- # # Copyright (c) 2008-2014 Raoul Snyman # # Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # # Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # # Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # # 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, Martin Zibricky # # --------------------------------------------------------------------------- # # 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 import time import Queue from PyQt4 import QtCore from openlp.core.lib import resize_image, image_to_byte, Receiver from openlp.core.ui import ScreenList log = logging.getLogger(__name__) class ImageThread(QtCore.QThread): """ 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. """ def __init__(self, manager): QtCore.QThread.__init__(self, None) self.imageManager = manager def run(self): """ Run the thread. """ self.imageManager._process() 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 ``_resetImage`` method with the Image instance as argument. """ secondary_priority = 0 def __init__(self, path, source, background): """ Create an image for the :class:`ImageManager`'s cache. ``path`` The image's file path. This should be an existing file path. ``source`` The source describes the image's origin. Possible values are described in the :class:`~openlp.core.lib.ImageSource` class. ``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.path = path self.image = None self.image_bytes = None self.priority = Priority.Normal self.source = source self.background = background self.timestamp = 0 if os.path.exists(path): 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``. ``image`` The image to remove. This should be an :class:`Image` instance. ``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. ``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(u'Image Manager loaded') def __init__(self): QtCore.QObject.__init__(self) currentScreen = ScreenList().current self.width = currentScreen[u'size'].width() self.height = currentScreen[u'size'].height() self._cache = {} self.imageThread = ImageThread(self) self._conversionQueue = PriorityQueue() self.stopManager = False QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'config_updated'), self.processUpdates) def updateDisplay(self): """ Screen has changed size so rebuild the cache to new size. """ 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. for image in self._cache.values(): self._resetImage(image) def updateImagesBorder(self, source, background): """ Border has changed so update all the images affected. """ log.debug(u'updateImages') # Mark the images as dirty for a rebuild by setting the image and byte # stream to None. for image in self._cache.values(): if image.source == source: image.background = background self._resetImage(image) def updateImageBorder(self, path, source, background): """ 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 # stream to None. image = self._cache[(path, source)] if image.source == source: image.background = background self._resetImage(image) def _resetImage(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._conversionQueue.modify_priority(image, Priority.Normal) def processUpdates(self): """ Flush the queue to updated any data to update """ # We want only one thread. if not self.imageThread.isRunning(): self.imageThread.start() def getImage(self, path, source): """ Return the ``QImage`` from the cache. If not present wait for the background thread to process it. """ log.debug(u'getImage %s' % path) image = self._cache[(path, source)] 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 def getImageBytes(self, path, source): """ Returns the byte string for an image. If not present wait for the background thread to process it. """ log.debug(u'getImageBytes %s' % path) image = self._cache[(path, source)] 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') time.sleep(0.1) return image.image_bytes def addImage(self, path, source, background): """ Add image to cache if it is not already there. """ log.debug(u'addImage %s' % path) if not (path, source) in self._cache: image = Image(path, source, background) self._cache[(path, source)] = image self._conversionQueue.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 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._resetImage(image) # We want only one thread. if not self.imageThread.isRunning(): self.imageThread.start() def _process(self): """ Controls the processing called from a ``QtCore.QThread``. """ log.debug(u'_process - started') while not self._conversionQueue.empty() and not self.stopManager: self._processCache() log.debug(u'_process - ended') def _processCache(self): """ Actually does the work. """ log.debug(u'_processCache') image = self._conversionQueue.get()[2] # Generate the QImage for the image. if image.image is None: image.image = resize_image(image.path, self.width, self.height, image.background) # 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) 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)