← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~trb143/openlp/images into lp:openlp

 

Tim Bentley has proposed merging lp:~trb143/openlp/images into lp:openlp.

Requested reviews:
  OpenLP Core (openlp-core)


Changes to Image processing to improve performance.
Moved image storage for Themes and Image plugins to central cache.
Moved conversion to byte array on to a thread
Built basic thread locking management to cover adding and processing at same time

Presentations and Videos work how that always did.

Cleaned up lots of image size and type conversions.

  
-- 
https://code.launchpad.net/~trb143/openlp/images/+merge/39218
Your team OpenLP Core is requested to review the proposed merge of lp:~trb143/openlp/images into lp:openlp.
=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py	2010-10-15 15:37:29 +0000
+++ openlp/core/lib/__init__.py	2010-10-24 06:46:49 +0000
@@ -314,6 +314,7 @@
 
 from spelltextedit import SpellTextEdit
 from eventreceiver import Receiver
+from imagemanager import ImageManager
 from settingsmanager import SettingsManager
 from plugin import PluginStatus, StringContent, Plugin
 from pluginmanager import PluginManager

=== modified file 'openlp/core/lib/htmlbuilder.py'
--- openlp/core/lib/htmlbuilder.py	2010-10-07 21:49:25 +0000
+++ openlp/core/lib/htmlbuilder.py	2010-10-24 06:46:49 +0000
@@ -90,16 +90,16 @@
     var transition = %s;
 
     function show_video(state, path, volume, loop){
-        // Note, the preferred method for looping would be to use the 
+        // Note, the preferred method for looping would be to use the
         // video tag loop attribute.
         // But QtWebKit doesn't support this. Neither does it support the
         // onended event, hence the setInterval()
         // In addition, setting the currentTime attribute to zero to restart
         // the video raises an INDEX_SIZE_ERROR: DOM Exception 1
-        // To complicate it further, sometimes vid.currentTime stops 
+        // To complicate it further, sometimes vid.currentTime stops
         // slightly short of vid.duration and vid.ended is intermittent!
         //
-        // Note, currently the background may go black between loops. Not 
+        // Note, currently the background may go black between loops. Not
         // desirable. Need to investigate using two <video>'s, and hiding/
         // preloading one, and toggle between the two when looping.
 
@@ -132,8 +132,8 @@
                 vid.style.visibility = 'visible';
                 if(vid.looping){
                     video_timer = setInterval(
-                        function() { 
-                            show_video('poll'); 
+                        function() {
+                            show_video('poll');
                         }, 200);
                 }
                 break;
@@ -328,6 +328,7 @@
     height = screen[u'size'].height()
     theme = item.themedata
     webkitvers = webkit_version()
+    # Image generated and poked in
     if item.bg_image_bytes:
         image = u'src="data:image/png;base64,%s"' % item.bg_image_bytes
     else:
@@ -455,7 +456,7 @@
             if theme.display_outline and webkitvers < 534.3:
                 shadow = u'padding-left: %spx; padding-top: %spx;' % \
                     (int(theme.display_shadow_size) +
-                    (int(theme.display_outline_size) * 2), 
+                    (int(theme.display_outline_size) * 2),
                     theme.display_shadow_size)
                 shadow += build_lyrics_outline_css(theme, True)
             else:

=== added file 'openlp/core/lib/imagemanager.py'
--- openlp/core/lib/imagemanager.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/imagemanager.py	2010-10-24 06:46:49 +0000
@@ -0,0 +1,158 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2010 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2010 Tim Bentley, Jonathan Corwin, Michael      #
+# Gorven, Scott Guerrieri, Meinert Jordan, Andreas Preikschat, Christian      #
+# Richter, Philip Ridout, Maikel Stuivenberg, Martin Thompson, Jon Tibble,    #
+# Carsten Tinggaard, Frode Woldsund                                           #
+# --------------------------------------------------------------------------- #
+# 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
+
+from PyQt4 import QtCore
+
+from openlp.core.lib import resize_image, image_to_byte
+
+log = logging.getLogger(__name__)
+
+class ImageThread(QtCore.QThread):
+    """
+    A special Qt thread class to speed up the display of text based frames.
+    This is threaded so it loads the frames in background
+    """
+    def __init__(self, manager):
+        QtCore.QThread.__init__(self, None)
+        self.image_mamager = manager
+
+    def run(self):
+        """
+        Run the thread.
+        """
+        self.image_mamager.process()
+
+
+class Image(object):
+    name = ''
+    path = ''
+    dirty = True
+    image = None
+    image_bytes = None
+
+class ImageManager(QtCore.QObject):
+    """
+    Image Manager handles the conversion and sizing of images.
+
+    """
+    log.info(u'Image Manager loaded')
+
+    def __init__(self):
+        self._cache = {}
+        self._thread_running = False
+        self._cache_dirty = False
+        self.image_thread = ImageThread(self)
+
+    def update_display(self, width, height):
+        """
+        Screen has changed size so rebuild the cache to new size
+        """
+        log.debug(u'update_display')
+        self.width = width
+        self.height = height
+        # mark the images as dirty for a rebuild
+        for key in self._cache.keys():
+            image = self._cache[key]
+            image.dirty = True
+            fullpath = os.path.join(image.path, image.name)
+            image.image = resize_image(fullpath,
+                self.width, self.height)
+        self._cache_dirty = True
+        # only one thread please
+        if not self._thread_running:
+            self.image_thread.start()
+
+    def get_image(self, name):
+        """
+        Return the Qimage from the cache
+        """
+        log.debug(u'get_image %s' % name)
+        return self._cache[name].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.
+        """
+        log.debug(u'get_image_bytes %s' % name)
+        if not self._cache[name].image_bytes:
+            while self._cache[name].dirty:
+                log.debug(u'get_image_bytes - waiting')
+                time.sleep(0.1)
+        return self._cache[name].image_bytes
+
+    def add_image(self, name, path):
+        """
+        Add image to cache if it is not already there
+        """
+        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)
+            self._cache[name] = image
+        self._cache_dirty = True
+        # only one thread please
+        if not self._thread_running:
+            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 ?
+        while self._cache_dirty:
+            log.debug(u'process - recycle')
+            self.clean_cache()
+        self._thread_running = False
+        log.debug(u'process - ended')
+
+    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

=== modified file 'openlp/core/lib/renderer.py'
--- openlp/core/lib/renderer.py	2010-09-29 20:39:15 +0000
+++ openlp/core/lib/renderer.py	2010-10-24 06:46:49 +0000
@@ -51,11 +51,6 @@
         self._rect = None
         self.theme_name = None
         self._theme = None
-        self._bg_image_filename = None
-        self.frame = None
-        self.bg_frame = None
-        self.bg_image = None
-        self.bg_image_bytes = None
 
     def set_theme(self, theme):
         """
@@ -66,14 +61,7 @@
         """
         log.debug(u'set theme')
         self._theme = theme
-        self.bg_frame = None
-        self.bg_image = None
-        self.bg_image_bytes = None
-        self._bg_image_filename = None
         self.theme_name = theme.theme_name
-        if theme.background_type == u'image':
-            if theme.background_filename:
-                self._bg_image_filename = unicode(theme.background_filename)
 
     def set_text_rectangle(self, rect_main, rect_footer):
         """
@@ -105,39 +93,6 @@
             (build_lyrics_format_css(self._theme, self.page_width,
             self.page_height), build_lyrics_outline_css(self._theme))
 
-    def set_frame_dest(self, frame_width, frame_height):
-        """
-        Set the size of the slide.
-
-        ``frame_width``
-            The width of the slide.
-
-        ``frame_height``
-            The height of the slide.
-
-        """
-        log.debug(u'set frame dest (frame) w %d h %d', frame_width,
-            frame_height)
-        self.frame = QtGui.QImage(frame_width, frame_height,
-            QtGui.QImage.Format_ARGB32_Premultiplied)
-        if self._bg_image_filename and not self.bg_image:
-            self.bg_image = resize_image(self._bg_image_filename,
-                self.frame.width(), self.frame.height())
-        if self._theme.background_type == u'image':
-            self.bg_frame = QtGui.QImage(self.frame.width(),
-                self.frame.height(),
-                QtGui.QImage.Format_ARGB32_Premultiplied)
-            painter = QtGui.QPainter()
-            painter.begin(self.bg_frame)
-            painter.fillRect(self.frame.rect(), QtCore.Qt.black)
-            if self.bg_image:
-                painter.drawImage(0, 0, self.bg_image)
-            painter.end()
-            self.bg_image_bytes = image_to_byte(self.bg_frame)
-        else:
-            self.bg_frame = None
-            self.bg_image_bytes = None
-
     def format_slide(self, words, line_break):
         """
         Figure out how much text can appear on a slide, using the current

=== modified file 'openlp/core/lib/rendermanager.py'
--- openlp/core/lib/rendermanager.py	2010-09-27 18:34:40 +0000
+++ openlp/core/lib/rendermanager.py	2010-10-24 06:46:49 +0000
@@ -28,7 +28,7 @@
 
 from PyQt4 import QtCore
 
-from openlp.core.lib import Renderer, ThemeLevel, ServiceItem
+from openlp.core.lib import Renderer, ThemeLevel, ServiceItem, ImageManager
 from openlp.core.ui import MainDisplay
 
 log = logging.getLogger(__name__)
@@ -56,7 +56,9 @@
         """
         log.debug(u'Initilisation started')
         self.screens = screens
+        self.image_manager = ImageManager()
         self.display = MainDisplay(self, screens, False)
+        self.display.imageManager = self.image_manager
         self.display.setup()
         self.theme_manager = theme_manager
         self.renderer = Renderer()
@@ -75,9 +77,11 @@
         log.debug(u'Update Display')
         self.calculate_default(self.screens.current[u'size'])
         self.display = MainDisplay(self, self.screens, False)
+        self.display.imageManager = self.image_manager
         self.display.setup()
         self.renderer.bg_frame = None
         self.themedata = None
+        self.image_manager.update_display(self.width, self.height)
 
     def set_global_theme(self, global_theme, theme_level=ThemeLevel.Global):
         """
@@ -153,7 +157,8 @@
             self.calculate_default(self.screens.current[u'size'])
             self.renderer.set_theme(self.themedata)
             self.build_text_rectangle(self.themedata)
-            self.renderer.set_frame_dest(self.width, self.height)
+            self.image_manager.add_image(self.themedata.theme_name,
+                self.themedata.background_filename)
         return self.renderer._rect, self.renderer._rect_footer
 
     def build_text_rectangle(self, theme):
@@ -211,7 +216,7 @@
         serviceItem.raw_footer = footer
         serviceItem.render(True)
         self.display.buildHtml(serviceItem)
-        raw_html = serviceItem.get_rendered_frame(0)[1]
+        raw_html = serviceItem.get_rendered_frame(0)
         preview = self.display.text(raw_html)
         # Reset the real screen size for subsequent render requests
         self.calculate_default(self.screens.current[u'size'])

=== modified file 'openlp/core/lib/serviceitem.py'
--- openlp/core/lib/serviceitem.py	2010-10-15 08:35:00 +0000
+++ openlp/core/lib/serviceitem.py	2010-10-24 06:46:49 +0000
@@ -30,7 +30,6 @@
 
 import logging
 import os
-import time
 import uuid
 
 from PyQt4 import QtGui
@@ -160,12 +159,10 @@
             theme = self.theme
         self.main, self.footer = \
             self.render_manager.set_override_theme(theme, useOverride)
-        self.bg_image_bytes = self.render_manager.renderer.bg_image_bytes
         self.themedata = self.render_manager.renderer._theme
         if self.service_item_type == ServiceItemType.Text:
             log.debug(u'Formatting slides')
             for slide in self._raw_frames:
-                before = time.time()
                 formatted = self.render_manager \
                     .format_slide(slide[u'raw_slide'], line_break)
                 for page in formatted:
@@ -174,12 +171,8 @@
                         u'text': clean_tags(page.rstrip()),
                         u'html': expand_tags(page.rstrip()),
                         u'verseTag': slide[u'verseTag'] })
-                log.log(15, u'Formatting took %4s' % (time.time() - before))
-        elif self.service_item_type == ServiceItemType.Image:
-            for slide in self._raw_frames:
-                slide[u'image'] = resize_image(slide[u'image'],
-                    self.render_manager.width, self.render_manager.height)
-        elif self.service_item_type == ServiceItemType.Command:
+        elif self.service_item_type == ServiceItemType.Image or \
+            self.service_item_type == ServiceItemType.Command:
             pass
         else:
             log.error(u'Invalid value renderer :%s' % self.service_item_type)
@@ -192,7 +185,7 @@
                 else:
                     self.foot_text = u'%s<br>%s' % (self.foot_text, foot)
 
-    def add_from_image(self, path, title, image):
+    def add_from_image(self, path, title):
         """
         Add an image slide to the service item.
 
@@ -201,13 +194,11 @@
 
         ``title``
             A title for the slide in the service item.
-
-        ``image``
-            The actual image file name.
         """
         self.service_item_type = ServiceItemType.Image
         self._raw_frames.append(
-            {u'title': title, u'image': image, u'path': path})
+            {u'title': title, u'path': path})
+        self.render_manager.image_manager.add_image(title, path)
         self._new_item()
 
     def add_from_text(self, title, raw_slide, verse_tag=None):
@@ -241,7 +232,7 @@
         """
         self.service_item_type = ServiceItemType.Command
         self._raw_frames.append(
-            {u'title': file_name, u'image': image, u'path': path})
+            {u'title': file_name, u'image':image, u'path': path})
         self._new_item()
 
     def get_service_repr(self):
@@ -310,8 +301,7 @@
         elif self.service_item_type == ServiceItemType.Image:
             for text_image in serviceitem[u'serviceitem'][u'data']:
                 filename = os.path.join(path, text_image)
-                real_image = QtGui.QImage(unicode(filename))
-                self.add_from_image(path, text_image, real_image)
+                self.add_from_image(filename, text_image)
         elif self.service_item_type == ServiceItemType.Command:
             for text_image in serviceitem[u'serviceitem'][u'data']:
                 filename = os.path.join(path, text_image[u'title'])
@@ -387,9 +377,11 @@
         renders it if required.
         """
         if self.service_item_type == ServiceItemType.Text:
-            return None, self._display_frames[row][u'html'].split(u'\n')[0]
+            return self._display_frames[row][u'html'].split(u'\n')[0]
+        elif self.service_item_type == ServiceItemType.Image:
+            return self._raw_frames[row][u'title']
         else:
-            return self._raw_frames[row][u'image'], u''
+            return self._raw_frames[row][u'image']
 
     def get_frame_title(self, row=0):
         """

=== modified file 'openlp/core/ui/maindisplay.py'
--- openlp/core/ui/maindisplay.py	2010-10-17 14:25:03 +0000
+++ openlp/core/ui/maindisplay.py	2010-10-24 06:46:49 +0000
@@ -225,7 +225,14 @@
                 shrinkItem.resize(self.screen[u'size'].width(),
                     self.screen[u'size'].height())
 
-    def image(self, image):
+    def directImage(self, name, path):
+        """
+        API for replacement backgrounds so Images are added directly to cache
+        """
+        image = self.imageManager.add_image(name, path)
+        self.image(name)
+
+    def image(self, name):
         """
         Add an image as the background.  The image is converted to a
         bytestream on route.
@@ -234,25 +241,20 @@
             The Image to be displayed can be QImage or QPixmap
         """
         log.debug(u'image to display')
-        if not isinstance(image, QtGui.QImage):
-            image = resize_image(image, self.screen[u'size'].width(),
-                self.screen[u'size'].height())
+        image = self.imageManager.get_image_bytes(name)
         self.resetVideo()
         self.displayImage(image)
         # show screen
         if self.isLive:
             self.setVisible(True)
+        return self.preview()
 
     def displayImage(self, image):
         """
         Display an image, as is.
         """
         if image:
-            if isinstance(image, QtGui.QImage):
-                js = u'show_image("data:image/png;base64,%s");' % \
-                    image_to_byte(image)
-            else:
-                js = u'show_image("data:image/png;base64,%s");' % image
+            js = u'show_image("data:image/png;base64,%s");' % image
         else:
             js = u'show_image("");'
         self.frame.evaluateJavaScript(js)
@@ -399,6 +401,9 @@
         self.loaded = False
         self.initialFrame = False
         self.serviceItem = serviceItem
+        if self.serviceItem.themedata.background_filename:
+            self.serviceItem.bg_image_bytes = self.imageManager. \
+                get_image_bytes(self.serviceItem.themedata.theme_name)
         html = build_html(self.serviceItem, self.screen, self.parent.alertTab,
             self.isLive)
         log.debug(u'buildHtml - pre setHtml')

=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py	2010-10-17 07:26:14 +0000
+++ openlp/core/ui/slidecontroller.py	2010-10-24 06:46:49 +0000
@@ -26,6 +26,7 @@
 
 import logging
 import os
+import time
 
 from PyQt4 import QtCore, QtGui
 from PyQt4.phonon import Phonon
@@ -400,6 +401,7 @@
         log.debug(u'screenSizeChanged live = %s' % self.isLive)
         # rebuild display as screen size changed
         self.display = MainDisplay(self, self.screens, self.isLive)
+        self.display.imageManager = self.parent.RenderManager.image_manager
         self.display.alertTab = self.alertTab
         self.ratio = float(self.screens.current[u'size'].width()) / \
             float(self.screens.current[u'size'].height())
@@ -585,13 +587,14 @@
                 label = QtGui.QLabel()
                 label.setMargin(4)
                 label.setScaledContents(True)
-                if isinstance(frame[u'image'], QtGui.QImage):
-                    label.setPixmap(QtGui.QPixmap.fromImage(frame[u'image']))
-                else:
-                    pixmap = resize_image(frame[u'image'],
+                if self.serviceItem.is_command():
+                    image = resize_image(frame[u'image'],
                         self.parent.RenderManager.width,
                         self.parent.RenderManager.height)
-                    label.setPixmap(QtGui.QPixmap.fromImage(pixmap))
+                else:
+                    image = self.parent.RenderManager.image_manager. \
+                            get_image(frame[u'title'])
+                label.setPixmap(QtGui.QPixmap.fromImage(image))
                 self.PreviewListWidget.setCellWidget(framenumber, 0, label)
                 slideHeight = width * self.parent.RenderManager.screen_ratio
                 row += 1
@@ -782,15 +785,12 @@
                     [self.serviceItem, self.isLive, row])
                 self.updatePreview()
             else:
-                frame, raw_html = self.serviceItem.get_rendered_frame(row)
+                toDisplay = self.serviceItem.get_rendered_frame(row)
                 if self.serviceItem.is_text():
-                    frame = self.display.text(raw_html)
-                else:
-                    self.display.image(frame)
-                if isinstance(frame, QtGui.QImage):
-                    self.SlidePreview.setPixmap(QtGui.QPixmap.fromImage(frame))
-                else:
-                    self.SlidePreview.setPixmap(QtGui.QPixmap(frame))
+                    frame = self.display.text(toDisplay)
+                else:
+                    frame = self.display.image(toDisplay)
+                self.SlidePreview.setPixmap(QtGui.QPixmap.fromImage(frame))
             self.selectedRow = row
         Receiver.send_message(u'slidecontroller_%s_changed' % self.typePrefix,
             row)

=== modified file 'openlp/plugins/images/lib/mediaitem.py'
--- openlp/plugins/images/lib/mediaitem.py	2010-09-27 18:34:40 +0000
+++ openlp/plugins/images/lib/mediaitem.py	2010-10-24 06:46:49 +0000
@@ -166,9 +166,8 @@
             for item in items:
                 bitem = self.listView.item(item.row())
                 filename = unicode(bitem.data(QtCore.Qt.UserRole).toString())
-                frame = QtGui.QImage(unicode(filename))
                 (path, name) = os.path.split(filename)
-                service_item.add_from_image(path, name, frame)
+                service_item.add_from_image(filename, name)
             return True
         else:
             return False
@@ -185,7 +184,8 @@
             for item in items:
                 bitem = self.listView.item(item.row())
                 filename = unicode(bitem.data(QtCore.Qt.UserRole).toString())
-                self.parent.liveController.display.image(filename)
+                (path, name) = os.path.split(filename)
+                self.parent.liveController.display.directImage(name, filename)
         self.resetButton.setVisible(True)
 
     def onPreviewClick(self):


Follow ups