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