← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~googol-hush/openlp/thumb-creation into lp:openlp

 

Andreas Preikschat has proposed merging lp:~googol-hush/openlp/thumb-creation into lp:openlp with lp:~trb143/openlp/b1 as a prerequisite.

Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~googol-hush/openlp/thumb-creation/+merge/65515

Hello,

1) I moved the create_thumb and validate_thumb functions to core/lib/init, thus to be able to use them (from) everywhere.

2) I changed the image manager in two ways:
- the resize_image is now done in the thread. Now you can add images to the service manager without delay and the thread resizes and creates the bytes streams in the background)
- implemented an 'intelligent' image queue to possible reduce the possibility that we are generating data (e. g. the byte stream) when it is not needed.
-- 
https://code.launchpad.net/~googol-hush/openlp/thumb-creation/+merge/65515
Your team OpenLP Core is requested to review the proposed merge of lp:~googol-hush/openlp/thumb-creation into lp:openlp.
=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py	2011-06-12 16:02:52 +0000
+++ openlp/core/lib/__init__.py	2011-06-22 15:13:52 +0000
@@ -137,6 +137,58 @@
     # convert to base64 encoding so does not get missed!
     return byte_array.toBase64()
 
+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.
+
+    ``image_path``
+        The image file to create the icon from.
+
+    ``thumb_path``
+        The filename to save the thumbnail to.
+
+    ``return_icon``
+        States if an icon should be build and returned from the thumb. Defaults
+        to ``True``.
+
+    ``size``
+        Defaults to ``None``.
+    """
+    ext = os.path.splitext(thumb_path)[1].lower()
+    reader = QtGui.QImageReader(image_path)
+    if size is None:
+        ratio = float(reader.size().width()) / float(reader.size().height())
+        reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88))
+    else:
+        reader.setScaledSize(size)
+    thumb = reader.read()
+    thumb.save(thumb_path, ext[1:])
+    if not return_icon:
+        return
+    if os.path.exists(thumb_path):
+        return build_icon(unicode(thumb_path))
+    # Fallback for files with animation support.
+    return build_icon(unicode(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.
+
+    ``file_path``
+        The path to the file. The file **must** exist!
+
+    ``thumb_path``
+        The path to the thumb.
+    """
+    if not os.path.exists(unicode(thumb_path)):
+        return False
+    image_date = os.stat(unicode(file_path)).st_mtime
+    thumb_date = os.stat(unicode(thumb_path)).st_mtime
+    return image_date <= thumb_date
+
 def resize_image(image_path, width, height, background=QtCore.Qt.black):
     """
     Resize an image to fit on the current screen.
@@ -151,7 +203,7 @@
         The new image height.
 
     ``background``
-        The background colour defaults to black.
+        The background colour. Defaults to ``QtCore.Qt.black``.
     """
     log.debug(u'resize_image - start')
     reader = QtGui.QImageReader(image_path)

=== modified file 'openlp/core/lib/imagemanager.py'
--- openlp/core/lib/imagemanager.py	2011-06-12 16:02:52 +0000
+++ openlp/core/lib/imagemanager.py	2011-06-22 15:13:52 +0000
@@ -32,6 +32,7 @@
 """
 import logging
 import time
+import Queue
 
 from PyQt4 import QtCore
 
@@ -53,15 +54,59 @@
         """
         Run the thread.
         """
-        self.imageManager.process()
+        self.imageManager._process()
+
+
+class Priority(object):
+    """
+    Enumeration class for different priorities.
+
+    ``Low``
+        Only the image's byte stream has to be generated. Neither the QImage nor
+        the byte stream has been requested yet.
+
+    ``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.
+
+    ``Urgent``
+        The image's byte stream as well as the image has to be generated. The
+        byte stream for this image has been requested.
+    """
+    Low = 3
+    Normal = 2
+    High = 1
+    Urgent = 0
 
 
 class Image(object):
-    name = ''
-    path = ''
-    dirty = True
-    image = None
-    image_bytes = None
+    def __init__(self, name='', path=''):
+        self.name = name
+        self.path = path
+        self.image = None
+        self.image_bytes = None
+        self.priority = Priority.Normal
+
+
+class PriorityQueue(Queue.PriorityQueue):
+    """
+    Customised ``Queue.PriorityQueue``.
+    """
+    def remove(self, item):
+        """
+        Removes the given ``item`` from the queue.
+
+        ``item``
+            The item to remove. This should be a tuple::
+
+                ``(Priority, Image)``
+        """
+        if item in self.queue:
+            self.queue.remove(item)
 
 
 class ImageManager(QtCore.QObject):
@@ -76,46 +121,63 @@
         self.width = current_screen[u'size'].width()
         self.height = current_screen[u'size'].height()
         self._cache = {}
-        self._thread_running = False
         self._cache_dirty = False
-        self.image_thread = ImageThread(self)
+        self._image_thread = ImageThread(self)
+        self._clean_queue = PriorityQueue()
 
     def update_display(self):
         """
