config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

pdf-tools.el (19674B)


      1 ;;; pdf-tools.el --- Support library for PDF documents -*- lexical-binding:t -*-
      2 
      3 ;; Copyright (C) 2013, 2014  Andreas Politz
      4 
      5 ;; Author: Andreas Politz <mail@andreas-politz.de>
      6 ;; Maintainer: Vedang Manerikar <vedang.manerikar@gmail.com>
      7 ;; URL: http://github.com/vedang/pdf-tools/
      8 ;; Keywords: files, multimedia
      9 ;; Package: pdf-tools
     10 ;; Version: 1.1.0
     11 ;; Package-Requires: ((emacs "26.3") (tablist "1.0") (let-alist "1.0.4"))
     12 
     13 ;; This program is free software; you can redistribute it and/or modify
     14 ;; it under the terms of the GNU General Public License as published by
     15 ;; the Free Software Foundation, either version 3 of the License, or
     16 ;; (at your option) any later version.
     17 
     18 ;; This program is distributed in the hope that it will be useful,
     19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     21 ;; GNU General Public License for more details.
     22 
     23 ;; You should have received a copy of the GNU General Public License
     24 ;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
     25 
     26 ;;; Commentary:
     27 ;;
     28 ;; PDF Tools is, among other things, a replacement of DocView for PDF
     29 ;; files.  The key difference is, that pages are not prerendered by
     30 ;; e.g. ghostscript and stored in the file-system, but rather created
     31 ;; on-demand and stored in memory.
     32 ;;
     33 ;; Note: This package is built and tested on GNU/Linux systems. It
     34 ;; works on macOS and Windows, but is officially supported only on
     35 ;; GNU/Linux systems. This package will not make macOS or Windows
     36 ;; specific functionality changes, behaviour on these systems is
     37 ;; provided as-is.
     38 ;;
     39 ;; Note: If you ever update it, you need to restart Emacs afterwards.
     40 ;;
     41 ;; To activate the package put
     42 ;;
     43 ;; (pdf-tools-install)
     44 ;;
     45 ;; somewhere in your .emacs.el .
     46 ;;
     47 ;; M-x pdf-tools-help RET
     48 ;;
     49 ;; gives some help on using the package and
     50 ;;
     51 ;; M-x pdf-tools-customize RET
     52 ;;
     53 ;; offers some customization options.
     54 
     55 ;; Features:
     56 ;;
     57 ;; * View
     58 ;;   View PDF documents in a buffer with DocView-like bindings.
     59 ;;
     60 ;; * Isearch
     61 ;;   Interactively search PDF documents like any other buffer. (Though
     62 ;;   there is currently no regexp support.)
     63 ;;
     64 ;; * Follow links
     65 ;;   Click on highlighted links, moving to some part of a different
     66 ;;   page, some external file, a website or any other URI.  Links may
     67 ;;   also be followed by keyboard commands.
     68 ;;
     69 ;; * Annotations
     70 ;;   Display and list text and markup annotations (like underline),
     71 ;;   edit their contents and attributes (e.g. color), move them around,
     72 ;;   delete them or create new ones and then save the modifications
     73 ;;   back to the PDF file.
     74 ;;
     75 ;; * Attachments
     76 ;;   Save files attached to the PDF-file or list them in a Dired buffer.
     77 ;;
     78 ;; * Outline
     79 ;;   Use imenu or a special buffer to examine and navigate the PDF's
     80 ;;   outline.
     81 ;;
     82 ;; * SyncTeX
     83 ;;   Jump from a position on a page directly to the TeX source and
     84 ;;   vice-versa.
     85 ;;
     86 ;; * Misc
     87 ;;    + Display PDF's metadata.
     88 ;;    + Mark a region and kill the text from the PDF.
     89 ;;    + Search for occurrences of a string.
     90 ;;    + Keep track of visited pages via a history.
     91 
     92 ;;; Code:
     93 
     94 (require 'pdf-view)
     95 (require 'pdf-util)
     96 (require 'pdf-info)
     97 (require 'cus-edit)
     98 (require 'compile)
     99 (require 'cl-lib)
    100 (require 'package)
    101 
    102 
    103 
    104 ;; * ================================================================== *
    105 ;; * Customizables
    106 ;; * ================================================================== *
    107 
    108 (defgroup pdf-tools nil
    109   "Support library for PDF documents."
    110   :group 'data)
    111 
    112 (defgroup pdf-tools-faces nil
    113   "Faces determining the colors used in the pdf-tools package.
    114 
    115 In order to customize dark and light colors use
    116 `pdf-tools-customize-faces', or set `custom-face-default-form' to
    117 'all."
    118   :group 'pdf-tools)
    119 
    120 (defconst pdf-tools-modes
    121   '(pdf-history-minor-mode
    122     pdf-isearch-minor-mode
    123     pdf-links-minor-mode
    124     pdf-misc-minor-mode
    125     pdf-outline-minor-mode
    126     pdf-misc-size-indication-minor-mode
    127     pdf-misc-menu-bar-minor-mode
    128     pdf-annot-minor-mode
    129     pdf-sync-minor-mode
    130     pdf-misc-context-menu-minor-mode
    131     pdf-cache-prefetch-minor-mode
    132     pdf-view-auto-slice-minor-mode
    133     pdf-occur-global-minor-mode
    134     pdf-virtual-global-minor-mode))
    135 
    136 (defcustom pdf-tools-enabled-modes
    137   '(pdf-history-minor-mode
    138     pdf-isearch-minor-mode
    139     pdf-links-minor-mode
    140     pdf-misc-minor-mode
    141     pdf-outline-minor-mode
    142     pdf-misc-size-indication-minor-mode
    143     pdf-misc-menu-bar-minor-mode
    144     pdf-annot-minor-mode
    145     pdf-sync-minor-mode
    146     pdf-misc-context-menu-minor-mode
    147     pdf-cache-prefetch-minor-mode
    148     pdf-occur-global-minor-mode)
    149   "A list of automatically enabled minor-modes.
    150 
    151 PDF Tools is build as a series of minor-modes.  This variable and
    152 the function `pdf-tools-install' merely serve as a convenient
    153 wrapper in order to load these modes in current and newly created
    154 PDF buffers."
    155   :group 'pdf-tools
    156   :type `(set ,@(mapcar (lambda (mode)
    157                           `(function-item ,mode))
    158                         pdf-tools-modes)))
    159 
    160 (defcustom pdf-tools-enabled-hook nil
    161   "A hook ran after PDF Tools is enabled in a buffer."
    162   :group 'pdf-tools
    163   :type 'hook)
    164 
    165 (defconst pdf-tools-auto-mode-alist-entry
    166   '("\\.[pP][dD][fF]\\'" . pdf-view-mode)
    167   "The entry to use for `auto-mode-alist'.")
    168 
    169 (defconst pdf-tools-magic-mode-alist-entry
    170   '("%PDF" . pdf-view-mode)
    171   "The entry to use for `magic-mode-alist'.")
    172 
    173 (defun pdf-tools-customize ()
    174   "Customize Pdf Tools."
    175   (interactive)
    176   (customize-group 'pdf-tools))
    177 
    178 (defun pdf-tools-customize-faces ()
    179   "Customize PDF Tool's faces."
    180   (interactive)
    181   (let ((buffer (format "*Customize Group: %s*"
    182                         (custom-unlispify-tag-name 'pdf-tools-faces))))
    183     (when (buffer-live-p (get-buffer buffer))
    184       (with-current-buffer (get-buffer buffer)
    185         (rename-uniquely)))
    186     (customize-group 'pdf-tools-faces)
    187     (with-current-buffer buffer
    188       (set (make-local-variable 'custom-face-default-form) 'all))))
    189 
    190 
    191 ;; * ================================================================== *
    192 ;; * Installation
    193 ;; * ================================================================== *
    194 
    195 ;;;###autoload
    196 (defcustom pdf-tools-handle-upgrades t
    197   "Whether PDF Tools should handle upgrading itself."
    198   :group 'pdf-tools
    199   :type 'boolean)
    200 
    201 (make-obsolete-variable 'pdf-tools-handle-upgrades
    202                         "Not used anymore" "0.90")
    203 
    204 (defconst pdf-tools-directory
    205   (or (and load-file-name
    206            (file-name-directory load-file-name))
    207       default-directory)
    208   "The directory from where this library was first loaded.")
    209 
    210 (defvar pdf-tools-msys2-directory nil)
    211 
    212 (defcustom pdf-tools-installer-os nil
    213   "Specifies which installer to use.
    214 
    215 If nil, the installer is chosen automatically.  This variable is
    216 useful if you have multiple installers present on your
    217 system (e.g. nix on arch linux)"
    218   :group 'pdf-tools
    219   :type '(choice
    220           (const :tag "Choose automatically" nil)
    221           string))
    222 
    223 (defun pdf-tools-identify-build-directory (directory)
    224   "Return non-nil, if DIRECTORY appears to contain the epdfinfo source.
    225 
    226 Returns the expanded directory-name of DIRECTORY or nil."
    227   (setq directory (file-name-as-directory
    228                    (expand-file-name directory)))
    229   (and (file-exists-p (expand-file-name "autobuild" directory))
    230        (file-exists-p (expand-file-name "epdfinfo.c" directory))
    231        directory))
    232 
    233 (defun pdf-tools-locate-build-directory ()
    234   "Attempt to locate a source directory.
    235 
    236 Returns a appropriate directory or nil.  See also
    237 `pdf-tools-identify-build-directory'."
    238   (cl-some #'pdf-tools-identify-build-directory
    239            (list default-directory
    240                  (expand-file-name "build/server" pdf-tools-directory)
    241                  (expand-file-name "server")
    242                  (expand-file-name "server" pdf-tools-directory)
    243                  (expand-file-name "../server" pdf-tools-directory))))
    244 
    245 (defun pdf-tools-msys2-directory (&optional noninteractive-p)
    246   "Locate the Msys2 installation directory.
    247 
    248 Ask the user if necessary and NONINTERACTIVE-P is nil.
    249 Returns always nil, unless `system-type' equals windows-nt."
    250   (cl-labels ((if-msys2-directory (directory)
    251                 (and (stringp directory)
    252                      (file-directory-p directory)
    253                      (file-exists-p
    254                       (expand-file-name "usr/bin/bash.exe" directory))
    255                      directory)))
    256     (when (eq system-type 'windows-nt)
    257       (setq pdf-tools-msys2-directory
    258             (or pdf-tools-msys2-directory
    259                 (cl-some #'if-msys2-directory
    260                          (cl-mapcan
    261                           (lambda (drive)
    262                             (list (format "%c:/msys64" drive)
    263                                   (format "%c:/msys32" drive)))
    264                           (number-sequence ?c ?z)))
    265                 (unless (or noninteractive-p
    266                             (not (y-or-n-p "Do you have Msys2 installed ? ")))
    267                   (if-msys2-directory
    268                    (read-directory-name
    269                     "Please enter Msys2 installation directory: " nil nil t))))))))
    270 
    271 (defun pdf-tools-msys2-mingw-bin ()
    272   "Return the location of /mingw*/bin."
    273   (when (pdf-tools-msys2-directory)
    274     (let ((arch (intern (car (split-string system-configuration "-" t)))))
    275     (expand-file-name
    276      (format "./mingw%s/bin" (if (eq arch 'x86_64) "64" "32"))
    277      (pdf-tools-msys2-directory)))))
    278 
    279 (defun pdf-tools-find-bourne-shell ()
    280   "Locate a usable sh."
    281   (or (and (eq system-type 'windows-nt)
    282            (let* ((directory (pdf-tools-msys2-directory)))
    283              (when directory
    284                (expand-file-name "usr/bin/bash.exe" directory))))
    285       (executable-find "sh")))
    286 
    287 (defun pdf-tools-build-server (target-directory
    288                                &optional
    289                                skip-dependencies-p
    290                                force-dependencies-p
    291                                callback
    292                                build-directory)
    293   "Build the epdfinfo program in the background.
    294 
    295 Install into TARGET-DIRECTORY, which should be a directory.
    296 
    297 If CALLBACK is non-nil, it should be a function.  It is called
    298 with the compiled executable as the single argument or nil, if
    299 the build failed.
    300 
    301 Expect sources to be in BUILD-DIRECTORY.  If nil, search for it
    302 using `pdf-tools-locate-build-directory'.
    303 
    304 See `pdf-tools-install' for the SKIP-DEPENDENCIES-P and
    305 FORCE-DEPENDENCIES-P arguments.
    306 
    307 Returns the buffer of the compilation process."
    308 
    309   (unless callback (setq callback #'ignore))
    310   (unless build-directory
    311     (setq build-directory (pdf-tools-locate-build-directory)))
    312   (cl-check-type target-directory (satisfies file-directory-p))
    313   (setq target-directory (file-name-as-directory
    314                           (expand-file-name target-directory)))
    315   (cl-check-type build-directory (and (not null)
    316                                       (satisfies file-directory-p)))
    317   (when (and skip-dependencies-p force-dependencies-p)
    318     (error "Can't simultaneously skip and force dependencies"))
    319   (let* ((compilation-auto-jump-to-first-error nil)
    320          (compilation-scroll-output t)
    321          (shell-file-name (pdf-tools-find-bourne-shell))
    322          (shell-command-switch "-c")
    323          (process-environment process-environment)
    324          (default-directory build-directory)
    325          (autobuild (shell-quote-argument
    326                      (expand-file-name "autobuild" build-directory)))
    327          (msys2-p (equal "bash.exe" (file-name-nondirectory shell-file-name))))
    328     (unless shell-file-name
    329       (error "No suitable shell found"))
    330     (when msys2-p
    331       (push "BASH_ENV=/etc/profile" process-environment))
    332     (let ((executable
    333            (expand-file-name
    334             (concat "epdfinfo" (and (eq system-type 'windows-nt) ".exe"))
    335             target-directory))
    336           (compilation-buffer
    337            (compilation-start
    338             (format "%s -i %s%s%s"
    339                     autobuild
    340                     (shell-quote-argument target-directory)
    341                     (cond
    342                      (skip-dependencies-p " -D")
    343                      (force-dependencies-p " -d")
    344                      (t ""))
    345                     (if pdf-tools-installer-os (concat " --os " pdf-tools-installer-os) ""))
    346             t)))
    347       ;; In most cases user-input is required, so select the window.
    348       (if (get-buffer-window compilation-buffer)
    349           (select-window (get-buffer-window compilation-buffer))
    350         (pop-to-buffer compilation-buffer))
    351       (with-current-buffer compilation-buffer
    352         (setq-local compilation-error-regexp-alist nil)
    353         (add-hook 'compilation-finish-functions
    354                   (lambda (_buffer status)
    355                     (funcall callback
    356                              (and (equal status "finished\n")
    357                                   executable)))
    358                   nil t)
    359         (current-buffer)))))
    360 
    361 
    362 ;; * ================================================================== *
    363 ;; * Initialization
    364 ;; * ================================================================== *
    365 
    366 ;;;###autoload
    367 (defun pdf-tools-install (&optional no-query-p skip-dependencies-p
    368                                     no-error-p force-dependencies-p)
    369   "Install PDF-Tools in all current and future PDF buffers.
    370 
    371 If the `pdf-info-epdfinfo-program' is not running or does not
    372 appear to be working, attempt to rebuild it.  If this build
    373 succeeded, continue with the activation of the package.
    374 Otherwise fail silently, i.e. no error is signaled.
    375 
    376 Build the program (if necessary) without asking first, if
    377 NO-QUERY-P is non-nil.
    378 
    379 Don't attempt to install system packages, if SKIP-DEPENDENCIES-P
    380 is non-nil.
    381 
    382 Do not signal an error in case the build failed, if NO-ERROR-P is
    383 non-nil.
    384 
    385 Attempt to install system packages (even if it is deemed
    386 unnecessary), if FORCE-DEPENDENCIES-P is non-nil.
    387 
    388 Note that SKIP-DEPENDENCIES-P and FORCE-DEPENDENCIES-P are
    389 mutually exclusive.
    390 
    391 Note further, that you can influence the installation directory
    392 by setting `pdf-info-epdfinfo-program' to an appropriate
    393 value (e.g. ~/bin/epdfinfo) before calling this function.
    394 
    395 See `pdf-view-mode' and `pdf-tools-enabled-modes'."
    396   (interactive)
    397   (if (or (pdf-info-running-p)
    398           (ignore-errors (pdf-info-check-epdfinfo) t))
    399       (pdf-tools-install-noverify)
    400     (let ((target-directory
    401            (or (and (stringp pdf-info-epdfinfo-program)
    402                     (file-name-directory
    403                      pdf-info-epdfinfo-program))
    404                pdf-tools-directory)))
    405       (if (or no-query-p
    406               (y-or-n-p "Need to (re)build the epdfinfo program, do it now ?"))
    407         (pdf-tools-build-server
    408          target-directory
    409          skip-dependencies-p
    410          force-dependencies-p
    411          (lambda (executable)
    412            (let ((msg (format
    413                        "Building the PDF Tools server %s"
    414                        (if executable "succeeded" "failed"))))
    415              (if (not executable)
    416                  (funcall (if no-error-p #'message #'error) "%s" msg)
    417                (message "%s" msg)
    418                (setq pdf-info-epdfinfo-program executable)
    419                (let ((pdf-info-restart-process-p t))
    420                  (pdf-tools-install-noverify))))))
    421         (message "PDF Tools not activated")))))
    422 
    423 (defun pdf-tools-install-noverify ()
    424   "Like `pdf-tools-install', but skip checking `pdf-info-epdfinfo-program'."
    425   (add-to-list 'auto-mode-alist pdf-tools-auto-mode-alist-entry)
    426   (add-to-list 'magic-mode-alist pdf-tools-magic-mode-alist-entry)
    427   ;; FIXME: Generalize this sometime.
    428   (when (memq 'pdf-occur-global-minor-mode
    429               pdf-tools-enabled-modes)
    430     (pdf-occur-global-minor-mode 1))
    431   (when (memq 'pdf-virtual-global-minor-mode
    432               pdf-tools-enabled-modes)
    433     (pdf-virtual-global-minor-mode 1))
    434   (add-hook 'pdf-view-mode-hook #'pdf-tools-enable-minor-modes)
    435   (dolist (buf (buffer-list))
    436     ;; This when check should not be necessary, but somehow dead
    437     ;; buffers are showing up here. See
    438     ;; https://github.com/vedang/pdf-tools/pull/93
    439     (when (buffer-live-p buf)
    440       (with-current-buffer buf
    441         (when (and (not (derived-mode-p 'pdf-view-mode))
    442                    (pdf-tools-pdf-buffer-p)
    443                    (buffer-file-name))
    444           (pdf-view-mode))))))
    445 
    446 (defun pdf-tools-uninstall ()
    447   "Uninstall PDF-Tools in all current and future PDF buffers."
    448   (interactive)
    449   (pdf-info-quit)
    450   (setq-default auto-mode-alist
    451     (remove pdf-tools-auto-mode-alist-entry auto-mode-alist))
    452   (setq-default magic-mode-alist
    453     (remove pdf-tools-magic-mode-alist-entry magic-mode-alist))
    454   (pdf-occur-global-minor-mode -1)
    455   (pdf-virtual-global-minor-mode -1)
    456   (remove-hook 'pdf-view-mode-hook #'pdf-tools-enable-minor-modes)
    457   (dolist (buf (buffer-list))
    458     (with-current-buffer buf
    459       (when (pdf-util-pdf-buffer-p buf)
    460         (pdf-tools-disable-minor-modes pdf-tools-modes)
    461         (normal-mode)))))
    462 
    463 (defun pdf-tools-pdf-buffer-p (&optional buffer)
    464   "Check if the current buffer is a PDF document.
    465 
    466 Optionally, take BUFFER as an argument and check if it is a PDF document."
    467   (save-current-buffer
    468     (when buffer (set-buffer buffer))
    469     (save-excursion
    470       (save-restriction
    471         (widen)
    472         (goto-char 1)
    473         (looking-at "%PDF")))))
    474 
    475 (defun pdf-tools-assert-pdf-buffer (&optional buffer)
    476   "Throw an error if the current BUFFER does not contain a PDF document."
    477   (unless (pdf-tools-pdf-buffer-p buffer)
    478     (error "Buffer does not contain a PDF document")))
    479 
    480 (defun pdf-tools-set-modes-enabled (enable &optional modes)
    481   "Enable/Disable all the pdf-tools modes on the current buffer based on ENABLE.
    482 
    483 Accepts MODES as a optional argument to enable/disable specific modes."
    484   (dolist (m (or modes pdf-tools-enabled-modes))
    485     (let ((enabled-p (and (boundp m)
    486                           (symbol-value m))))
    487       (unless (or (and enabled-p enable)
    488                   (and (not enabled-p) (not enable)))
    489         (funcall m (if enable 1 -1))))))
    490 
    491 ;;;###autoload
    492 (defun pdf-tools-enable-minor-modes (&optional modes)
    493   "Enable MODES in the current buffer.
    494 
    495 MODES defaults to `pdf-tools-enabled-modes'."
    496   (interactive)
    497   (pdf-util-assert-pdf-buffer)
    498   (pdf-tools-set-modes-enabled t modes)
    499   (run-hooks 'pdf-tools-enabled-hook))
    500 
    501 (defun pdf-tools-disable-minor-modes (&optional modes)
    502   "Disable MODES in the current buffer.
    503 
    504 MODES defaults to `pdf-tools-enabled-modes'."
    505   (interactive)
    506   (pdf-tools-set-modes-enabled nil modes))
    507 
    508 (declare-function pdf-occur-global-minor-mode "pdf-occur.el")
    509 (declare-function pdf-virtual-global-minor-mode "pdf-virtual.el")
    510 
    511 ;;;###autoload
    512 (defun pdf-tools-help ()
    513   "Show a Help buffer for `pdf-tools'."
    514   (interactive)
    515   (help-setup-xref (list #'pdf-tools-help)
    516                    (called-interactively-p 'interactive))
    517   (with-help-window (help-buffer)
    518     (princ "PDF Tools Help\n\n")
    519     (princ "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n")
    520     (dolist (m (cons 'pdf-view-mode
    521                      (sort (copy-sequence pdf-tools-modes) #'string<)))
    522       (princ (format "`%s' is " m))
    523       (describe-function-1 m)
    524       (terpri) (terpri)
    525       (princ "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"))))
    526 
    527 
    528 ;; * ================================================================== *
    529 ;; * Debugging
    530 ;; * ================================================================== *
    531 
    532 (defvar pdf-tools-debug nil
    533   "Non-nil, if debugging PDF Tools.")
    534 
    535 (defun pdf-tools-toggle-debug ()
    536   "Turn debugging on/off for pdf-tools."
    537   (interactive)
    538   (setq pdf-tools-debug (not pdf-tools-debug))
    539   (when (called-interactively-p 'any)
    540     (message "Toggled debugging %s" (if pdf-tools-debug "on" "off"))))
    541 
    542 (provide 'pdf-tools)
    543 
    544 ;;; pdf-tools.el ends here