d133c42a351da987266a53b3731a80705dfce5a8
[joel/kofoto.git] / src / gkofoto / gkofoto / imagepreloader.py
1 import gobject
2 import gtk
3 from kofoto.timer import Timer
4 from kofoto.common import calculateDownscaledDimensions
5
6 class _PreloadState:
7     def __init__(self, filename, fileSystemCodeset):
8         self.fullsizePixbuf = None
9         self.pixbufLoader = gtk.gdk.PixbufLoader()
10         self.loadFinished = False # Whether loading of fullsizePixbuf is ready.
11         self.scaledPixbuf = None
12         try:
13             self.fp = open(filename.encode(fileSystemCodeset))
14         except OSError:
15             self.loadFinished = True
16
17 class ImagePreloader(object):
18     def __init__(self, fileSystemCodeset, debugPrintFunction=None):
19         self._fileSystemCodeset = fileSystemCodeset
20         if debugPrintFunction:
21             self._debugPrint = debugPrintFunction
22         else:
23             self._debugPrint = lambda x: None
24         self.__delayTimerTag = None
25         self.__idleTimerTag = None
26         # filename --> _PreloadState
27         self.__preloadStates = {}
28
29     def preloadImages(self, filenames, scaledMaxWidth, scaledMaxHeight):
30         """Preload images.
31
32         The images are loaded and stored both in a fullsize version
33         and a scaled-down version.
34
35         Note that this method discards previously preloaded images,
36         except those present in the filenames argument.
37
38         filenames -- Iterable of filenames of images to preload.
39         scaledMaxWidth -- Wanted maximum width of the scaled image.
40         scaledMaxHeight -- Wanted maximum height of the scaled image.
41         """
42         if self.__delayTimerTag != None:
43             gobject.source_remove(self.__delayTimerTag)
44         if self.__idleTimerTag != None:
45             gobject.source_remove(self.__idleTimerTag)
46
47         # Delay preloading somewhat to make display of the current
48         # image faster. Not sure whether it helps, though...
49         self.__delayTimerTag = gobject.timeout_add(
50             500,
51             self._beginPreloading,
52             filenames,
53             scaledMaxWidth,
54             scaledMaxHeight)
55
56     def getPixbuf(self, filename, maxWidth=None, maxHeight=None):
57         """Get a pixbuf.
58
59         If maxWidth and maxHeight are None, the fullsize version is
60         returned, otherwise a scaled version no larger than maxWidth
61         and maxHeight is returned.
62
63         The pixbuf may be None if the image was unloadable.
64         """
65         pixbuf = None
66
67         if not self.__preloadStates.has_key(filename):
68             self.__preloadStates[filename] = _PreloadState(
69                 filename, self._fileSystemCodeset)
70         ps = self.__preloadStates[filename]
71         if not ps.loadFinished:
72             try:
73                 ps.pixbufLoader.write(ps.fp.read())
74                 ps.pixbufLoader.close()
75                 ps.fullsizePixbuf = ps.pixbufLoader.get_pixbuf()
76             except (gobject.GError, OSError):
77                 ps.pixbufLoader.close()
78                 ps.fullsizePixbuf = None
79             ps.pixbufLoader = None
80             ps.loadFinished = True
81         if (ps.fullsizePixbuf == None or
82             (maxWidth == None and maxHeight == None) or
83             (ps.fullsizePixbuf.get_width() <= maxWidth and
84              ps.fullsizePixbuf.get_height() <= maxHeight)):
85             # Requested fullsize pixbuf or scaled pixbuf larger than
86             # fullsize.
87             return ps.fullsizePixbuf
88         else:
89             # Requested scaled pixbuf.
90             ps.scaledPixbuf = self._maybeScalePixbuf(
91                 ps.fullsizePixbuf,
92                 ps.scaledPixbuf,
93                 maxWidth,
94                 maxHeight,
95                 filename)
96             return ps.scaledPixbuf
97
98     def _beginPreloading(self, filenames, scaledMaxWidth, scaledMaxHeight):
99         self.__idleTimerTag = gobject.idle_add(
100             self._preloadImagesWorker(
101                 filenames, scaledMaxWidth, scaledMaxHeight).next)
102         return False
103
104     def _preloadImagesWorker(self, filenames, scaledMaxWidth, scaledMaxHeight):
105         filenames = list(filenames)
106         self._debugPrint("Preloading images %s" % str(filenames))
107
108         # Discard old preloaded images.
109         for filename in self.__preloadStates.keys():
110             if not filename in filenames:
111                 del self.__preloadStates[filename]
112
113         # Preload the new images.
114         for filename in filenames:
115             if not self.__preloadStates.has_key(filename):
116                 self.__preloadStates[filename] = _PreloadState(
117                     filename, self._fileSystemCodeset)
118             ps = self.__preloadStates[filename]
119             try:
120                 self._debugPrint("Preloading %s" % filename)
121                 timer = Timer()
122                 while not ps.loadFinished: # could be set by getPixbuf
123                     data = ps.fp.read(32768)
124                     if not data:
125                         ps.pixbufLoader.close()
126                         ps.fullsizePixbuf = ps.pixbufLoader.get_pixbuf()
127                         break
128                     ps.pixbufLoader.write(data)
129                     yield True
130                 self._debugPrint("Preload of %s took %.2f seconds" % (
131                     filename, timer.get()))
132             except (gobject.GError, OSError):
133                 ps.pixbufLoader.close()
134             ps.pixbufLoader = None
135             ps.loadFinished = True
136
137             ps.scaledPixbuf = self._maybeScalePixbuf(
138                 ps.fullsizePixbuf,
139                 ps.scaledPixbuf,
140                 scaledMaxWidth,
141                 scaledMaxHeight,
142                 filename)
143             yield True
144
145         # We're finished.
146         self.__idleTimerTag = None
147         yield False
148
149     def _maybeScalePixbuf(self, fullsizePixbuf, scaledPixbuf,
150                           maxWidth, maxHeight, filename):
151         if not fullsizePixbuf:
152             return None
153         elif (fullsizePixbuf.get_width() <= maxWidth and
154               fullsizePixbuf.get_height() <= maxHeight):
155             return fullsizePixbuf
156         elif not (scaledPixbuf and
157                   scaledPixbuf.get_width() <= maxWidth and
158                   scaledPixbuf.get_height() <= maxHeight and
159                   (scaledPixbuf.get_width() == maxWidth or
160                    scaledPixbuf.get_height() == maxHeight)):
161             scaledWidth, scaledHeight = calculateDownscaledDimensions(
162                 fullsizePixbuf.get_width(),
163                 fullsizePixbuf.get_height(),
164                 maxWidth,
165                 maxHeight)
166             self._debugPrint("Scaling %s to %dx%d" % (
167                 filename, scaledWidth, scaledHeight))
168             if scaledPixbuf:
169                 self._debugPrint("old size: %dx%d" % (
170                     scaledPixbuf.get_width(),
171                     scaledPixbuf.get_height()))
172                 self._debugPrint("new size: %dx%d" % (
173                     scaledWidth,
174                     scaledHeight))
175             timer = Timer()
176             scaledPixbuf = fullsizePixbuf.scale_simple(
177                 scaledWidth,
178                 scaledHeight,
179                 gtk.gdk.INTERP_BILINEAR) # TODO: Make configurable.
180             self._debugPrint("Scaling of %s to %dx%d took %.2f seconds" % (
181                 filename, scaledWidth, scaledHeight, timer.get()))
182             return scaledPixbuf
183         else: # Appropriately sized scaled pixbuf.
184             return scaledPixbuf