|
|
- ;;; pdf-cache.el --- Cache time-critical or frequent epdfinfo queries. -*- lexical-binding:t -*-
-
- ;; Copyright (C) 2013 Andreas Politz
-
- ;; Author: Andreas Politz <politza@fh-trier.de>
- ;; Keywords: files, doc-view, pdf
-
- ;; 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, see <http://www.gnu.org/licenses/>.
-
- ;;; Commentary:
- ;;
- ;;; Code:
- ;;
-
- (require 'pdf-info)
- (require 'pdf-util)
-
- ;; * ================================================================== *
- ;; * Customiazations
- ;; * ================================================================== *
-
- (defcustom pdf-cache-image-limit 64
- "Maximum number of cached PNG images per buffer."
- :type 'integer
- :group 'pdf-cache
- :group 'pdf-view)
-
- (defcustom pdf-cache-prefetch-delay 0.5
- "Idle time in seconds before prefetching images starts."
- :group 'pdf-view
- :type 'number)
-
- (defcustom pdf-cache-prefetch-pages-function
- 'pdf-cache-prefetch-pages-function-default
- "A function returning a list of pages to be prefetched.
-
- It is called with no arguments in the PDF window and should
- return a list of page-numbers, determining the pages that should
- be prefetched and their order."
- :group 'pdf-view
- :type 'function)
-
- ;; * ================================================================== *
- ;; * Simple Value cache
- ;; * ================================================================== *
-
- (defvar-local pdf-cache--data nil)
-
- (defvar pdf-annot-modified-functions)
-
- (defun pdf-cache--initialize ()
- (unless pdf-cache--data
- (setq pdf-cache--data (make-hash-table))
- (add-hook 'pdf-info-close-document-hook 'pdf-cache-clear-data nil t)
- (add-hook 'pdf-annot-modified-functions
- 'pdf-cache--clear-data-of-annotations
- nil t)))
-
- (defun pdf-cache--clear-data-of-annotations (fn)
- (apply 'pdf-cache-clear-data-of-pages
- (mapcar (lambda (a)
- (cdr (assq 'page a)))
- (funcall fn t))))
-
- (defun pdf-cache--data-put (key value &optional page)
- "Put KEY with VALUE in the cache of PAGE, return value."
- (pdf-cache--initialize)
- (puthash page (cons (cons key value)
- (assq-delete-all
- key
- (gethash page pdf-cache--data)))
- pdf-cache--data)
- value)
-
- (defun pdf-cache--data-get (key &optional page)
- "Get value of KEY in the cache of PAGE.
-
- Returns a cons \(HIT . VALUE\), where HIT is non-nil if KEY was
- stored previously for PAGE and VALUE it's value. Otherwise HIT
- is nil and VALUE undefined."
- (pdf-cache--initialize)
- (let ((elt (assq key (gethash page pdf-cache--data))))
- (if elt
- (cons t (cdr elt))
- (cons nil nil))))
-
- (defun pdf-cache--data-clear (key &optional page)
- (pdf-cache--initialize)
- (puthash page
- (assq-delete-all key (gethash page pdf-cache--data))
- pdf-cache--data)
- nil)
-
- (defun pdf-cache-clear-data-of-pages (&rest pages)
- (when pdf-cache--data
- (dolist (page pages)
- (remhash page pdf-cache--data))))
-
- (defun pdf-cache-clear-data ()
- (interactive)
- (when pdf-cache--data
- (clrhash pdf-cache--data)))
-
- (defmacro define-pdf-cache-function (command &optional page-arg-p)
- "Define a simple data cache function.
-
- COMMAND is the name of the command, e.g. number-of-pages. It
- should have a corresponding pdf-info function. If PAGE-ARG-P is
- non-nil, define a one-dimensional cache indexed by the page
- number. Otherwise the value is constant for each document, like
- e.g. number-of-pages.
-
- Both args are unevaluated."
-
- (let ((args (if page-arg-p (list 'page)))
- (fn (intern (format "pdf-cache-%s" command)))
- (ifn (intern (format "pdf-info-%s" command)))
- (doc (format "Cached version of `pdf-info-%s', which see.
-
- Make sure, not to modify it's return value." command)))
- `(defun ,fn ,args
- ,doc
- (let ((hit-value (pdf-cache--data-get ',command ,(if page-arg-p 'page))))
- (if (car hit-value)
- (cdr hit-value)
- (pdf-cache--data-put
- ',command
- ,(if page-arg-p
- (list ifn 'page)
- (list ifn))
- ,(if page-arg-p 'page)))))))
-
- (define-pdf-cache-function pagelinks t)
- (define-pdf-cache-function number-of-pages)
- ;; The boundingbox may change if annotations change.
- (define-pdf-cache-function boundingbox t)
- (define-pdf-cache-function textregions t)
- (define-pdf-cache-function pagesize t)
-
- ;; * ================================================================== *
- ;; * PNG image LRU cache
- ;; * ================================================================== *
-
- (defvar pdf-cache-image-inihibit nil
- "Non-nil, if the image cache should be bypassed.")
-
- (defvar-local pdf-cache--image-cache nil)
-
- (defmacro pdf-cache--make-image (page width data hash)
- `(list ,page ,width ,data ,hash))
- (defmacro pdf-cache--image/page (img) `(nth 0 ,img))
- (defmacro pdf-cache--image/width (img) `(nth 1 ,img))
- (defmacro pdf-cache--image/data (img) `(nth 2 ,img))
- (defmacro pdf-cache--image/hash (img) `(nth 3 ,img))
-
- (defun pdf-cache--image-match (image page min-width &optional max-width hash)
- "Match IMAGE with specs.
-
- IMAGE should be a list as created by `pdf-cache--make-image'.
-
- Return non-nil, if IMAGE's page is the same as PAGE, it's width
- is at least MIN-WIDTH and at most MAX-WIDTH and it's stored
- hash-value is `eql' to HASH."
- (and (= (pdf-cache--image/page image)
- page)
- (or (null min-width)
- (>= (pdf-cache--image/width image)
- min-width))
- (or (null max-width)
- (<= (pdf-cache--image/width image)
- max-width))
- (eql (pdf-cache--image/hash image)
- hash)))
-
- (defun pdf-cache-lookup-image (page min-width &optional max-width hash)
- "Return PAGE's cached PNG data as a string or nil.
-
- Does not modify the cache. See also `pdf-cache-get-image'."
- (let ((image (car (cl-member
- (list page min-width max-width hash)
- pdf-cache--image-cache
- :test (lambda (spec image)
- (apply 'pdf-cache--image-match image spec))))))
- (and image
- (pdf-cache--image/data image))))
-
- (defun pdf-cache-get-image (page min-width &optional max-width hash)
- "Return PAGE's PNG data as a string.
-
- Return an image of at least MIN-WIDTH and, if non-nil, maximum
- width MAX-WIDTH and `eql' hash value.
-
- Remember that image was recently used.
-
- Returns nil, if no matching image was found."
- (let ((cache pdf-cache--image-cache)
- image)
- ;; Find it in the cache.
- (while (and (setq image (pop cache))
- (not (pdf-cache--image-match
- image page min-width max-width hash))))
- ;; Remove it and push it to the front.
- (when image
- (setq pdf-cache--image-cache
- (cons image (delq image pdf-cache--image-cache)))
- (pdf-cache--image/data image))))
-
- (defun pdf-cache-put-image (page width data &optional hash)
- "Cache image of PAGE with WIDTH, DATA and HASH.
-
- DATA should the string of a PNG image of width WIDTH and from
- page PAGE in the current buffer. See `pdf-cache-get-image' for
- the HASH argument.
-
- This function always returns nil."
- (unless pdf-cache--image-cache
- (add-hook 'pdf-info-close-document-hook 'pdf-cache-clear-images nil t)
- (add-hook 'pdf-annot-modified-functions
- 'pdf-cache--clear-images-of-annotations nil t))
- (push (pdf-cache--make-image page width data hash)
- pdf-cache--image-cache)
- ;; Forget old image(s).
- (when (> (length pdf-cache--image-cache)
- pdf-cache-image-limit)
- (if (> pdf-cache-image-limit 1)
- (setcdr (nthcdr (1- pdf-cache-image-limit)
- pdf-cache--image-cache)
- nil)
- (setq pdf-cache--image-cache nil)))
- nil)
-
- (defun pdf-cache-clear-images ()
- "Clear the image cache."
- (setq pdf-cache--image-cache nil))
-
- (defun pdf-cache-clear-images-if (fn)
- "Remove images from the cache according to FN.
-
- FN should be function accepting 4 Arguments \(PAGE WIDTH DATA
- HASH\). It should return non-nil, if the image should be removed
- from the cache."
- (setq pdf-cache--image-cache
- (cl-remove-if
- (lambda (image)
- (funcall
- fn
- (pdf-cache--image/page image)
- (pdf-cache--image/width image)
- (pdf-cache--image/data image)
- (pdf-cache--image/hash image)))
- pdf-cache--image-cache)))
-
-
- (defun pdf-cache--clear-images-of-annotations (fn)
- (apply 'pdf-cache-clear-images-of-pages
- (mapcar (lambda (a)
- (cdr (assq 'page a)))
- (funcall fn t))))
-
- (defun pdf-cache-clear-images-of-pages (&rest pages)
- (pdf-cache-clear-images-if
- (lambda (page &rest _) (memq page pages))))
-
- (defun pdf-cache-renderpage (page min-width &optional max-width)
- "Render PAGE according to MIN-WIDTH and MAX-WIDTH.
-
- Return the PNG data of an image as a string, such that it's width
- is at least MIN-WIDTH and, if non-nil, at most MAX-WIDTH.
-
- If such an image is not available in the cache, call
- `pdf-info-renderpage' to create one."
- (if pdf-cache-image-inihibit
- (pdf-info-renderpage page min-width)
- (or (pdf-cache-get-image page min-width max-width)
- (let ((data (pdf-info-renderpage page min-width)))
- (pdf-cache-put-image page min-width data)
- data))))
-
- (defun pdf-cache-renderpage-text-regions (page width single-line-p
- &rest selection)
- "Render PAGE according to WIDTH, SINGLE-LINE-P and SELECTION.
-
- See also `pdf-info-renderpage-text-regions' and
- `pdf-cache-renderpage'."
- (if pdf-cache-image-inihibit
- (apply 'pdf-info-renderpage-text-regions
- page width single-line-p nil selection)
- (let ((hash (sxhash
- (format "%S" (cons 'renderpage-text-regions
- (cons single-line-p selection))))))
- (or (pdf-cache-get-image page width width hash)
- (let ((data (apply 'pdf-info-renderpage-text-regions
- page width single-line-p nil selection)))
- (pdf-cache-put-image page width data hash)
- data)))))
-
- (defun pdf-cache-renderpage-highlight (page width &rest regions)
- "Highlight PAGE according to WIDTH and REGIONS.
-
- See also `pdf-info-renderpage-highlight' and
- `pdf-cache-renderpage'."
- (if pdf-cache-image-inihibit
- (apply 'pdf-info-renderpage-highlight
- page width nil regions)
- (let ((hash (sxhash
- (format "%S" (cons 'renderpage-highlight
- regions)))))
- (or (pdf-cache-get-image page width width hash)
- (let ((data (apply 'pdf-info-renderpage-highlight
- page width nil regions)))
- (pdf-cache-put-image page width data hash)
- data)))))
-
- ;; * ================================================================== *
- ;; * Prefetching images
- ;; * ================================================================== *
-
- (defvar-local pdf-cache--prefetch-pages nil
- "Pages to be prefetched.")
-
- (defvar-local pdf-cache--prefetch-timer nil
- "Timer used when prefetching images.")
-
- (define-minor-mode pdf-cache-prefetch-minor-mode
- "Try to load images which will probably be needed in a while."
- nil nil nil
- (pdf-cache--prefetch-cancel)
- (cond
- (pdf-cache-prefetch-minor-mode
- (pdf-util-assert-pdf-buffer)
- (add-hook 'pre-command-hook 'pdf-cache--prefetch-stop nil t)
- ;; FIXME: Disable the time when the buffer is killed or it's
- ;; major-mode changes.
- (setq pdf-cache--prefetch-timer
- (run-with-idle-timer (or pdf-cache-prefetch-delay 1)
- t 'pdf-cache--prefetch-start (current-buffer))))
- (t
- (remove-hook 'pre-command-hook 'pdf-cache--prefetch-stop t))))
-
- (defun pdf-cache-prefetch-pages-function-default ()
- (let ((page (pdf-view-current-page)))
- (pdf-util-remove-duplicates
- (cl-remove-if-not
- (lambda (page)
- (and (>= page 1)
- (<= page (pdf-cache-number-of-pages))))
- (append
- ;; +1, -1, +2, -2, ...
- (let ((sign 1)
- (incr 1))
- (mapcar (lambda (_)
- (setq page (+ page (* sign incr))
- sign (- sign)
- incr (1+ incr))
- page)
- (number-sequence 1 16)))
- ;; First and last
- (list 1 (pdf-cache-number-of-pages))
- ;; Links
- (mapcar
- (apply-partially 'alist-get 'page)
- (cl-remove-if-not
- (lambda (link) (eq (alist-get 'type link) 'goto-dest))
- (pdf-cache-pagelinks
- (pdf-view-current-page)))))))))
-
- (defun pdf-cache--prefetch-pages (window image-width)
- (when (and (eq window (selected-window))
- (pdf-util-pdf-buffer-p))
- (let ((page (pop pdf-cache--prefetch-pages)))
- (while (and page
- (pdf-cache-lookup-image
- page
- image-width
- (if (not (pdf-view-use-scaling-p))
- image-width
- (* 2 image-width))))
- (setq page (pop pdf-cache--prefetch-pages)))
- (pdf-util-debug
- (when (null page)
- (message "Prefetching done.")))
- (when page
- (let* ((buffer (current-buffer))
- (pdf-info-asynchronous
- (lambda (status data)
- (when (and (null status)
- (eq window
- (selected-window))
- (eq buffer (window-buffer)))
- (with-current-buffer (window-buffer)
- (when (derived-mode-p 'pdf-view-mode)
- (pdf-cache-put-image
- page image-width data)
- (image-size (pdf-view-create-page page))
- (pdf-util-debug
- (message "Prefetched page %s." page))
- ;; Avoid max-lisp-eval-depth
- (run-with-timer
- 0.001 nil 'pdf-cache--prefetch-pages window image-width)))))))
- (condition-case err
- (pdf-info-renderpage page image-width)
- (error
- (pdf-cache-prefetch-minor-mode -1)
- (signal (car err) (cdr err)))))))))
-
- (defvar pdf-cache--prefetch-started-p nil
- "Guard against multiple prefetch starts.
-
- Used solely in `pdf-cache--prefetch-start'.")
-
- (defun pdf-cache--prefetch-start (buffer)
- "Start prefetching images in BUFFER."
- (when (and pdf-cache-prefetch-minor-mode
- (not pdf-cache--prefetch-started-p)
- (pdf-util-pdf-buffer-p)
- (not isearch-mode)
- (null pdf-cache--prefetch-pages)
- (eq (window-buffer) buffer)
- (fboundp pdf-cache-prefetch-pages-function))
- (let* ((pdf-cache--prefetch-started-p t)
- (pages (funcall pdf-cache-prefetch-pages-function)))
- (setq pdf-cache--prefetch-pages
- (butlast pages (max 0 (- (length pages)
- pdf-cache-image-limit))))
- (pdf-cache--prefetch-pages
- (selected-window)
- (car (pdf-view-desired-image-size))))))
-
- (defun pdf-cache--prefetch-stop ()
- "Stop prefetching images in current buffer."
- (setq pdf-cache--prefetch-pages nil))
-
- (defun pdf-cache--prefetch-cancel ()
- "Cancel prefetching images in current buffer."
- (pdf-cache--prefetch-stop)
- (when pdf-cache--prefetch-timer
- (cancel-timer pdf-cache--prefetch-timer))
- (setq pdf-cache--prefetch-timer nil))
-
- (provide 'pdf-cache)
- ;;; pdf-cache.el ends here
|