diff options
Diffstat (limited to 'elpa/pdf-tools-20200512.1524/pdf-cache.el')
-rw-r--r-- | elpa/pdf-tools-20200512.1524/pdf-cache.el | 456 |
1 files changed, 456 insertions, 0 deletions
diff --git a/elpa/pdf-tools-20200512.1524/pdf-cache.el b/elpa/pdf-tools-20200512.1524/pdf-cache.el new file mode 100644 index 0000000..9ed7241 --- /dev/null +++ b/elpa/pdf-tools-20200512.1524/pdf-cache.el @@ -0,0 +1,456 @@ +;;; 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 |