-        Screen has changed size so rebuild the cache to new size
+        Screen has changed size so rebuild the cache to new size.
         """
         log.debug(u'update_display')
         current_screen = ScreenList.get_instance().current
         self.width = current_screen[u'size'].width()
         self.height = current_screen[u'size'].height()
-        # mark the images as dirty for a rebuild
+        # Mark the images as dirty for a rebuild.
+        self._clean_queue = PriorityQueue()
         for key in self._cache.keys():
             image = self._cache[key]
-            image.dirty = True
-            image.image = resize_image(image.path, self.width, self.height)
+            image.priority = Priority.Normal
+            image.image = None
+            image.image_bytes = None
+            self._clean_queue.put((image.priority, image))
         self._cache_dirty = True
-        # only one thread please
-        if not self._thread_running:
-            self.image_thread.start()
+        # We want only one thread.
+        if not self._image_thread.isRunning():
+            self._image_thread.start()
 
     def get_image(self, name):
         """
-        Return the Qimage from the cache
+        Return the Qimage from the cache.
         """
+        print u'get_image:', name
         log.debug(u'get_image %s' % name)
-        return self._cache[name].image
+        image = self._cache[name]
+        if image.image is None:
+            self._clean_queue.remove((image.priority, image))
+            image.priority = Priority.High
+            self._clean_queue.put((image.priority, image))
+            while image.image is None:
+                log.debug(u'get_image - waiting')
+                time.sleep(0.1)
+        return image.image
 
     def get_image_bytes(self, name):
         """
-        Returns the byte string for an image
-        If not present wait for the background thread to process it.
+        Returns the byte string for an image. If not present wait for the
+        background thread to process it.
         """
+        print u'get_image_bytes:', name
         log.debug(u'get_image_bytes %s' % name)
-        if not self._cache[name].image_bytes:
-            while self._cache[name].dirty:
+        image = self._cache[name]
+        if image.image_bytes is None:
+            self._clean_queue.remove((image.priority, image))
+            image.priority = Priority.Urgent
+            self._clean_queue.put((image.priority, image))
+            while image.image_bytes is None:
                 log.debug(u'get_image_bytes - waiting')
                 time.sleep(0.1)
-        return self._cache[name].image_bytes
+        return image.image_bytes
 
     def del_image(self, name):
         """
