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