;;; pdf-links.el --- Handle PDF links. -*- lexical-binding: t -*- ;; Copyright (C) 2013, 2014 Andreas Politz ;; Author: Andreas Politz ;; Keywords: files, multimedia ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; (require 'pdf-info) (require 'pdf-util) (require 'pdf-misc) (require 'pdf-cache) (require 'pdf-isearch) (require 'let-alist) (require 'org) ;;; Code: ;; * ================================================================== * ;; * Customizations ;; * ================================================================== * (defgroup pdf-links nil "Following links in PDF documents." :group 'pdf-tools) (defface pdf-links-read-link '((((background dark)) (:background "red" :foreground "yellow")) (((background light)) (:background "red" :foreground "yellow"))) "Face used to determine the colors when reading links." ;; :group 'pdf-links :group 'pdf-tools-faces) (defcustom pdf-links-read-link-convert-commands '(;;"-font" "FreeMono" "-pointsize" "%P" "-undercolor" "%f" "-fill" "%b" "-draw" "text %X,%Y '%c'") "The commands for the convert program, when decorating links for reading. See `pdf-util-convert' for an explanation of the format. Aside from the description there, two additional escape chars are available. %P -- The scaled font pointsize, i.e. IMAGE-WIDTH * SCALE (See `pdf-links-convert-pointsize-scale'). %c -- String describing the current link key (e.g. AA, AB, etc.)." :group 'pdf-links :type '(repeat string) :link '(variable-link pdf-isearch-convert-commands) :link '(url-link "http://www.imagemagick.org/script/convert.php")) (defcustom pdf-links-convert-pointsize-scale 0.01 "The scale factor for the -pointsize convert command. This determines the relative size of the font, when interactively reading links." :group 'pdf-links :type '(restricted-sexp :match-alternatives ((lambda (x) (and (numberp x) (<= x 1) (>= x 0)))))) (defcustom pdf-links-browse-uri-function 'pdf-links-browse-uri-default "The function for handling uri links. This function should accept one argument, the URI to follow, and do something with it." :group 'pdf-links :type 'function) ;; * ================================================================== * ;; * Minor Mode ;; * ================================================================== * (defvar pdf-links-minor-mode-map (let ((kmap (make-sparse-keymap))) (define-key kmap (kbd "f") 'pdf-links-isearch-link) (define-key kmap (kbd "F") 'pdf-links-action-perform) kmap)) ;;;###autoload (define-minor-mode pdf-links-minor-mode "Handle links in PDF documents.\\ If this mode is enabled, most links in the document may be activated by clicking on them or by pressing \\[pdf-links-action-perform] and selecting one of the displayed keys, or by using isearch limited to links via \\[pdf-links-isearch-link]. \\{pdf-links-minor-mode-map}" :group 'pdf-links (pdf-util-assert-pdf-buffer) (cond (pdf-links-minor-mode (pdf-view-add-hotspot-function 'pdf-links-hotspots-function 0)) (t (pdf-view-remove-hotspot-function 'pdf-links-hotspots-function))) (pdf-view-redisplay t)) (defun pdf-links-hotspots-function (page size) "Create hotspots for links on PAGE using SIZE." (let ((links (pdf-cache-pagelinks page)) (id-fmt "link-%d-%d") (i 0) (pointer 'hand) hotspots) (dolist (l links) (let ((e (pdf-util-scale (cdr (assq 'edges l)) size 'round)) (id (intern (format id-fmt page (cl-incf i))))) (push `((rect . ((,(nth 0 e) . ,(nth 1 e)) . (,(nth 2 e) . ,(nth 3 e)))) ,id (pointer ,pointer help-echo ,(pdf-links-action-to-string l))) hotspots) (local-set-key (vector id 'mouse-1) (lambda nil (interactive "@") (pdf-links-action-perform l))) (local-set-key (vector id t) 'pdf-util-image-map-mouse-event-proxy))) (nreverse hotspots))) (defun pdf-links-action-to-string (link) "Return a string representation of ACTION." (let-alist link (concat (cl-case .type (goto-dest (if (> .page 0) (format "Goto page %d" .page) "Destination not found")) (goto-remote (if (and .filename (file-exists-p .filename)) (format "Goto %sfile '%s'" (if (> .page 0) (format "p.%d of " .page) "") .filename) (format "Link to nonexistent file '%s'" .filename))) (uri (if (> (length .uri) 0) (format "Link to uri '%s'" .uri) (format "Link to empty uri"))) (t (format "Unrecognized link type: %s" .type))) (if (> (length .title) 0) (format " (%s)" .title))))) ;;;###autoload (defun pdf-links-action-perform (link) "Follow LINK, depending on its type. This may turn to another page, switch to another PDF buffer or invoke `pdf-links-browse-uri-function'. Interactively, link is read via `pdf-links-read-link-action'. This function displays characters around the links in the current page and starts reading characters (ignoring case). After a sufficient number of characters have been read, the corresponding link's link is invoked. Additionally, SPC may be used to scroll the current page." (interactive (list (or (pdf-links-read-link-action "Activate link (SPC scrolls): ") (error "No link selected")))) (let-alist link (cl-case .type ((goto-dest goto-remote) (let ((window (selected-window))) (cl-case .type (goto-dest (unless (> .page 0) (error "Link points to nowhere"))) (goto-remote (unless (and .filename (file-exists-p .filename)) (error "Link points to nonexistent file %s" .filename)) (setq window (display-buffer (or (find-buffer-visiting .filename) (find-file-noselect .filename)))))) (with-selected-window window (when (derived-mode-p 'pdf-view-mode) (when (> .page 0) (pdf-view-goto-page .page)) (when .top ;; Showing the tooltip delays displaying the page for ;; some reason (sit-for/redisplay don't help), do it ;; later. (run-with-idle-timer 0.001 nil (lambda () (when (window-live-p window) (with-selected-window window (when (derived-mode-p 'pdf-view-mode) (pdf-util-tooltip-arrow .top))))))))))) (uri (funcall pdf-links-browse-uri-function .uri)) (t (error "Unrecognized link type: %s" .type))) nil)) (defun pdf-links-read-link-action (prompt) "Using PROMPT, interactively read a link-action. See `pdf-links-action-perform' for the interface." (pdf-util-assert-pdf-window) (let* ((links (pdf-cache-pagelinks (pdf-view-current-page))) (keys (pdf-links-read-link-action--create-keys (length links))) (key-strings (mapcar (apply-partially 'apply 'string) keys)) (alist (cl-mapcar 'cons keys links)) (size (pdf-view-image-size)) (colors (pdf-util-face-colors 'pdf-links-read-link pdf-view-dark-minor-mode)) (args (list :foreground (car colors) :background (cdr colors) :formats `((?c . ,(lambda (_edges) (pop key-strings))) (?P . ,(number-to-string (max 1 (* (cdr size) pdf-links-convert-pointsize-scale))))) :commands pdf-links-read-link-convert-commands :apply (pdf-util-scale-relative-to-pixel (mapcar (lambda (l) (cdr (assq 'edges l))) links))))) (unless links (error "No links on this page")) (unwind-protect (let ((image-data (pdf-cache-get-image (pdf-view-current-page) (car size) (car size) 'pdf-links-read-link-action))) (unless image-data (setq image-data (apply 'pdf-util-convert-page args )) (pdf-cache-put-image (pdf-view-current-page) (car size) image-data 'pdf-links-read-link-action)) (pdf-view-display-image (create-image image-data (pdf-view-image-type) t)) (pdf-links-read-link-action--read-chars prompt alist)) (pdf-view-redisplay)))) (defun pdf-links-read-link-action--read-chars (prompt alist) (catch 'done (let (key) (while t (let* ((chars (append (mapcar 'caar alist) (mapcar 'downcase (mapcar 'caar alist)) (list ?\s))) (ch (read-char-choice prompt chars))) (setq ch (upcase ch)) (cond ((= ch ?\s) (when (= (window-vscroll) (image-scroll-up)) (image-scroll-down (window-vscroll)))) (t (setq alist (delq nil (mapcar (lambda (elt) (and (eq ch (caar elt)) (cons (cdar elt) (cdr elt)))) alist)) key (append key (list ch)) prompt (concat prompt (list ch))) (when (= (length alist) 1) (message nil) (throw 'done (cdar alist)))))))))) (defun pdf-links-read-link-action--create-keys (n) (when (> n 0) (let ((len (1+ (floor (log n 26)))) keys) (dotimes (i n) (let (key) (dotimes (_x len) (push (+ (% i 26) ?A) key) (setq i (/ i 26))) (push key keys))) (nreverse keys)))) (defun pdf-links-isearch-link () (interactive) (let* (quit-p (isearch-mode-end-hook (cons (lambda nil (setq quit-p isearch-mode-end-hook-quit)) isearch-mode-end-hook)) (pdf-isearch-filter-matches-function 'pdf-links-isearch-link-filter-matches) (pdf-isearch-narrow-to-page t) (isearch-message-prefix-add "(Links)") pdf-isearch-batch-mode) (isearch-forward) (unless (or quit-p (null pdf-isearch-current-match)) (let* ((page (pdf-view-current-page)) (match (car pdf-isearch-current-match)) (size (pdf-view-image-size)) (links (sort (cl-remove-if (lambda (e) (= 0 (pdf-util-edges-intersection-area (car e) match))) (mapcar (lambda (l) (cons (pdf-util-scale (alist-get 'edges l) size) l)) (pdf-cache-pagelinks page))) (lambda (e1 e2) (> (pdf-util-edges-intersection-area (alist-get 'edges e1) match) (pdf-util-edges-intersection-area (alist-get 'edges e2) match)))))) (unless links (error "No link found at this position")) (pdf-links-action-perform (car links)))))) (defun pdf-links-isearch-link-filter-matches (matches) (let ((links (pdf-util-scale (mapcar (apply-partially 'alist-get 'edges) (pdf-cache-pagelinks (pdf-view-current-page))) (pdf-view-image-size)))) (cl-remove-if-not (lambda (m) (cl-some (lambda (edges) (cl-some (lambda (link) (pdf-util-with-edges (link edges) (let ((area (min (* link-width link-height) (* edges-width edges-height)))) (> (/ (pdf-util-edges-intersection-area edges link) (float area)) 0.5)))) links)) m)) matches))) (defun pdf-links-browse-uri-default (uri) "Open the string URI using Org. Wraps the URI in \[\[ ... \]\] and calls `org-open-link-from-string' on the resulting string." (cl-check-type uri string) (message "Opening `%s' with Org" uri) (cond ((fboundp 'org-link-open-from-string) (org-link-open-from-string (format "[[%s]]" uri))) ;; For Org 9.2 and older ((fboundp 'org-open-link-from-string) (org-open-link-from-string (format "[[%s]]" uri))))) (provide 'pdf-links) ;;; pdf-links.el ends here