@@ -131,41 +193,45 @@
         """
         log.debug(u'add_image %s:%s' % (name, path))
         if not name in self._cache:
-            image = Image()
-            image.name = name
-            image.path = path
-            image.image = resize_image(path, self.width, self.height)
+            image = Image(name, path)
             self._cache[name] = image
+            self._clean_queue.put((image.priority, image))
         else:
             log.debug(u'Image in cache %s:%s' % (name, path))
         self._cache_dirty = True
-        # only one thread please
-        if not self._thread_running:
-            self.image_thread.start()
+        # We want only one thread.
+        if not self._image_thread.isRunning():
+            self._image_thread.start()
 
-    def process(self):
-        """
-        Controls the processing called from a QThread
-        """
-        log.debug(u'process - started')
-        self._thread_running = True
-        self.clean_cache()
-        # data loaded since we started ?
+    def _process(self):
+        """
+        Controls the processing called from a ``QtCore.QThread``.
+        """
+        log.debug(u'_process - started')
         while self._cache_dirty:
-            log.debug(u'process - recycle')
-            self.clean_cache()
-        self._thread_running = False
-        log.debug(u'process - ended')
+            log.debug(u'_process - recycle')
+            self._clean_cache()
+        log.debug(u'_process - ended')
 
-    def clean_cache(self):
+    def _clean_cache(self):
         """
         Actually does the work.
         """
-        log.debug(u'clean_cache')
-        # we will clean the cache now
-        self._cache_dirty = False
-        for key in self._cache.keys():
-            image = self._cache[key]
-            if image.dirty:
-                image.image_bytes = image_to_byte(image.image)
-                image.dirty = False
+        log.debug(u'_clean_cache')
+        if self._clean_queue.empty():
+            print u'empty'
+            self._cache_dirty = False
+            return
+        image = self._clean_queue.get()[1]
+        if image.image is None:
+            print u'processing (image):', image.name, image.priority
+            image.image = resize_image(image.path, self.width, self.height)
+            if image.priority != Priority.Urgent:
+                self._clean_queue.task_done()
+                image.priority = Priority.Low
+                self._clean_queue.put((image.priority, image))
+                return
+        if image.image_bytes is None:
+            print u'processing (bytes):', image.name, image.priority
+            image.image_bytes = image_to_byte(image.image)
+            self._clean_queue.task_done()

=== modified file 'openlp/core/lib/mediamanageritem.py'
--- openlp/core/lib/mediamanageritem.py	2011-06-12 16:02:52 +0000
+++ openlp/core/lib/mediamanageritem.py	2011-06-22 15:13:52 +0000
@@ -374,44 +374,6 @@
             count += 1
         return filelist
 
-    def validate(self, image, thumb):
-        """
-        Validates whether an image still exists and, if it does, is the
-        thumbnail representation of the image up to date.
-        """
-        if not os.path.exists(unicode(image)):
-            return False
-        if os.path.exists(thumb):
-            imageDate = os.stat(unicode(image)).st_mtime
-            thumbDate = os.stat(unicode(thumb)).st_mtime
-            # If image has been updated rebuild icon
-            if imageDate > thumbDate:
-                self.iconFromFile(image, thumb)
-        else:
-            self.iconFromFile(image, thumb)
-        return True
-
-    def iconFromFile(self, image_path, thumb_path):
-        """
-        Create a thumbnail icon from a given image.
-
-        ``image_path``
-            The image file to create the icon from.
-
-        ``thumb_path``
-            The filename to save the thumbnail to.
-        """
-        ext = os.path.splitext(thumb_path)[1].lower()
-        reader = QtGui.QImageReader(image_path)
-        ratio = float(reader.size().width()) / float(reader.size().height())
-        reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88))
-        thumb = reader.read()
-        thumb.save(thumb_path, ext[1:])
-        if os.path.exists(thumb_path):
-            return build_icon(unicode(thumb_path))
-        # Fallback for files with animation support.
-        return build_icon(unicode(image_path))
-
     def loadList(self, list):
         raise NotImplementedError(u'MediaManagerItem.loadList needs to be '
             u'defined by the plugin')

=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py	2011-06-16 22:03:59 +0000
+++ openlp/core/ui/slidecontroller.py	2011-06-22 15:13:52 +0000
@@ -582,6 +582,9 @@
         Loads a ServiceItem into the system from ServiceManager
         Display the slide number passed
         """
+        import time
+        import datetime
+        start = time.time()
         log.debug(u'processManagerItem live = %s' % self.isLive)
         self.onStopLoop()
         old_item = self.serviceItem
@@ -625,14 +628,14 @@
                 label.setMargin(4)
                 label.setScaledContents(True)
                 if self.serviceItem.is_command():
