Set binary flag when opening image file in ImagePreloader. Fixes
[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), "rb")
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                 pixbufLoader = self.__preloadStates[filename].pixbufLoader
112                 if pixbufLoader:
113                     pixbufLoader.close()
114                 del self.__preloadStates[filename]
115
116         # Preload the new images.
117         for filename in filenames:
118             if not self.__preloadStates.has_key(filename):
119                 self.__preloadStates[filename] = _PreloadState(
120                     filename, self._fileSystemCodeset)
121             ps = self.__preloadStates[filename]
122             try:
123                 self._debugPrint("Preloading %s" % filename)
124                 timer = Timer()
125                 while not ps.loadFinished: # could be set by getPixbuf
126                     data = ps.fp.read(32768)
127                     if not data:
128                         ps.pixbufLoader.close()
129                         ps.fullsizePixbuf = ps.pixbufLoader.get_pixbuf()
130                         break
131                     ps.pixbufLoader.write(data)
132                     yield True
133                 self._debugPrint("Preload of %s took %.2f seconds" % (
134                     filename, timer.get()))
135             except (gobject.GError, OSError):
136                 ps.pixbufLoader.close()
137             ps.pixbufLoader = None
138             ps.loadFinished = True
139
140             ps.scaledPixbuf = self._maybeScalePixbuf(
141                 ps.fullsizePixbuf,
142                 ps.scaledPixbuf,
143                 scaledMaxWidth,
144                 scaledMaxHeight,
145                 filename)
146             yield True
147
148         # We're finished.
149         self.__idleTimerTag = None
150         yield False
151
152     def _maybeScalePixbuf(self, fullsizePixbuf, scaledPixbuf,
153                           maxWidth, maxHeight, filename):
154         if not fullsizePixbuf:
155             return None
156         elif (fullsizePixbuf.get_width() <= maxWidth and
157               fullsizePixbuf.get_height() <= maxHeight):
158             return fullsizePixbuf
159         elif not (scaledPixbuf and
160                   scaledPixbuf.get_width() <= maxWidth and
161                   scaledPixbuf.get_height() <= maxHeight and
162                   (scaledPixbuf.get_width() == maxWidth or
163                    scaledPixbuf.get_height() == maxHeight)):
164             scaledWidth, scaledHeight = calculateDownscaledDimensions(
165                 fullsizePixbuf.get_width(),
166                 fullsizePixbuf.get_height(),
167                 maxWidth,
168                 maxHeight)
169             self._debugPrint("Scaling %s to %dx%d" % (
170                 filename, scaledWidth, scaledHeight))
171             if scaledPixbuf:
172                 self._debugPrint("old size: %dx%d" % (
173                     scaledPixbuf.get_width(),
174                     scaledPixbuf.get_height()))
175                 self._debugPrint("new size: %dx%d" % (
176                     scaledWidth,
177                     scaledHeight))
178             timer = Timer()
179             scaledPixbuf = fullsizePixbuf.scale_simple(
180                 scaledWidth,
181                 scaledHeight,
182                 gtk.gdk.INTERP_BILINEAR) # TODO: Make configurable.
183             self._debugPrint("Scaling of %s to %dx%d took %.2f seconds" % (
184                 filename, scaledWidth, scaledHeight, timer.get()))
185             return scaledPixbuf
186         else: # Appropriately sized scaled pixbuf.
187             return scaledPixbuf