config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

pdf-cache.el (17714B)


      1 ;;; pdf-cache.el --- Cache time-critical or frequent epdfinfo queries. -*- lexical-binding:t -*-
      2 
      3 ;; Copyright (C) 2013  Andreas Politz
      4 
      5 ;; Author: Andreas Politz <politza@fh-trier.de>
      6 ;; Keywords: files, doc-view, pdf
      7 
      8 ;; This program is free software; you can redistribute it and/or modify
      9 ;; it under the terms of the GNU General Public License as published by
     10 ;; the Free Software Foundation, either version 3 of the License, or
     11 ;; (at your option) any later version.
     12 
     13 ;; This program is distributed in the hope that it will be useful,
     14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     16 ;; GNU General Public License for more details.
     17 
     18 ;; You should have received a copy of the GNU General Public License
     19 ;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
     20 
     21 ;;; Commentary:
     22 ;;
     23 ;;; Code:
     24 ;;
     25 
     26 (require 'pdf-macs)
     27 (require 'pdf-info)
     28 (require 'pdf-util)
     29 
     30 
     31 ;; * ================================================================== *
     32 ;; * Customiazations
     33 ;; * ================================================================== *
     34 
     35 (defcustom pdf-cache-image-limit 64
     36   "Maximum number of cached PNG images per buffer."
     37   :type 'integer
     38   :group 'pdf-cache
     39   :group 'pdf-view)
     40 
     41 (defcustom pdf-cache-prefetch-delay 0.5
     42   "Idle time in seconds before prefetching images starts."
     43   :group 'pdf-view
     44   :type 'number)
     45 
     46 (defcustom pdf-cache-prefetch-pages-function
     47   'pdf-cache-prefetch-pages-function-default
     48   "A function returning a list of pages to be prefetched.
     49 
     50 It is called with no arguments in the PDF window and should
     51 return a list of page-numbers, determining the pages that should
     52 be prefetched and their order."
     53   :group 'pdf-view
     54   :type 'function)
     55 
     56 
     57 ;; * ================================================================== *
     58 ;; * Simple Value cache
     59 ;; * ================================================================== *
     60 
     61 (defvar-local pdf-cache--data nil)
     62 
     63 (defvar pdf-annot-modified-functions)
     64 
     65 (defun pdf-cache--initialize ()
     66   "Initialize the cache to store document data.
     67 
     68 Note: The cache is only initialized once. After that it needs to
     69 be cleared before this function makes any changes to it. This is
     70 an internal function and not meant to be directly used."
     71   (unless pdf-cache--data
     72     (setq pdf-cache--data (make-hash-table))
     73     (add-hook 'pdf-info-close-document-hook #'pdf-cache-clear-data nil t)
     74     (add-hook 'pdf-annot-modified-functions
     75               #'pdf-cache--clear-data-of-annotations
     76               nil t)))
     77 
     78 (defun pdf-cache--clear-data-of-annotations (fn)
     79   "Clear the data cache when annotations are modified.
     80 
     81 FN is a closure as described in `pdf-annot-modified-functions'.
     82 
     83 Note: This is an internal function and not meant to be directly used."
     84   (apply #'pdf-cache-clear-data-of-pages
     85          (mapcar (lambda (a)
     86                    (cdr (assq 'page a)))
     87                  (funcall fn t))))
     88 
     89 (defun pdf-cache--data-put (key value &optional page)
     90   "Put KEY with VALUE in the cache of PAGE, return value."
     91   (pdf-cache--initialize)
     92   (puthash page (cons (cons key value)
     93                       (assq-delete-all
     94                        key
     95                        (gethash page pdf-cache--data)))
     96            pdf-cache--data)
     97   value)
     98 
     99 (defun pdf-cache--data-get (key &optional page)
    100   "Get value of KEY in the cache of PAGE.
    101 
    102 Returns a cons \(HIT . VALUE\), where HIT is non-nil if KEY was
    103 stored previously for PAGE and VALUE its value.  Otherwise HIT
    104 is nil and VALUE undefined."
    105   (pdf-cache--initialize)
    106   (let ((elt (assq key (gethash page pdf-cache--data))))
    107     (if elt
    108         (cons t (cdr elt))
    109       (cons nil nil))))
    110 
    111 (defun pdf-cache--data-clear (key &optional page)
    112   "Remove KEY from the cache of PAGE."
    113   (pdf-cache--initialize)
    114   (puthash page
    115            (assq-delete-all key (gethash page pdf-cache--data))
    116            pdf-cache--data)
    117   nil)
    118 
    119 (defun pdf-cache-clear-data-of-pages (&rest pages)
    120   "Remove all PAGES from the cache."
    121   (when pdf-cache--data
    122     (dolist (page pages)
    123       (remhash page pdf-cache--data))))
    124 
    125 (defun pdf-cache-clear-data ()
    126   "Remove the entire cache."
    127   (interactive)
    128   (when pdf-cache--data
    129     (clrhash pdf-cache--data)))
    130 
    131 (defmacro define-pdf-cache-function (command &optional page-arg-p)
    132   "Define a simple data cache function.
    133 
    134 COMMAND is the name of the command, e.g. number-of-pages.  It
    135 should have a corresponding pdf-info function.  If PAGE-ARG-P is
    136 non-nil, define a one-dimensional cache indexed by the page
    137 number. Otherwise the value is constant for each document, like
    138 e.g. number-of-pages.
    139 
    140 Both args are unevaluated."
    141 
    142   (let ((args (if page-arg-p (list 'page)))
    143         (fn (intern (format "pdf-cache-%s" command)))
    144         (ifn (intern (format "pdf-info-%s" command)))
    145         (doc (format "Cached version of `pdf-info-%s', which see.
    146 
    147 Make sure, not to modify its return value." command)))
    148     `(defun ,fn ,args
    149        ,doc
    150        (let ((hit-value (pdf-cache--data-get ',command ,(if page-arg-p 'page))))
    151          (if (car hit-value)
    152              (cdr hit-value)
    153            (pdf-cache--data-put
    154             ',command
    155             ,(if page-arg-p
    156                  (list ifn 'page)
    157                (list ifn))
    158             ,(if page-arg-p 'page)))))))
    159 
    160 (define-pdf-cache-function pagelinks t)
    161 (define-pdf-cache-function number-of-pages)
    162 ;; The boundingbox may change if annotations change.
    163 (define-pdf-cache-function boundingbox t)
    164 (define-pdf-cache-function textregions t)
    165 (define-pdf-cache-function pagesize t)
    166 
    167 
    168 ;; * ================================================================== *
    169 ;; * PNG image LRU cache
    170 ;; * ================================================================== *
    171 
    172 (defvar pdf-cache-image-inihibit nil
    173   "Non-nil, if the image cache should be bypassed.")
    174 
    175 (defvar-local pdf-cache--image-cache nil)
    176 
    177 (defmacro pdf-cache--make-image (page width data hash)
    178   "Make the image that we store in the image cache.
    179 
    180 An image is a tuple of PAGE WIDTH DATA HASH."
    181   `(list ,page ,width ,data ,hash))
    182 (defmacro pdf-cache--image/page (img)
    183   "Return the page value for IMG."
    184   `(nth 0 ,img))
    185 (defmacro pdf-cache--image/width (img)
    186   "Return the width value for IMG."
    187   `(nth 1 ,img))
    188 (defmacro pdf-cache--image/data (img)
    189   "Return the data value for IMG."
    190   `(nth 2 ,img))
    191 (defmacro pdf-cache--image/hash (img)
    192   "Return the hash value for IMG."
    193   `(nth 3 ,img))
    194 
    195 (defun pdf-cache--image-match (image page min-width &optional max-width hash)
    196   "Match IMAGE with specs.
    197 
    198 IMAGE should be a list as created by `pdf-cache--make-image'.
    199 
    200 Return non-nil, if IMAGE's page is the same as PAGE, its width
    201 is at least MIN-WIDTH and at most MAX-WIDTH and its stored
    202 hash-value is `eql' to HASH."
    203   (and (= (pdf-cache--image/page image)
    204           page)
    205        (or (null min-width)
    206            (>= (pdf-cache--image/width image)
    207                min-width))
    208        (or (null max-width)
    209            (<= (pdf-cache--image/width image)
    210                max-width))
    211        (eql (pdf-cache--image/hash image)
    212             hash)))
    213 
    214 (defun pdf-cache-lookup-image (page min-width &optional max-width hash)
    215   "Return PAGE's cached PNG data as a string or nil.
    216 
    217 Return an image of at least MIN-WIDTH and, if non-nil, maximum
    218 width MAX-WIDTH and `eql' HASH value.
    219 
    220 Does not modify the cache.  See also `pdf-cache-get-image'."
    221   (let ((image (car (cl-member
    222                      (list page min-width max-width hash)
    223                      pdf-cache--image-cache
    224                      :test (lambda (spec image)
    225                              (apply #'pdf-cache--image-match image spec))))))
    226     (and image
    227          (pdf-cache--image/data image))))
    228 
    229 (defun pdf-cache-get-image (page min-width &optional max-width hash)
    230   "Return PAGE's PNG data as a string.
    231 
    232 Return an image of at least MIN-WIDTH and, if non-nil, maximum
    233 width MAX-WIDTH and `eql' HASH value.
    234 
    235 Remember that image was recently used.
    236 
    237 Returns nil, if no matching image was found."
    238   (let ((cache pdf-cache--image-cache)
    239         image)
    240     ;; Find it in the cache.
    241     (while (and (setq image (pop cache))
    242                 (not (pdf-cache--image-match
    243                       image page min-width max-width hash))))
    244     ;; Remove it and push it to the front.
    245     (when image
    246       (setq pdf-cache--image-cache
    247             (cons image (delq image pdf-cache--image-cache)))
    248       (pdf-cache--image/data image))))
    249 
    250 (defun pdf-cache-put-image (page width data &optional hash)
    251   "Cache image of PAGE with WIDTH, DATA and HASH.
    252 
    253 DATA should the string of a PNG image of width WIDTH and from
    254 page PAGE in the current buffer.  See `pdf-cache-get-image' for
    255 the HASH argument.
    256 
    257 This function always returns nil."
    258   (unless pdf-cache--image-cache
    259     (add-hook 'pdf-info-close-document-hook #'pdf-cache-clear-images nil t)
    260     (add-hook 'pdf-annot-modified-functions
    261               #'pdf-cache--clear-images-of-annotations nil t))
    262   (push (pdf-cache--make-image page width data hash)
    263         pdf-cache--image-cache)
    264   ;; Forget old image(s).
    265   (when (> (length pdf-cache--image-cache)
    266            pdf-cache-image-limit)
    267     (if (> pdf-cache-image-limit 1)
    268         (setcdr (nthcdr (1- pdf-cache-image-limit)
    269                         pdf-cache--image-cache)
    270                 nil)
    271       (setq pdf-cache--image-cache nil)))
    272   nil)
    273 
    274 (defun pdf-cache-clear-images ()
    275   "Clear the image cache."
    276   (setq pdf-cache--image-cache nil))
    277 
    278 (defun pdf-cache-clear-images-if (fn)
    279   "Remove images from the cache according to FN.
    280 
    281 FN should be function accepting 4 Arguments \(PAGE WIDTH DATA
    282 HASH\).  It should return non-nil, if the image should be removed
    283 from the cache."
    284   (setq pdf-cache--image-cache
    285         (cl-remove-if
    286          (lambda (image)
    287            (funcall
    288             fn
    289             (pdf-cache--image/page image)
    290             (pdf-cache--image/width image)
    291             (pdf-cache--image/data image)
    292             (pdf-cache--image/hash image)))
    293          pdf-cache--image-cache)))
    294 
    295 
    296 (defun pdf-cache--clear-images-of-annotations (fn)
    297   "Clear the images cache when annotations are modified.
    298 
    299 FN is a closure as described in `pdf-annot-modified-functions'.
    300 
    301 Note: This is an internal function and not meant to be directly used."
    302   (apply #'pdf-cache-clear-images-of-pages
    303          (mapcar (lambda (a)
    304                    (cdr (assq 'page a)))
    305                  (funcall fn t))))
    306 
    307 (defun pdf-cache-clear-images-of-pages (&rest pages)
    308   "Remove all images of PAGES from the image cache."
    309   (pdf-cache-clear-images-if
    310    (lambda (page &rest _) (memq page pages))))
    311 
    312 (defun pdf-cache-renderpage (page min-width &optional max-width)
    313   "Render PAGE according to MIN-WIDTH and MAX-WIDTH.
    314 
    315 Return the PNG data of an image as a string, such that its width
    316 is at least MIN-WIDTH and, if non-nil, at most MAX-WIDTH.
    317 
    318 If such an image is not available in the cache, call
    319 `pdf-info-renderpage' to create one."
    320   (if pdf-cache-image-inihibit
    321       (pdf-info-renderpage page min-width)
    322     (or (pdf-cache-get-image page min-width max-width)
    323         (let ((data (pdf-info-renderpage page min-width)))
    324           (pdf-cache-put-image page min-width data)
    325           data))))
    326 
    327 (defun pdf-cache-renderpage-text-regions (page width single-line-p
    328                                                &rest selection)
    329   "Render PAGE according to WIDTH, SINGLE-LINE-P and SELECTION.
    330 
    331 See also `pdf-info-renderpage-text-regions' and
    332 `pdf-cache-renderpage'."
    333   (if pdf-cache-image-inihibit
    334       (apply #'pdf-info-renderpage-text-regions
    335              page width single-line-p nil nil selection)
    336     (let ((hash (sxhash
    337                  (format "%S" (cons 'renderpage-text-regions
    338                                     (cons single-line-p selection))))))
    339       (or (pdf-cache-get-image page width width hash)
    340           (let ((data (apply #'pdf-info-renderpage-text-regions
    341                              page width single-line-p nil nil selection)))
    342             (pdf-cache-put-image page width data hash)
    343             data)))))
    344 
    345 (defun pdf-cache-renderpage-highlight (page width &rest regions)
    346   "Highlight PAGE according to WIDTH and REGIONS.
    347 
    348 See also `pdf-info-renderpage-highlight' and
    349 `pdf-cache-renderpage'."
    350   (if pdf-cache-image-inihibit
    351       (apply #'pdf-info-renderpage-highlight
    352              page width nil regions)
    353     (let ((hash (sxhash
    354                  (format "%S" (cons 'renderpage-highlight
    355                                     regions)))))
    356       (or (pdf-cache-get-image page width width hash)
    357           (let ((data (apply #'pdf-info-renderpage-highlight
    358                              page width nil regions)))
    359             (pdf-cache-put-image page width data hash)
    360             data)))))
    361 
    362 
    363 ;; * ================================================================== *
    364 ;; * Prefetching images
    365 ;; * ================================================================== *
    366 
    367 (defvar-local pdf-cache--prefetch-pages nil
    368   "Pages to be prefetched.")
    369 
    370 (defvar-local pdf-cache--prefetch-timer nil
    371   "Timer used when prefetching images.")
    372 
    373 (define-minor-mode pdf-cache-prefetch-minor-mode
    374   "Try to load images which will probably be needed in a while."
    375   :group 'pdf-cache
    376   (pdf-cache--prefetch-cancel)
    377   (cond
    378    (pdf-cache-prefetch-minor-mode
    379     (pdf-util-assert-pdf-buffer)
    380     (add-hook 'pre-command-hook #'pdf-cache--prefetch-stop nil t)
    381     ;; FIXME: Disable the time when the buffer is killed or its
    382     ;; major-mode changes.
    383     (setq pdf-cache--prefetch-timer
    384           (run-with-idle-timer (or pdf-cache-prefetch-delay 1) t
    385                                #'pdf-cache--prefetch-start (current-buffer))))
    386    (t
    387     (remove-hook 'pre-command-hook #'pdf-cache--prefetch-stop t))))
    388 
    389 (defun pdf-cache-prefetch-pages-function-default ()
    390   "The default function to prefetch pages.
    391 
    392 See `pdf-cache-prefetch-pages-function' for an explanation of
    393 what this function does."
    394   (let ((page (pdf-view-current-page)))
    395     (pdf-util-remove-duplicates
    396      (cl-remove-if-not
    397       (lambda (page)
    398         (and (>= page 1)
    399              (<= page (pdf-cache-number-of-pages))))
    400       (append
    401        ;; +1, -1, +2, -2, ...
    402        (let ((sign 1)
    403              (incr 1))
    404          (mapcar (lambda (_)
    405                    (setq page (+ page (* sign incr))
    406                          sign (- sign)
    407                          incr (1+ incr))
    408                    page)
    409                  (number-sequence 1 16)))
    410        ;; First and last
    411        (list 1 (pdf-cache-number-of-pages))
    412        ;; Links
    413        (mapcar
    414         (apply-partially 'alist-get 'page)
    415         (cl-remove-if-not
    416          (lambda (link) (eq (alist-get 'type link) 'goto-dest))
    417          (pdf-cache-pagelinks
    418           (pdf-view-current-page)))))))))
    419 
    420 (defvar pdf-view-use-scaling)
    421 (defun pdf-cache--prefetch-pages (window image-width)
    422   "Internal function to prefetch pages and store them in the cache.
    423 
    424 WINDOW and IMAGE-WIDTH decide the page and scale of the final image."
    425   (when (and (eq window (selected-window))
    426              (pdf-util-pdf-buffer-p))
    427     (let ((page (pop pdf-cache--prefetch-pages)))
    428       (while (and page
    429                   (pdf-cache-lookup-image
    430                    page
    431                    image-width
    432                    (if pdf-view-use-scaling
    433                        (* 2 image-width)
    434                      image-width)))
    435         (setq page (pop pdf-cache--prefetch-pages)))
    436       (pdf-util-debug
    437         (when (null page)
    438           (message  "Prefetching done.")))
    439       (when page
    440         (let* ((buffer (current-buffer))
    441                (pdf-info-asynchronous
    442                 (lambda (status data)
    443                   (when (and (null status)
    444                              (eq window
    445                                  (selected-window))
    446                              (eq buffer (window-buffer)))
    447                     (with-current-buffer (window-buffer)
    448                       (when (derived-mode-p 'pdf-view-mode)
    449                         (pdf-cache-put-image
    450                          page image-width data)
    451                         (image-size (pdf-view-create-page page))
    452                         (pdf-util-debug
    453                           (message "Prefetched page %s." page))
    454                         ;; Avoid max-lisp-eval-depth
    455                         (run-with-timer
    456                          0.001 nil
    457                          #'pdf-cache--prefetch-pages window image-width)))))))
    458           (condition-case err
    459               (pdf-info-renderpage page image-width)
    460             (error
    461              (pdf-cache-prefetch-minor-mode -1)
    462              (signal (car err) (cdr err)))))))))
    463 
    464 (defvar pdf-cache--prefetch-started-p nil
    465   "Guard against multiple prefetch starts.
    466 
    467 Used solely in `pdf-cache--prefetch-start'.")
    468 
    469 (defun pdf-cache--prefetch-start (buffer)
    470   "Start prefetching images in BUFFER."
    471   (when (and pdf-cache-prefetch-minor-mode
    472              (not pdf-cache--prefetch-started-p)
    473              (pdf-util-pdf-buffer-p)
    474              (not isearch-mode)
    475              (null pdf-cache--prefetch-pages)
    476              (eq (window-buffer) buffer)
    477              (fboundp pdf-cache-prefetch-pages-function))
    478     (let* ((pdf-cache--prefetch-started-p t)
    479            (pages (funcall pdf-cache-prefetch-pages-function)))
    480       (setq pdf-cache--prefetch-pages
    481             (butlast pages (max 0 (- (length pages)
    482                                      pdf-cache-image-limit))))
    483       (pdf-cache--prefetch-pages
    484        (selected-window)
    485        (car (pdf-view-desired-image-size))))))
    486 
    487 (defun pdf-cache--prefetch-stop ()
    488   "Stop prefetching images in current buffer."
    489   (setq pdf-cache--prefetch-pages nil))
    490 
    491 (defun pdf-cache--prefetch-cancel ()
    492   "Cancel prefetching images in current buffer."
    493   (pdf-cache--prefetch-stop)
    494   (when pdf-cache--prefetch-timer
    495     (cancel-timer pdf-cache--prefetch-timer))
    496   (setq pdf-cache--prefetch-timer nil))
    497 
    498 (provide 'pdf-cache)
    499 ;;; pdf-cache.el ends here