-                    image = QtGui.QImage(frame[u'image'])
+                    label.setPixmap(QtGui.QPixmap(frame[u'image']))
                 else:
                     # If current slide set background to image
                     if framenumber == slideno:
                         self.serviceItem.bg_image_bytes = \
                             self.imageManager.get_image_bytes(frame[u'title'])
                     image = self.imageManager.get_image(frame[u'title'])
-                label.setPixmap(QtGui.QPixmap.fromImage(image))
+                    label.setPixmap(QtGui.QPixmap.fromImage(image))
                 self.previewListWidget.setCellWidget(framenumber, 0, label)
                 slideHeight = width * self.parent().renderer.screen_ratio
                 row += 1
@@ -668,6 +671,7 @@
                 self.onMediaClose()
         Receiver.send_message(u'slidecontroller_%s_started' % self.typePrefix,
             [serviceItem])
+        print unicode(datetime.timedelta(seconds=time.time() - start))
 
     def __updatePreviewSelection(self, slideno):
         """

=== modified file 'openlp/core/ui/thememanager.py'
--- openlp/core/ui/thememanager.py	2011-06-12 16:02:52 +0000
+++ openlp/core/ui/thememanager.py	2011-06-22 15:13:52 +0000
@@ -36,7 +36,7 @@
 
 from openlp.core.lib import OpenLPToolbar, get_text_file_string, build_icon, \
     Receiver, SettingsManager, translate, check_item_selected, \
-    check_directory_exists
+    check_directory_exists, create_thumb, validate_thumb
 from openlp.core.lib.theme import ThemeXML, BackgroundType, VerticalType, \
     BackgroundGradientType
 from openlp.core.lib.ui import UiStrings, critical_error_message_box
@@ -364,7 +364,7 @@
             The theme to delete.
         """
         self.themelist.remove(theme)
-        thumb = theme + u'.png'
+        thumb = u'%s.png' % theme
         delete_file(os.path.join(self.path, thumb))
         delete_file(os.path.join(self.thumbPath, thumb))
         try:
@@ -478,15 +478,12 @@
                     name = textName
                 thumb = os.path.join(self.thumbPath, u'%s.png' % textName)
                 item_name = QtGui.QListWidgetItem(name)
-                if os.path.exists(thumb):
+                if validate_thumb(theme, thumb):
                     icon = build_icon(thumb)
                 else:
-                    icon = build_icon(theme)
-                    pixmap = icon.pixmap(QtCore.QSize(88, 50))
-                    pixmap.save(thumb, u'png')
+                    icon = create_thumb(theme, thumb)
                 item_name.setIcon(icon)
-                item_name.setData(QtCore.Qt.UserRole,
-                    QtCore.QVariant(textName))
+                item_name.setData(QtCore.Qt.UserRole, QtCore.QVariant(textName))
                 self.themeListWidget.addItem(item_name)
                 self.themelist.append(textName)
         self._pushThemes()
@@ -658,9 +655,7 @@
             os.unlink(samplepathname)
         frame.save(samplepathname, u'png')
         thumb = os.path.join(self.thumbPath, u'%s.png' % name)
-        icon = build_icon(frame)
-        pixmap = icon.pixmap(QtCore.QSize(88, 50))
-        pixmap.save(thumb, u'png')
+        create_thumb(samplepathname, thumb, False)
         log.debug(u'Theme image written to %s', samplepathname)
 
     def updatePreviewImages(self):

=== modified file 'openlp/plugins/images/lib/mediaitem.py'
--- openlp/plugins/images/lib/mediaitem.py	2011-06-12 17:56:11 +0000
+++ openlp/plugins/images/lib/mediaitem.py	2011-06-22 15:13:52 +0000
@@ -33,7 +33,7 @@
 
 from openlp.core.lib import MediaManagerItem, build_icon, ItemCapabilities, \
     SettingsManager, translate, check_item_selected, check_directory_exists, \
-    Receiver
+    Receiver, create_thumb, validate_thumb
 from openlp.core.lib.ui import UiStrings, critical_error_message_box
 from openlp.core.utils import AppLocation, delete_file, get_images_filter
 
