1# This file is part of the qpageview package. 2# 3# Copyright (c) 2016 - 2019 by Wilbert Berendsen 4# 5# This program is free software; you can redistribute it and/or 6# modify it under the terms of the GNU General Public License 7# as published by the Free Software Foundation; either version 2 8# of the License, or (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program; if not, write to the Free Software 17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18# See http://www.gnu.org/licenses/ for more information. 19 20""" 21Cache logic. 22""" 23 24import weakref 25import time 26 27 28class ImageEntry: 29 def __init__(self, image): 30 self.image = image 31 self.bcount = image.byteCount() 32 self.time = time.time() 33 34 35class ImageCache: 36 """Cache generated images. 37 38 Store and retrieve them under a key (see render.Renderer.key()). 39 40 """ 41 maxsize = 209715200 # 200M 42 currentsize = 0 43 44 def __init__(self): 45 self._cache = weakref.WeakKeyDictionary() 46 47 def clear(self): 48 """Remove all cached images.""" 49 self._cache.clear() 50 self.currentsize = 0 51 52 def invalidate(self, page): 53 """Clear cache contents for the specified page.""" 54 try: 55 del self._cache[page.group()][page.ident()] 56 except KeyError: 57 pass 58 59 def tileset(self, key): 60 """Return a dictionary with tile-entry pairs for the key. 61 62 If no single tile is available, an empty dict is returned. 63 64 """ 65 try: 66 return self._cache[key.group][key.ident][key[2:]] 67 except KeyError: 68 return {} 69 70 def addtile(self, key, tile, image): 71 """Add image for the specified key and tile.""" 72 d = self._cache.setdefault(key.group, {}).setdefault(key.ident, {}).setdefault(key[2:], {}) 73 try: 74 self.currentsize -= d[tile].bcount 75 except KeyError: 76 pass 77 78 purgeneeded = self.currentsize > self.maxsize 79 80 e = d[tile] = ImageEntry(image) 81 self.currentsize += e.bcount 82 83 if not purgeneeded: 84 return 85 86 # purge old images is needed, 87 # cache groups may have disappeared so count all images 88 89 items = sorted( 90 (entry.time, entry.bcount, group, ident, key, tile) 91 for group, identd in self._cache.items() 92 for ident, keyd in identd.items() 93 for key, tiled in keyd.items() 94 for tile, entry in tiled.items()) 95 96 # now count the newest images until maxsize ... 97 items = reversed(items) 98 currentsize = 0 99 for time, bcount, group, ident, key, tile in items: 100 currentsize += bcount 101 if currentsize > self.maxsize: 102 break 103 self.currentsize = currentsize 104 # ... and delete the remaining images, deleting empty dicts as well 105 for time, bcount, group, ident, key, tile in items: 106 del self._cache[group][ident][key][tile] 107 if not self._cache[group][ident][key]: 108 del self._cache[group][ident][key] 109 if not self._cache[group][ident]: 110 del self._cache[group][ident] 111 if not self._cache[group]: 112 del self._cache[group] 113 114 def closest(self, key): 115 """Iterate over suitable image tilesets but with a different size. 116 117 Yields (width, height, tileset) tuples. 118 119 This can be used for interim display while the real image is being 120 rendered. 121 122 """ 123 # group and ident must be there. 124 try: 125 keyd = self._cache[key.group][key.ident] 126 except KeyError: 127 return () 128 129 # prevent returning images that are too small 130 minwidth = min(100, key.width / 2) 131 132 suitable = [ 133 (k[1], k[2], tileset) 134 for k, tileset in keyd.items() 135 if k[0] == key.rotation and k[1] != key.width and k[1] > minwidth] 136 return sorted(suitable, key=lambda s: abs(1 - s[0] / key.width)) 137 138 139