emacs/org/elpa/pdf-tools-20220823.513/pdf-links.el

380 lines
14 KiB
EmacsLisp
Raw Normal View History

2022-08-25 15:49:25 +00:00
;;; pdf-links.el --- Handle PDF links. -*- lexical-binding: t -*-
;; Copyright (C) 2013, 2014 Andreas Politz
;; Author: Andreas Politz <politza@fh-trier.de>
;; 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 <http://www.gnu.org/licenses/>.
;;; 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.\\<pdf-links-minor-mode-map>
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