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