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