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
114In 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
152PDF Tools is build as a series of minor-modes.  This variable and
153the function `pdf-tools-install' merely serve as a convenient
154wrapper in order to load these modes in current and newly created
155PDF 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
216If nil the installer is chosen automatically. This variable is
217useful if you have multiple installers present on your
218system (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
225Returns 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
235Returns 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
246Ask the user if necessary and NONINTERACTIVE-P is nil.
247Returns 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
293Install into TARGET-DIRECTORY, which should be a directory.
294
295If CALLBACK is non-nil, it should be a function.  It is called
296with the compiled executable as the single argument or nil, if
297the build failed.
298
299Expect sources to be in BUILD-DIRECTORY.  If nil, search for it
300using `pdf-tools-locate-build-directory'.
301
302See `pdf-tools-install' for the SKIP-DEPENDENCIES-P and
303FORCE-DEPENDENCIES-P arguments.
304
305Returns 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
368If the `pdf-info-epdfinfo-program' is not running or does not
369appear to be working, attempt to rebuild it.  If this build
370succeeded, continue with the activation of the package.
371Otherwise fail silently, i.e. no error is signaled.
372
373Build the program (if necessary) without asking first, if
374NO-QUERY-P is non-nil.
375
376Don't attempt to install system packages, if SKIP-DEPENDENCIES-P
377is non-nil.
378
379Do not signal an error in case the build failed, if NO-ERROR-P is
380non-nil.
381
382Attempt to install system packages (even if it is deemed
383unnecessary), if FORCE-DEPENDENCIES-P is non-nil.
384
385Note that SKIP-DEPENDENCIES-P and FORCE-DEPENDENCIES-P are
386mutually exclusive.
387
388Note further, that you can influence the installation directory
389by setting `pdf-info-epdfinfo-program' to an appropriate
390value (e.g. ~/bin/epdfinfo) before calling this function.
391
392See `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
482MODES 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
491MODES 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
530