Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/src/jarabe/journal/preview.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/jarabe/journal/preview.py')
-rw-r--r--src/jarabe/journal/preview.py216
1 files changed, 216 insertions, 0 deletions
diff --git a/src/jarabe/journal/preview.py b/src/jarabe/journal/preview.py
new file mode 100644
index 0000000..1670884
--- /dev/null
+++ b/src/jarabe/journal/preview.py
@@ -0,0 +1,216 @@
+# Copyright (C) 2010, Aleksey Lim
+#
+# 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 2 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, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+import os
+import math
+
+import gtk
+import gio
+import gobject
+
+from sugar import dispatch
+from sugar.util import LRU
+from sugar.graphics import style
+
+from jarabe.journal import model
+
+
+fetched = dispatch.Signal()
+
+THUMB_WIDTH = style.zoom(240)
+THUMB_HEIGHT = style.zoom(180)
+
+_CHUNK_SIZE = 1024 * 10 # 10K
+_MAX_FILESIZE = 1024 * 1024 * 10 # 10M
+
+_fetch_queue = []
+
+
+def fetch(offset, metadata):
+ entry = _CacheEntry(offset, metadata)
+
+ if entry not in _fetch_queue:
+ _fetch_queue.append(entry)
+ if len(_fetch_queue) == 1:
+ gobject.idle_add(_process_queue)
+
+def discard_queue(visible_range):
+ new_queue = []
+
+ for i in _fetch_queue:
+ if i.offset in visible_range:
+ new_queue.append(i)
+
+ global _fetch_queue
+ _fetch_queue = new_queue
+
+def _process_queue():
+ while len(_fetch_queue):
+ entry = _fetch_queue[0]
+
+ logging.debug('Loading preview for %s', entry.uid)
+
+ if entry.uid.startswith('/'):
+ if not os.path.isfile(entry.uid):
+ logging.warning('Preview %s is not a file', entry.uid)
+ _commit(entry, None)
+ elif os.path.getsize(entry.uid) > _MAX_FILESIZE:
+ logging.debug('Preview %s is too big to load', entry.uid)
+ _commit(entry, None)
+ else:
+ _AsyncLoader(entry)
+ else:
+ _load_props(entry)
+
+ break
+
+ return False
+
+def _commit(entry, pixbuf):
+ if not _fetch_queue or _fetch_queue[0] != entry:
+ logging.debug('Discard %r preview', entry.uid)
+ else:
+ del _fetch_queue[0]
+
+ if pixbuf is None:
+ logging.debug('Empty preview for %s', entry.uid)
+ else:
+ logging.debug('Ready preview for %s', entry.uid)
+ fetched.send(None, offset=entry.offset, pixbuf=pixbuf)
+
+ if len(_fetch_queue):
+ gobject.idle_add(_process_queue)
+
+def _load_preview(entry, preview):
+ if not preview:
+ logging.debug('Empty preview for %s', entry.uid)
+ _commit(entry, None)
+ return
+
+ if preview[1:4] != 'PNG':
+ # TODO: We are close to be able to drop this.
+ import base64
+ preview = base64.b64decode(preview)
+
+ loader = gtk.gdk.PixbufLoader()
+ loader.connect('size-prepared', _size_prepared_cb)
+
+ try:
+ loader.write(preview)
+ except Exception:
+ logging.exception('Can not load preview from metadata for %s',
+ entry.uid)
+ finally:
+ loader.close()
+
+ pixbuf = loader.get_pixbuf()
+ if pixbuf is None:
+ _commit(entry, None)
+ else:
+ _commit(entry, pixbuf)
+
+def _load_props(entry):
+
+ def reply_cb(props):
+ if props is None:
+ _commit(entry, None)
+ else:
+ _load_preview(entry, props.get('preview'))
+
+ model.get(entry.uid, reply_cb)
+
+def _size_prepared_cb(loader, width, height):
+ dest_width = THUMB_WIDTH
+ dest_height = THUMB_HEIGHT
+
+ if width == dest_width and height == dest_height:
+ return
+
+ ratio_width = float(dest_width) / width
+ ratio_height = float(dest_height) / height
+ ratio = min(ratio_width, ratio_height)
+
+ # preserve original ration
+ if ratio_width != ratio:
+ dest_width = int(math.ceil(width * ratio))
+ elif ratio_height != ratio:
+ dest_height = int(math.ceil(height * ratio))
+
+ loader.set_size(dest_width, dest_height)
+
+
+class _AsyncLoader(object):
+
+ def __init__(self, entry):
+ self._entry = entry
+
+ self._loader = gtk.gdk.PixbufLoader()
+ self._loader.connect('size-prepared', _size_prepared_cb)
+
+ self._stream = None
+ self._file = gio.File(entry.uid)
+ self._file.read_async(self.__file_read_async_cb)
+
+ def __file_read_async_cb(self, input_file, result):
+ try:
+ self._stream = self._file.read_finish(result)
+ except Exception:
+ logging.exception('Can not read preview for %s', self._entry.uid)
+ _commit(self._entry, None)
+ return
+
+ self._stream.read_async(_CHUNK_SIZE, self.__stream_read_async_cb,
+ gobject.PRIORITY_LOW)
+
+ def __stream_read_async_cb(self, input_stream, result):
+ data = self._stream.read_finish(result)
+
+ if data and self._process_loader(self._loader.write, data):
+ self._stream.read_async(_CHUNK_SIZE, self.__stream_read_async_cb,
+ gobject.PRIORITY_LOW)
+ return
+
+ if data is None:
+ logging.warning('Bad preview data from %s', self._entry.uid)
+
+ self._stream.close()
+
+ if self._process_loader(self._loader.close):
+ _commit(self._entry, self._loader.get_pixbuf())
+ else:
+ _commit(self._entry, None)
+
+ def _process_loader(self, method, *args):
+ try:
+ method(*args)
+ except Exception, e:
+ logging.debug('Can not process preview for %s: %r',
+ self._entry.uid, e)
+ return False
+ else:
+ return True
+
+class _CacheEntry(object):
+ uid = None
+ offset = None
+
+ def __init__(self, offset, metadata):
+ self.uid = metadata['uid']
+ self.offset = offset
+
+ def __cmp__(self, other):
+ return cmp((self.uid, self.offset), (other.uid, other.offset))