Klimi's new dotfiles with stow.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

456 lines
16 KiB

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