dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

pdf-cache.el (16263B)


      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   (unless pdf-cache--data
     67     (setq pdf-cache--data (make-hash-table))
     68     (add-hook 'pdf-info-close-document-hook 'pdf-cache-clear-data nil t)
     69     (add-hook 'pdf-annot-modified-functions
     70               'pdf-cache--clear-data-of-annotations
     71               nil t)))
     72 
     73 (defun pdf-cache--clear-data-of-annotations (fn)
     74   (apply 'pdf-cache-clear-data-of-pages
     75          (mapcar (lambda (a)
     76                    (cdr (assq 'page a)))
     77                  (funcall fn t))))
     78 
     79 (defun pdf-cache--data-put (key value &optional page)
     80   "Put KEY with VALUE in the cache of PAGE, return value."
     81   (pdf-cache--initialize)
     82   (puthash page (cons (cons key value)
     83                       (assq-delete-all
     84                        key
     85                        (gethash page pdf-cache--data)))
     86            pdf-cache--data)
     87   value)
     88 
     89 (defun pdf-cache--data-get (key &optional page)
     90   "Get value of KEY in the cache of PAGE.
     91 
     92 Returns a cons \(HIT . VALUE\), where HIT is non-nil if KEY was
     93 stored previously for PAGE and VALUE it's value.  Otherwise HIT
     94 is nil and VALUE undefined."
     95   (pdf-cache--initialize)
     96   (let ((elt (assq key (gethash page pdf-cache--data))))
     97     (if elt
     98         (cons t (cdr elt))
     99       (cons nil nil))))
    100 
    101 (defun pdf-cache--data-clear (key &optional page)
    102   (pdf-cache--initialize)
    103   (puthash page
    104            (assq-delete-all key (gethash page pdf-cache--data))
    105            pdf-cache--data)
    106   nil)
    107 
    108 (defun pdf-cache-clear-data-of-pages (&rest pages)
    109   (when pdf-cache--data
    110     (dolist (page pages)
    111       (remhash page pdf-cache--data))))
    112 
    113 (defun pdf-cache-clear-data ()
    114   (interactive)
    115   (when pdf-cache--data
    116     (clrhash pdf-cache--data)))
    117 
    118 (defmacro define-pdf-cache-function (command &optional page-arg-p)
    119   "Define a simple data cache function.
    120 
    121 COMMAND is the name of the command, e.g. number-of-pages.  It
    122 should have a corresponding pdf-info function.  If PAGE-ARG-P is
    123 non-nil, define a one-dimensional cache indexed by the page
    124 number. Otherwise the value is constant for each document, like
    125 e.g. number-of-pages.
    126 
    127 Both args are unevaluated."
    128 
    129   (let ((args (if page-arg-p (list 'page)))
    130         (fn (intern (format "pdf-cache-%s" command)))
    131         (ifn (intern (format "pdf-info-%s" command)))
    132         (doc (format "Cached version of `pdf-info-%s', which see.
    133 
    134 Make sure, not to modify it's return value." command)))
    135     `(defun ,fn ,args
    136        ,doc
    137        (let ((hit-value (pdf-cache--data-get ',command ,(if page-arg-p 'page))))
    138          (if (car hit-value)
    139              (cdr hit-value)
    140            (pdf-cache--data-put
    141             ',command
    142             ,(if page-arg-p
    143                  (list ifn 'page)
    144                (list ifn))
    145             ,(if page-arg-p 'page)))))))
    146 
    147 (define-pdf-cache-function pagelinks t)
    148 (define-pdf-cache-function number-of-pages)
    149 ;; The boundingbox may change if annotations change.
    150 (define-pdf-cache-function boundingbox t)
    151 (define-pdf-cache-function textregions t)
    152 (define-pdf-cache-function pagesize t)
    153 
    154 
    155 ;; * ================================================================== *
    156 ;; * PNG image LRU cache
    157 ;; * ================================================================== *
    158 
    159 (defvar pdf-cache-image-inihibit nil
    160   "Non-nil, if the image cache should be bypassed.")
    161 
    162 (defvar-local pdf-cache--image-cache nil)
    163 
    164 (defmacro pdf-cache--make-image (page width data hash)
    165   `(list ,page ,width ,data ,hash))
    166 (defmacro pdf-cache--image/page (img) `(nth 0 ,img))
    167 (defmacro pdf-cache--image/width (img) `(nth 1 ,img))
    168 (defmacro pdf-cache--image/data (img) `(nth 2 ,img))
    169 (defmacro pdf-cache--image/hash (img) `(nth 3 ,img))
    170 
    171 (defun pdf-cache--image-match (image page min-width &optional max-width hash)
    172   "Match IMAGE with specs.
    173 
    174 IMAGE should be a list as created by `pdf-cache--make-image'.
    175 
    176 Return non-nil, if IMAGE's page is the same as PAGE, it's width
    177 is at least MIN-WIDTH and at most MAX-WIDTH and it's stored
    178 hash-value is `eql' to HASH."
    179   (and (= (pdf-cache--image/page image)
    180           page)
    181        (or (null min-width)
    182            (>= (pdf-cache--image/width image)
    183                min-width))
    184        (or (null max-width)
    185            (<= (pdf-cache--image/width image)
    186                max-width))
    187        (eql (pdf-cache--image/hash image)
    188             hash)))
    189 
    190 (defun pdf-cache-lookup-image (page min-width &optional max-width hash)
    191   "Return PAGE's cached PNG data as a string or nil.
    192 
    193 Does not modify the cache.  See also `pdf-cache-get-image'."
    194   (let ((image (car (cl-member
    195                      (list page min-width max-width hash)
    196                      pdf-cache--image-cache
    197                      :test (lambda (spec image)
    198                              (apply 'pdf-cache--image-match image spec))))))
    199     (and image
    200          (pdf-cache--image/data image))))
    201 
    202 (defun pdf-cache-get-image (page min-width &optional max-width hash)
    203   "Return PAGE's PNG data as a string.
    204 
    205 Return an image of at least MIN-WIDTH and, if non-nil, maximum
    206 width MAX-WIDTH and `eql' hash value.
    207 
    208 Remember that image was recently used.
    209 
    210 Returns nil, if no matching image was found."
    211   (let ((cache pdf-cache--image-cache)
    212         image)
    213     ;; Find it in the cache.
    214     (while (and (setq image (pop cache))
    215                 (not (pdf-cache--image-match
    216                       image page min-width max-width hash))))
    217     ;; Remove it and push it to the front.
    218     (when image
    219       (setq pdf-cache--image-cache
    220             (cons image (delq image pdf-cache--image-cache)))
    221       (pdf-cache--image/data image))))
    222 
    223 (defun pdf-cache-put-image (page width data &optional hash)
    224   "Cache image of PAGE with WIDTH, DATA and HASH.
    225 
    226 DATA should the string of a PNG image of width WIDTH and from
    227 page PAGE in the current buffer.  See `pdf-cache-get-image' for
    228 the HASH argument.
    229 
    230 This function always returns nil."
    231   (unless pdf-cache--image-cache
    232     (add-hook 'pdf-info-close-document-hook 'pdf-cache-clear-images nil t)
    233     (add-hook 'pdf-annot-modified-functions
    234               'pdf-cache--clear-images-of-annotations nil t))
    235   (push (pdf-cache--make-image page width data hash)
    236         pdf-cache--image-cache)
    237   ;; Forget old image(s).
    238   (when (> (length pdf-cache--image-cache)
    239            pdf-cache-image-limit)
    240     (if (> pdf-cache-image-limit 1)
    241         (setcdr (nthcdr (1- pdf-cache-image-limit)
    242                         pdf-cache--image-cache)
    243                 nil)
    244       (setq pdf-cache--image-cache nil)))
    245   nil)
    246 
    247 (defun pdf-cache-clear-images ()
    248   "Clear the image cache."
    249   (setq pdf-cache--image-cache nil))
    250 
    251 (defun pdf-cache-clear-images-if (fn)
    252   "Remove images from the cache according to FN.
    253 
    254 FN should be function accepting 4 Arguments \(PAGE WIDTH DATA
    255 HASH\).  It should return non-nil, if the image should be removed
    256 from the cache."
    257   (setq pdf-cache--image-cache
    258         (cl-remove-if
    259          (lambda (image)
    260            (funcall
    261             fn
    262             (pdf-cache--image/page image)
    263             (pdf-cache--image/width image)
    264             (pdf-cache--image/data image)
    265             (pdf-cache--image/hash image)))
    266          pdf-cache--image-cache)))
    267 
    268 
    269 (defun pdf-cache--clear-images-of-annotations (fn)
    270   (apply 'pdf-cache-clear-images-of-pages
    271          (mapcar (lambda (a)
    272                    (cdr (assq 'page a)))
    273                  (funcall fn t))))
    274 
    275 (defun pdf-cache-clear-images-of-pages (&rest pages)
    276   (pdf-cache-clear-images-if
    277    (lambda (page &rest _) (memq page pages))))
    278 
    279 (defun pdf-cache-renderpage (page min-width &optional max-width)
    280   "Render PAGE according to MIN-WIDTH and MAX-WIDTH.
    281 
    282 Return the PNG data of an image as a string, such that it's width
    283 is at least MIN-WIDTH and, if non-nil, at most MAX-WIDTH.
    284 
    285 If such an image is not available in the cache, call
    286 `pdf-info-renderpage' to create one."
    287   (if pdf-cache-image-inihibit
    288       (pdf-info-renderpage page min-width)
    289     (or (pdf-cache-get-image page min-width max-width)
    290         (let ((data (pdf-info-renderpage page min-width)))
    291           (pdf-cache-put-image page min-width data)
    292           data))))
    293 
    294 (defun pdf-cache-renderpage-text-regions (page width single-line-p
    295                                                &rest selection)
    296   "Render PAGE according to WIDTH, SINGLE-LINE-P and SELECTION.
    297 
    298 See also `pdf-info-renderpage-text-regions' and
    299 `pdf-cache-renderpage'."
    300   (if pdf-cache-image-inihibit
    301       (apply 'pdf-info-renderpage-text-regions
    302              page width single-line-p nil selection)
    303     (let ((hash (sxhash
    304                  (format "%S" (cons 'renderpage-text-regions
    305                                     (cons single-line-p selection))))))
    306       (or (pdf-cache-get-image page width width hash)
    307           (let ((data (apply 'pdf-info-renderpage-text-regions
    308                              page width single-line-p nil selection)))
    309             (pdf-cache-put-image page width data hash)
    310             data)))))
    311 
    312 (defun pdf-cache-renderpage-highlight (page width &rest regions)
    313   "Highlight PAGE according to WIDTH and REGIONS.
    314 
    315 See also `pdf-info-renderpage-highlight' and
    316 `pdf-cache-renderpage'."
    317   (if pdf-cache-image-inihibit
    318       (apply 'pdf-info-renderpage-highlight
    319              page width nil regions)
    320     (let ((hash (sxhash
    321                  (format "%S" (cons 'renderpage-highlight
    322                                     regions)))))
    323       (or (pdf-cache-get-image page width width hash)
    324           (let ((data (apply 'pdf-info-renderpage-highlight
    325                              page width nil regions)))
    326             (pdf-cache-put-image page width data hash)
    327             data)))))
    328 
    329 
    330 ;; * ================================================================== *
    331 ;; * Prefetching images
    332 ;; * ================================================================== *
    333 
    334 (defvar-local pdf-cache--prefetch-pages nil
    335   "Pages to be prefetched.")
    336 
    337 (defvar-local pdf-cache--prefetch-timer nil
    338   "Timer used when prefetching images.")
    339 
    340 (define-minor-mode pdf-cache-prefetch-minor-mode
    341   "Try to load images which will probably be needed in a while."
    342   :group 'pdf-cache
    343   (pdf-cache--prefetch-cancel)
    344   (cond
    345    (pdf-cache-prefetch-minor-mode
    346     (pdf-util-assert-pdf-buffer)
    347     (add-hook 'pre-command-hook 'pdf-cache--prefetch-stop nil t)
    348     ;; FIXME: Disable the time when the buffer is killed or it's
    349     ;; major-mode changes.
    350     (setq pdf-cache--prefetch-timer
    351           (run-with-idle-timer (or pdf-cache-prefetch-delay 1)
    352               t 'pdf-cache--prefetch-start (current-buffer))))
    353    (t
    354     (remove-hook 'pre-command-hook 'pdf-cache--prefetch-stop t))))
    355 
    356 (defun pdf-cache-prefetch-pages-function-default ()
    357   (let ((page (pdf-view-current-page)))
    358     (pdf-util-remove-duplicates
    359      (cl-remove-if-not
    360       (lambda (page)
    361         (and (>= page 1)
    362              (<= page (pdf-cache-number-of-pages))))
    363       (append
    364        ;; +1, -1, +2, -2, ...
    365        (let ((sign 1)
    366              (incr 1))
    367          (mapcar (lambda (_)
    368                    (setq page (+ page (* sign incr))
    369                          sign (- sign)
    370                          incr (1+ incr))
    371                    page)
    372                  (number-sequence 1 16)))
    373        ;; First and last
    374        (list 1 (pdf-cache-number-of-pages))
    375        ;; Links
    376        (mapcar
    377         (apply-partially 'alist-get 'page)
    378         (cl-remove-if-not
    379          (lambda (link) (eq (alist-get 'type link) 'goto-dest))
    380          (pdf-cache-pagelinks
    381           (pdf-view-current-page)))))))))
    382 
    383 (defvar pdf-view-use-scaling)
    384 (defun pdf-cache--prefetch-pages (window image-width)
    385   (when (and (eq window (selected-window))
    386              (pdf-util-pdf-buffer-p))
    387     (let ((page (pop pdf-cache--prefetch-pages)))
    388       (while (and page
    389                   (pdf-cache-lookup-image
    390                    page
    391                    image-width
    392                    (if (not pdf-view-use-scaling)
    393                        image-width
    394                      (* 2 image-width))))
    395         (setq page (pop pdf-cache--prefetch-pages)))
    396       (pdf-util-debug
    397         (when (null page)
    398           (message  "Prefetching done.")))
    399       (when page
    400         (let* ((buffer (current-buffer))
    401                (pdf-info-asynchronous
    402                 (lambda (status data)
    403                   (when (and (null status)
    404                              (eq window
    405                                  (selected-window))
    406                              (eq buffer (window-buffer)))
    407                     (with-current-buffer (window-buffer)
    408                       (when (derived-mode-p 'pdf-view-mode)
    409                         (pdf-cache-put-image
    410                          page image-width data)
    411                         (image-size (pdf-view-create-page page))
    412                         (pdf-util-debug
    413                           (message "Prefetched page %s." page))
    414                         ;; Avoid max-lisp-eval-depth
    415                         (run-with-timer
    416                             0.001 nil 'pdf-cache--prefetch-pages window image-width)))))))
    417           (condition-case err
    418               (pdf-info-renderpage page image-width)
    419             (error
    420              (pdf-cache-prefetch-minor-mode -1)
    421              (signal (car err) (cdr err)))))))))
    422 
    423 (defvar pdf-cache--prefetch-started-p nil
    424   "Guard against multiple prefetch starts.
    425 
    426 Used solely in `pdf-cache--prefetch-start'.")
    427 
    428 (defun pdf-cache--prefetch-start (buffer)
    429   "Start prefetching images in BUFFER."
    430   (when (and pdf-cache-prefetch-minor-mode
    431              (not pdf-cache--prefetch-started-p)
    432              (pdf-util-pdf-buffer-p)
    433              (not isearch-mode)
    434              (null pdf-cache--prefetch-pages)
    435              (eq (window-buffer) buffer)
    436              (fboundp pdf-cache-prefetch-pages-function))
    437     (let* ((pdf-cache--prefetch-started-p t)
    438            (pages (funcall pdf-cache-prefetch-pages-function)))
    439       (setq pdf-cache--prefetch-pages
    440             (butlast pages (max 0 (- (length pages)
    441                                      pdf-cache-image-limit))))
    442       (pdf-cache--prefetch-pages
    443        (selected-window)
    444        (car (pdf-view-desired-image-size))))))
    445 
    446 (defun pdf-cache--prefetch-stop ()
    447   "Stop prefetching images in current buffer."
    448   (setq pdf-cache--prefetch-pages nil))
    449 
    450 (defun pdf-cache--prefetch-cancel ()
    451   "Cancel prefetching images in current buffer."
    452   (pdf-cache--prefetch-stop)
    453   (when pdf-cache--prefetch-timer
    454     (cancel-timer pdf-cache--prefetch-timer))
    455   (setq pdf-cache--prefetch-timer nil))
    456 
    457 (provide 'pdf-cache)
    458 ;;; pdf-cache.el ends here