Klimi's new dotfiles with stow.
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 
 
 

456 řádky
16 KiB

;;; 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