Source code for pympress.surfacecache

# -*- coding: utf-8 -*-
#
#       surfacecache.py
#
#       Copyright 2010 Thomas Jost <thomas.jost@gmail.com>
#       Copyright 2015 Cimbali <me@cimba.li>
#
#       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 2 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 Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.
"""
:mod:`pympress.surfacecache` -- pages prerendering and caching
--------------------------------------------------------------

This modules contains stuff needed for caching pages and prerendering them. This
is done by the :class:`~pympress.surfacecache.SurfaceCache` class, using several
`dict` of :class:`~cairo.ImageSurface` for storing rendered pages.

The problem is, neither Gtk+ nor Poppler are particularly threadsafe.
Hence the prerendering isn't really done in parallel in another thread, but
scheduled on the main thread at idle times using GLib.idle_add().
"""

import logging
logger = logging.getLogger(__name__)

import threading
import functools
import collections

import gi
import cairo
gi.require_version('Gtk', '3.0')
from gi.repository import GLib


[docs] class SurfaceCache(object): """ Pages caching and prerendering made (almost) easy. Args: doc (:class:`~pympress.document.Document`): the current document max_pages (`int`): The maximum page number. """ #: The actual cache. The `dict`s keys are widget names and its values are #: :class:`~collections.OrderedDict`, whose keys are page numbers #: and values are instances of :class:`~cairo.ImageSurface`. #: In each :class:`~collections.OrderedDict` keys are ordered by #: Least Recently Used (get or set), when the size is beyond #: :attr:`max_pages`, pages are popped from the start of the cache. surface_cache = {} #: `dict` containing functions that return a :class:`~cairo.Surface` given a :class:`~cairo.Format`, #: width `int` and height `int`, see :meth:`~Gtk.Window.create_similar_image_surface` surface_factory = {} #: Size of the different managed widgets, as a `dict` of tuples surface_size = {} #: Type of document handled by each widget. It is a `dict`: its keys are #: widget names and its values are document types from ui. surface_type = {} #: Dictionary of :class:`~threading.Lock` used for managing conccurent #: accesses to :attr:`surface_cache` and :attr:`surface_size` locks = {} #: Set of widgets for which we ignore the max unlimited = set() #: The current :class:`~pympress.document.Document`. doc = None #: :class:`~threading.Lock` used to manage conccurent accesses to :attr:`doc`. doc_lock = None #: Set of active widgets active_widgets = set() #: maximum number of pages we keep in cache max_pages = 200 def __init__(self, doc, max_pages): self.max_pages = max_pages self.doc = doc self.doc_lock = threading.Lock()
[docs] def add_widget(self, widget, wtype, prerender_enabled = True, zoomed = False, ignore_max = False): """ Add a widget to the list of widgets that have to be managed (for caching and prerendering). This creates new entries for ``widget_name`` in the needed internal data structures, and creates a new thread for prerendering pages for this widget. Args: widget (:class:`~Gtk.Widget`): The widget for which we need to cache wtype (`int`): type of document handled by the widget (see :attr:`surface_type`) prerender_enabled (`bool`): whether this widget is initially in the list of widgets to prerender zoomed (`bool`): whether we will cache a zoomed portion of the widget ignore_max (`bool`): whether we will cache an unlimited number of slides """ widget_name = widget.get_name().rstrip('0123456789') + ('_zoomed' if zoomed else '') with self.locks.setdefault(widget_name, threading.Lock()): self.surface_cache[widget_name] = collections.OrderedDict() self.surface_size[widget_name] = (-1, -1) self.surface_type[widget_name] = wtype self.surface_factory[widget_name] = functools.partial(self._create_surface, widget) if prerender_enabled and not zoomed: self.enable_prerender(widget_name) if ignore_max: self.unlimited.add(widget_name)
[docs] def swap_document(self, new_doc): """ Replaces the current document for which to cache slides with a new one. This function also clears the cached pages, since they now belong to an outdated document. Args: new_doc (:class:`~pympress.document.Document`): the new document """ with self.doc_lock: self.doc = new_doc self.clear_cache()
[docs] def disable_prerender(self, widget_name): """ Remove a widget from the ones to be prerendered. Args: widget_name (`str`): string used to identify a widget """ self.active_widgets.discard(widget_name)
[docs] def enable_prerender(self, widget_name): """ Add a widget to the ones to be prerendered. Args: widget_name (`str`): string used to identify a widget """ self.active_widgets.add(widget_name)
[docs] def set_widget_type(self, widget_name, wtype): """ Set the document type of a widget. Args: widget_name (`str`): string used to identify a widget wtype (`int`): type of document handled by the widget (see :attr:`surface_type`) """ with self.locks[widget_name]: if self.surface_type[widget_name] != wtype: self.surface_type[widget_name] = wtype self.surface_cache[widget_name].clear()
[docs] def get_widget_type(self, widget_name): """ Get the document type of a widget. Args: widget_name (`str`): string used to identify a widget Returns: `int`: type of document handled by the widget (see :attr:`surface_type`) """ return self.surface_type[widget_name]
[docs] def clear_cache(self, widget_name=None): """ Remove all cached values for a given widget. Useful for zoomed views. Args: widget_name (`str`): name of the widget that is resized, `None` for all widgets. """ for widget in [widget_name] if widget_name is not None else self.locks: with self.locks[widget]: self.surface_cache[widget].clear()
[docs] def resize_widget(self, widget_name, width, height): """ Change the size of a registered widget, thus invalidating all the cached pages. Args: widget_name (`str`): name of the widget that is resized width (`int`): new width of the widget height (`int`): new height of the widget """ with self.locks[widget_name]: if (width, height) != self.surface_size[widget_name]: self.surface_cache[widget_name].clear() self.surface_size[widget_name] = (width, height)
[docs] def get(self, widget_name, page_nb): """ Fetch a cached, prerendered page for the specified widget. Args: widget_name (`str`): name of the concerned widget page_nb (`int`): number of the page to fetch in the cache Returns: :class:`~cairo.ImageSurface`: the cached page if available, or `None` otherwise """ with self.locks[widget_name]: pc = self.surface_cache[widget_name] if page_nb in pc: pc.move_to_end(page_nb) return pc[page_nb] else: return None
[docs] def put(self, widget_name, page_nb, val): """ Store a rendered page in the cache. Args: widget_name (`str`): name of the concerned widget page_nb (`int`): number of the page to store in the cache val (:class:`~cairo.ImageSurface`): content to store in the cache """ with self.locks[widget_name]: pc = self.surface_cache[widget_name] pc[page_nb] = val pc.move_to_end(page_nb) while len(pc) > self.max_pages: pc.popitem(False)
def _create_surface(self, widget, fmt, width, height): """ Given a widget, create a cairo Image surface with appropriate size and scaling. Args: widget (:class:`~Gtk.Widget`): the widget for which we’re caching data. fmt (:class:`~cairo.Format`): the format for the new surface width (`int`): width of the new surface height (`int`): height of the new surface Returns: :class:`~cairo.ImageSurface`: a new image surface """ window = widget.get_window() scale = window.get_scale_factor() try: return window.create_similar_image_surface(fmt, width * scale, height * scale, scale) except cairo.Error: logger.warning('Failed creating a {} cache surface sized {}x{} scale {} for widget {}' .format(fmt, width * scale, height * scale, scale, widget.get_name()), exc_info=True) # Warn here but handle error at the call site raise
[docs] def prerender(self, page_nb): """ Queue a page for prerendering. The specified page will be prerendered for all the registered widgets. Args: page_nb (`int`): number of the page to be prerendered """ for name in self.active_widgets: GLib.idle_add(self.renderer, name, page_nb)
[docs] def renderer(self, widget_name, page_nb): """ Rendering function. This function is meant to be scheduled on the GLib main loop. When run, it will go through the following steps: - check if the job's result is not already available in the cache - render it in a new :class:`~cairo.ImageSurface` if necessary - store it in the cache if it was not added there since the beginning of the process and the widget configuration is still valid Args: widget_name (`str`): name of the concerned widget page_nb (`int`): number of the page to store in the cache """ with self.locks[widget_name]: if page_nb in self.surface_cache[widget_name]: # Already in cache return GLib.SOURCE_REMOVE ww, wh = self.surface_size[widget_name] wtype = self.surface_type[widget_name] if ww < 0 or wh < 0: logger.warning('Widget {} with invalid size {}x{} when rendering'.format(widget_name, ww, wh)) return GLib.SOURCE_REMOVE with self.doc_lock: page = self.doc.page(page_nb) if page is None: return GLib.SOURCE_REMOVE # Render to a ImageSurface try: surface = self.surface_factory[widget_name](cairo.Format.RGB24, ww, wh) except AttributeError: logger.warning('Widget {} was not mapped when rendering'.format(widget_name), exc_info = True) return GLib.SOURCE_REMOVE except cairo.Error: return GLib.SOURCE_REMOVE context = cairo.Context(surface) page.render_cairo(context, ww, wh, wtype) del context # Save if possible and necessary − using PDF page numbering page_nb = page.page_nb with self.locks[widget_name]: pc = self.surface_cache[widget_name] if (ww, wh) == self.surface_size[widget_name] and page_nb not in pc: pc[page_nb] = surface if widget_name not in self.unlimited: pc.move_to_end(page_nb) while len(pc) > self.max_pages: pc.popitem(False) return GLib.SOURCE_REMOVE
## # Local Variables: # mode: python # indent-tabs-mode: nil # py-indent-offset: 4 # fill-column: 80 # end: