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.

376 lines
13 KiB

4 years ago
  1. ;;; pdf-links.el --- Handle PDF links. -*- lexical-binding: t -*-
  2. ;; Copyright (C) 2013, 2014 Andreas Politz
  3. ;; Author: Andreas Politz <politza@fh-trier.de>
  4. ;; Keywords: files, multimedia
  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. (require 'pdf-info)
  18. (require 'pdf-util)
  19. (require 'pdf-misc)
  20. (require 'pdf-cache)
  21. (require 'pdf-isearch)
  22. (require 'let-alist)
  23. (require 'org)
  24. ;;; Code:
  25. ;; * ================================================================== *
  26. ;; * Customizations
  27. ;; * ================================================================== *
  28. (defgroup pdf-links nil
  29. "Following links in PDF documents."
  30. :group 'pdf-tools)
  31. (defface pdf-links-read-link
  32. '((((background dark)) (:background "red" :foreground "yellow"))
  33. (((background light)) (:background "red" :foreground "yellow")))
  34. "Face used to determine the colors when reading links."
  35. ;; :group 'pdf-links
  36. :group 'pdf-tools-faces)
  37. (defcustom pdf-links-read-link-convert-commands
  38. '(;;"-font" "FreeMono"
  39. "-pointsize" "%P"
  40. "-undercolor" "%f"
  41. "-fill" "%b"
  42. "-draw" "text %X,%Y '%c'")
  43. "The commands for the convert program, when decorating links for reading.
  44. See `pdf-util-convert' for an explanation of the format.
  45. Aside from the description there, two additional escape chars are
  46. available.
  47. %P -- The scaled font pointsize, i.e. IMAGE-WIDTH * SCALE (See
  48. `pdf-links-convert-pointsize-scale').
  49. %c -- String describing the current link key (e.g. AA, AB,
  50. etc.)."
  51. :group 'pdf-links
  52. :type '(repeat string)
  53. :link '(variable-link pdf-isearch-convert-commands)
  54. :link '(url-link "http://www.imagemagick.org/script/convert.php"))
  55. (defcustom pdf-links-convert-pointsize-scale 0.01
  56. "The scale factor for the -pointsize convert command.
  57. This determines the relative size of the font, when interactively
  58. reading links."
  59. :group 'pdf-links
  60. :type '(restricted-sexp :match-alternatives
  61. ((lambda (x) (and (numberp x)
  62. (<= x 1)
  63. (>= x 0))))))
  64. (defcustom pdf-links-browse-uri-function
  65. 'pdf-links-browse-uri-default
  66. "The function for handling uri links.
  67. This function should accept one argument, the URI to follow, and
  68. do something with it."
  69. :group 'pdf-links
  70. :type 'function)
  71. ;; * ================================================================== *
  72. ;; * Minor Mode
  73. ;; * ================================================================== *
  74. (defvar pdf-links-minor-mode-map
  75. (let ((kmap (make-sparse-keymap)))
  76. (define-key kmap (kbd "f") 'pdf-links-isearch-link)
  77. (define-key kmap (kbd "F") 'pdf-links-action-perform)
  78. kmap))
  79. ;;;###autoload
  80. (define-minor-mode pdf-links-minor-mode
  81. "Handle links in PDF documents.\\<pdf-links-minor-mode-map>
  82. If this mode is enabled, most links in the document may be
  83. activated by clicking on them or by pressing \\[pdf-links-action-perform] and selecting
  84. one of the displayed keys, or by using isearch limited to
  85. links via \\[pdf-links-isearch-link].
  86. \\{pdf-links-minor-mode-map}"
  87. nil nil nil
  88. :group 'pdf-links
  89. (pdf-util-assert-pdf-buffer)
  90. (cond
  91. (pdf-links-minor-mode
  92. (pdf-view-add-hotspot-function 'pdf-links-hotspots-function 0))
  93. (t
  94. (pdf-view-remove-hotspot-function 'pdf-links-hotspots-function)))
  95. (pdf-view-redisplay t))
  96. (defun pdf-links-hotspots-function (page size)
  97. "Create hotspots for links on PAGE using SIZE."
  98. (let ((links (pdf-cache-pagelinks page))
  99. (id-fmt "link-%d-%d")
  100. (i 0)
  101. (pointer 'hand)
  102. hotspots)
  103. (dolist (l links)
  104. (let ((e (pdf-util-scale
  105. (cdr (assq 'edges l)) size 'round))
  106. (id (intern (format id-fmt page
  107. (cl-incf i)))))
  108. (push `((rect . ((,(nth 0 e) . ,(nth 1 e))
  109. . (,(nth 2 e) . ,(nth 3 e))))
  110. ,id
  111. (pointer
  112. ,pointer
  113. help-echo ,(pdf-links-action-to-string l)))
  114. hotspots)
  115. (local-set-key
  116. (vector id 'mouse-1)
  117. (lambda nil
  118. (interactive "@")
  119. (pdf-links-action-perform l)))
  120. (local-set-key
  121. (vector id t)
  122. 'pdf-util-image-map-mouse-event-proxy)))
  123. (nreverse hotspots)))
  124. (defun pdf-links-action-to-string (link)
  125. "Return a string representation of ACTION."
  126. (let-alist link
  127. (concat
  128. (cl-case .type
  129. (goto-dest
  130. (if (> .page 0)
  131. (format "Goto page %d" .page)
  132. "Destination not found"))
  133. (goto-remote
  134. (if (and .filename (file-exists-p .filename))
  135. (format "Goto %sfile '%s'"
  136. (if (> .page 0)
  137. (format "p.%d of " .page)
  138. "")
  139. .filename)
  140. (format "Link to nonexistent file '%s'" .filename)))
  141. (uri
  142. (if (> (length .uri) 0)
  143. (format "Link to uri '%s'" .uri)
  144. (format "Link to empty uri")))
  145. (t (format "Unrecognized link type: %s" .type)))
  146. (if (> (length .title) 0)
  147. (format " (%s)" .title)))))
  148. ;;;###autoload
  149. (defun pdf-links-action-perform (link)
  150. "Follow LINK, depending on its type.
  151. This may turn to another page, switch to another PDF buffer or
  152. invoke `pdf-links-browse-uri-function'.
  153. Interactively, link is read via `pdf-links-read-link-action'.
  154. This function displays characters around the links in the current
  155. page and starts reading characters (ignoring case). After a
  156. sufficient number of characters have been read, the corresponding
  157. link's link is invoked. Additionally, SPC may be used to
  158. scroll the current page."
  159. (interactive
  160. (list (or (pdf-links-read-link-action "Activate link (SPC scrolls): ")
  161. (error "No link selected"))))
  162. (let-alist link
  163. (cl-case .type
  164. ((goto-dest goto-remote)
  165. (let ((window (selected-window)))
  166. (cl-case .type
  167. (goto-dest
  168. (unless (> .page 0)
  169. (error "Link points to nowhere")))
  170. (goto-remote
  171. (unless (and .filename (file-exists-p .filename))
  172. (error "Link points to nonexistent file %s" .filename))
  173. (setq window (display-buffer
  174. (or (find-buffer-visiting .filename)
  175. (find-file-noselect .filename))))))
  176. (with-selected-window window
  177. (when (derived-mode-p 'pdf-view-mode)
  178. (when (> .page 0)
  179. (pdf-view-goto-page .page))
  180. (when .top
  181. ;; Showing the tooltip delays displaying the page for
  182. ;; some reason (sit-for/redisplay don't help), do it
  183. ;; later.
  184. (run-with-idle-timer 0.001 nil
  185. (lambda ()
  186. (when (window-live-p window)
  187. (with-selected-window window
  188. (when (derived-mode-p 'pdf-view-mode)
  189. (pdf-util-tooltip-arrow .top)))))))))))
  190. (uri
  191. (funcall pdf-links-browse-uri-function .uri))
  192. (t
  193. (error "Unrecognized link type: %s" .type)))
  194. nil))
  195. (defun pdf-links-read-link-action (prompt)
  196. "Using PROMPT, interactively read a link-action.
  197. See `pdf-links-action-perform' for the interface."
  198. (pdf-util-assert-pdf-window)
  199. (let* ((links (pdf-cache-pagelinks
  200. (pdf-view-current-page)))
  201. (keys (pdf-links-read-link-action--create-keys
  202. (length links)))
  203. (key-strings (mapcar (apply-partially 'apply 'string)
  204. keys))
  205. (alist (cl-mapcar 'cons keys links))
  206. (size (pdf-view-image-size))
  207. (colors (pdf-util-face-colors
  208. 'pdf-links-read-link pdf-view-dark-minor-mode))
  209. (args (list
  210. :foreground (car colors)
  211. :background (cdr colors)
  212. :formats
  213. `((?c . ,(lambda (_edges) (pop key-strings)))
  214. (?P . ,(number-to-string
  215. (max 1 (* (cdr size)
  216. pdf-links-convert-pointsize-scale)))))
  217. :commands pdf-links-read-link-convert-commands
  218. :apply (pdf-util-scale-relative-to-pixel
  219. (mapcar (lambda (l) (cdr (assq 'edges l)))
  220. links)))))
  221. (unless links
  222. (error "No links on this page"))
  223. (unwind-protect
  224. (let ((image-data
  225. (pdf-cache-get-image
  226. (pdf-view-current-page)
  227. (car size) (car size) 'pdf-links-read-link-action)))
  228. (unless image-data
  229. (setq image-data (apply 'pdf-util-convert-page args ))
  230. (pdf-cache-put-image
  231. (pdf-view-current-page)
  232. (car size) image-data 'pdf-links-read-link-action))
  233. (pdf-view-display-image
  234. (create-image image-data (pdf-view-image-type) t))
  235. (pdf-links-read-link-action--read-chars prompt alist))
  236. (pdf-view-redisplay))))
  237. (defun pdf-links-read-link-action--read-chars (prompt alist)
  238. (catch 'done
  239. (let (key)
  240. (while t
  241. (let* ((chars (append (mapcar 'caar alist)
  242. (mapcar 'downcase (mapcar 'caar alist))
  243. (list ?\s)))
  244. (ch (read-char-choice prompt chars)))
  245. (setq ch (upcase ch))
  246. (cond
  247. ((= ch ?\s)
  248. (when (= (window-vscroll) (image-scroll-up))
  249. (image-scroll-down (window-vscroll))))
  250. (t
  251. (setq alist (delq nil (mapcar (lambda (elt)
  252. (and (eq ch (caar elt))
  253. (cons (cdar elt)
  254. (cdr elt))))
  255. alist))
  256. key (append key (list ch))
  257. prompt (concat prompt (list ch)))
  258. (when (= (length alist) 1)
  259. (message nil)
  260. (throw 'done (cdar alist))))))))))
  261. (defun pdf-links-read-link-action--create-keys (n)
  262. (when (> n 0)
  263. (let ((len (1+ (floor (log n 26))))
  264. keys)
  265. (dotimes (i n)
  266. (let (key)
  267. (dotimes (_x len)
  268. (push (+ (% i 26) ?A) key)
  269. (setq i (/ i 26)))
  270. (push key keys)))
  271. (nreverse keys))))
  272. (defun pdf-links-isearch-link ()
  273. (interactive)
  274. (let* (quit-p
  275. (isearch-mode-end-hook
  276. (cons (lambda nil
  277. (setq quit-p isearch-mode-end-hook-quit))
  278. isearch-mode-end-hook))
  279. (pdf-isearch-filter-matches-function
  280. 'pdf-links-isearch-link-filter-matches)
  281. (pdf-isearch-narrow-to-page t)
  282. (isearch-message-prefix-add "(Links)")
  283. pdf-isearch-batch-mode)
  284. (isearch-forward)
  285. (unless (or quit-p (null pdf-isearch-current-match))
  286. (let* ((page (pdf-view-current-page))
  287. (match (car pdf-isearch-current-match))
  288. (size (pdf-view-image-size))
  289. (links (sort (cl-remove-if
  290. (lambda (e)
  291. (= 0 (pdf-util-edges-intersection-area (car e) match)))
  292. (mapcar (lambda (l)
  293. (cons (pdf-util-scale (alist-get 'edges l) size)
  294. l))
  295. (pdf-cache-pagelinks page)))
  296. (lambda (e1 e2)
  297. (> (pdf-util-edges-intersection-area
  298. (alist-get 'edges e1) match)
  299. (pdf-util-edges-intersection-area
  300. (alist-get 'edges e2) match))))))
  301. (unless links
  302. (error "No link found at this position"))
  303. (pdf-links-action-perform (car links))))))
  304. (defun pdf-links-isearch-link-filter-matches (matches)
  305. (let ((links (pdf-util-scale
  306. (mapcar (apply-partially 'alist-get 'edges)
  307. (pdf-cache-pagelinks
  308. (pdf-view-current-page)))
  309. (pdf-view-image-size))))
  310. (cl-remove-if-not
  311. (lambda (m)
  312. (cl-some
  313. (lambda (edges)
  314. (cl-some (lambda (link)
  315. (pdf-util-with-edges (link edges)
  316. (let ((area (min (* link-width link-height)
  317. (* edges-width edges-height))))
  318. (> (/ (pdf-util-edges-intersection-area edges link)
  319. (float area)) 0.5))))
  320. links))
  321. m))
  322. matches)))
  323. (defun pdf-links-browse-uri-default (uri)
  324. "Open the string URI using Org.
  325. Wraps the URI in \[\[ ... \]\] and calls `org-open-link-from-string'
  326. on the resulting string."
  327. (cl-check-type uri string)
  328. (message "Opening `%s' with Org" uri)
  329. (org-open-link-from-string (format "[[%s]]" uri)))
  330. (provide 'pdf-links)
  331. ;;; pdf-links.el ends here