# -*- 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 prepare(self, tiles):
"""Make room before rendering new tiles.
This ensures there is space for the new tiles by adjusting
the cache size and/or purging older images as needed.
"""
# approximate size of the new tiles at 32 bits (== 4 bytes) per pixel
tileBytes = sum(tile.w * tile.h for tile in tiles) * 4
# adjust the cache size to fit at least two pages' visible tiles
# to avoid re-rendering either page when both are displayed
oldsize = self.maxsize
self.maxsize = max(type(self).maxsize, 2 * tileBytes)
if self.maxsize < oldsize:
# clear everything when shrinking the cache because otherwise
# larger tiles might prove difficult to purge()
self.clear()
else:
# free memory from unneeded tiles before rendering the new ones
self.purge(tileBytes)
[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
e = d[tile] = ImageEntry(image)
self.currentsize += e.bcount
[docs]
def purge(self, reservedsize=0):
"""Purge old images if needed.
At least `reservedsize` bytes will remain available.
"""
limit = self.maxsize - reservedsize
if self.currentsize <= limit:
return
# 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 our limit ...
currentsize = 0
for time, bcount, group, ident, key, tile in entries:
currentsize += bcount
if currentsize >= limit:
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))