# -*- coding: utf-8 -*-
#
# This file is part of the qpageview package.
#
# Copyright (c) 2016 - 2019 by Wilbert Berendsen
#
# 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 3
# 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
# See http://www.gnu.org/licenses/ for more information.
"""
Cache logic.
"""
import weakref
import time
[docs]
class ImageEntry:
def __init__(self, image):
self.image = image
self.bcount = image.sizeInBytes()
self.time = time.time()
[docs]
class ImageCache:
"""Cache generated images.
Store and retrieve them under a key (see render.Renderer.key()).
"""
maxsize = 209715200 # 200M
currentsize = 0
def __init__(self):
self._cache = weakref.WeakKeyDictionary()
[docs]
def clear(self):
"""Remove all cached images."""
self._cache.clear()
self.currentsize = 0
[docs]
def invalidate(self, page):
"""Clear cache contents for the specified page."""
try:
del self._cache[page.group()][page.ident()]
except KeyError:
pass
[docs]
def tileset(self, key):
"""Return a dictionary with tile-entry pairs for the key.
If no single tile is available, an empty dict is returned.
"""
try:
return self._cache[key.group][key.ident][key[2:]]
except KeyError:
return {}
[docs]
def addtile(self, key, tile, image):
"""Add image for the specified key and tile."""
d = self._cache.setdefault(key.group, {}).setdefault(key.ident, {}).setdefault(key[2:], {})
try:
self.currentsize -= d[tile].bcount
except KeyError:
pass
purgeneeded = self.currentsize > self.maxsize
e = d[tile] = ImageEntry(image)
self.currentsize += e.bcount
if not purgeneeded:
return
# purge old images is needed,
# cache groups may have disappeared so count all images
entries = iter(sorted(
((entry.time, entry.bcount, group, ident, key, tile)
for group, identd in self._cache.items()
for ident, keyd in identd.items()
for key, tiled in keyd.items()
for tile, entry in tiled.items()),
key=(lambda item: item[:2]), reverse=True))
# now count the newest images until maxsize ...
currentsize = 0
for time, bcount, group, ident, key, tile in entries:
currentsize += bcount
if currentsize > self.maxsize:
break
self.currentsize = currentsize
# ... and delete the remaining images, deleting empty dicts as well
for time, bcount, group, ident, key, tile in entries:
del self._cache[group][ident][key][tile]
if not self._cache[group][ident][key]:
del self._cache[group][ident][key]
if not self._cache[group][ident]:
del self._cache[group][ident]
if not self._cache[group]:
del self._cache[group]
[docs]
def closest(self, key):
"""Iterate over suitable image tilesets but with a different size.
Yields (width, height, tileset) tuples.
This can be used for interim display while the real image is being
rendered.
"""
# group and ident must be there.
try:
keyd = self._cache[key.group][key.ident]
except KeyError:
return ()
# prevent returning images that are too small
minwidth = min(100, key.width / 2)
suitable = [
(k[1], k[2], tileset)
for k, tileset in keyd.items()
if k[0] == key.rotation and k[1] != key.width and k[1] > minwidth]
return sorted(suitable, key=lambda s: abs(1 - s[0] / key.width))