@@ -122,13 +122,13 @@
                 self.plugin.formparent.incrementProgressBar()
             filename = os.path.split(unicode(imageFile))[1]
             thumb = os.path.join(self.servicePath, filename)
-            if os.path.exists(thumb):
-                if self.validate(imageFile, thumb):
+            if not os.path.exists(imageFile):
+                icon = build_icon(u':/general/general_delete.png')
+            else:
+                if validate_thumb(imageFile, thumb):
                     icon = build_icon(thumb)
                 else:
-                    icon = build_icon(u':/general/general_delete.png')
-            else:
-                icon = self.iconFromFile(imageFile, thumb)
+                    icon = create_thumb(imageFile, thumb)
             item_name = QtGui.QListWidgetItem(filename)
             item_name.setIcon(icon)
             item_name.setData(QtCore.Qt.UserRole, QtCore.QVariant(imageFile))

=== modified file 'openlp/plugins/presentations/lib/mediaitem.py'
--- openlp/plugins/presentations/lib/mediaitem.py	2011-06-12 17:56:11 +0000
+++ openlp/plugins/presentations/lib/mediaitem.py	2011-06-22 15:13:52 +0000
@@ -32,7 +32,8 @@
 from PyQt4 import QtCore, QtGui
 
 from openlp.core.lib import MediaManagerItem, build_icon, SettingsManager, \
-    translate, check_item_selected, Receiver, ItemCapabilities
+    translate, check_item_selected, Receiver, ItemCapabilities, create_thumb, \
+    validate_thumb
 from openlp.core.lib.ui import UiStrings, critical_error_message_box, \
     media_item_combo_box
 from openlp.plugins.presentations.lib import MessageListener
@@ -190,10 +191,13 @@
                     doc.load_presentation()
                     preview = doc.get_thumbnail_path(1, True)
                 doc.close_presentation()
-                if preview and self.validate(preview, thumb):
-                    icon = build_icon(thumb)
+                if not (preview and os.path.exists(preview)):
+                    icon = build_icon(u':/general/general_delete.png')
                 else:
-                    icon = build_icon(u':/general/general_delete.png')
+                    if validate_thumb(preview, thumb):
+                        icon = build_icon(thumb)
+                    else:
+                        icon = create_thumb(preview, thumb)
             else:
                 if initialLoad:
                     icon = build_icon(u':/general/general_delete.png')

=== modified file 'openlp/plugins/presentations/lib/presentationcontroller.py'
--- openlp/plugins/presentations/lib/presentationcontroller.py	2011-06-12 16:02:52 +0000
+++ openlp/plugins/presentations/lib/presentationcontroller.py	2011-06-22 15:13:52 +0000
@@ -31,7 +31,7 @@
 
 from PyQt4 import QtCore
 
-from openlp.core.lib import Receiver, resize_image
+from openlp.core.lib import Receiver, create_thumb, resize_image, validate_thumb
 from openlp.core.utils import AppLocation
 
 log = logging.getLogger(__name__)
@@ -145,15 +145,13 @@
 
     def check_thumbnails(self):
         """
-        Returns true if the thumbnail images look to exist and are more
-        recent than the powerpoint
+        Returns ``True`` if the thumbnail images exist and are more recent than
+        the powerpoint file.
         """
         lastimage = self.get_thumbnail_path(self.get_slide_count(), True)
         if not (lastimage and os.path.isfile(lastimage)):
             return False
-        imgdate = os.stat(lastimage).st_mtime
-        pptdate = os.stat(self.filepath).st_mtime
-        return imgdate >= pptdate
+        return validate_thumb(self.filepath, lastimage)
 
     def close_presentation(self):
         """
@@ -246,8 +244,8 @@
         if self.check_thumbnails():
             return
         if os.path.isfile(file):
-            img = resize_image(file, 320, 240)
-            img.save(self.get_thumbnail_path(idx, False))
+            thumb_path = self.get_thumbnail_path(idx, False)
+            create_thumb(file, thumb_path, False, QtCore.QSize(320, 240))
 
     def get_thumbnail_path(self, slide_no, check_exists):
         """


Follow ups