dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

pdf-tools.el (18867B)


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