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