diff options
Diffstat (limited to 'elpa/pdf-tools-20200512.1524/pdf-view.el')
-rw-r--r-- | elpa/pdf-tools-20200512.1524/pdf-view.el | 1676 |
1 files changed, 1676 insertions, 0 deletions
diff --git a/elpa/pdf-tools-20200512.1524/pdf-view.el b/elpa/pdf-tools-20200512.1524/pdf-view.el new file mode 100644 index 0000000..433f5da --- /dev/null +++ b/elpa/pdf-tools-20200512.1524/pdf-view.el @@ -0,0 +1,1676 @@ +;;; pdf-view.el --- View PDF documents. -*- lexical-binding:t -*- + +;; Copyright (C) 2013 Andreas Politz + +;; Author: Andreas Politz <politza@fh-trier.de> +;; Keywords: files, doc-view, pdf + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Functions related to viewing PDF documents. + +;;; Code: + +(require 'image-mode) +(require 'pdf-util) +(require 'pdf-info) +(require 'pdf-cache) +(require 'jka-compr) +(require 'bookmark) +(require 'password-cache) + +(declare-function cua-copy-region "cua-base") + + +;; * ================================================================== * +;; * Customizations +;; * ================================================================== * + +(defgroup pdf-view nil + "View PDF documents." + :group 'pdf-tools) + +(defcustom pdf-view-display-size 'fit-width + "The desired size of displayed pages. + +This may be one of `fit-height', `fit-width', `fit-page' or a +number as a scale factor applied to the document's size. Any +other value behaves like `fit-width'." + :group 'pdf-view + :type '(choice number + (const fit-height) + (const fit-width) + (const fit-page))) + +(make-variable-buffer-local 'pdf-view-display-size) + +(defcustom pdf-view-resize-factor 1.25 + "Fractional amount of resizing of one resize command." + :group 'pdf-view + :type 'number) + +(defcustom pdf-view-continuous t + "In Continuous mode reaching the page edge advances to next/previous page. + +When non-nil, scrolling a line upward at the bottom edge of the page +moves to the next page, and scrolling a line downward at the top edge +of the page moves to the previous page." + :type 'boolean + :group 'pdf-view) + +(defcustom pdf-view-bounding-box-margin 0.05 + "Fractional margin used for slicing with the bounding-box." + :group 'pdf-view + :type 'number) + +(defcustom pdf-view-use-imagemagick nil + "Whether imagemagick should be used for rendering. + +This variable has no effect, if imagemagick was not compiled into +Emacs or if imagemagick is the only way to display PNG images. +FIXME: Explain dis-/advantages of imagemagick and png." + :group 'pdf-view + :type 'boolean) + +(defcustom pdf-view-use-scaling nil + "Whether images should be allowed to be scaled for rendering. + +This variable affects both the reuse of higher-resolution images +as lower-resolution ones by down-scaling the image. As well as +the rendering of higher-resolution for high-resolution displays, +if available. + +It has no effect, unless either the imagemagick or image-io +image-format is available." + :group 'pdf-view + :type 'boolean) + +(defface pdf-view-region + '((((background dark)) (:inherit region)) + (((background light)) (:inherit region))) + "Face used to determine the colors of the region." + :group 'pdf-view + :group 'pdf-tools-faces) + +(defface pdf-view-rectangle + '((((background dark)) (:inherit highlight)) + (((background light)) (:inherit highlight))) + "Face used to determine the colors of the highlighted rectangle." + :group 'pdf-view + :group 'pdf-tools-faces) + +(defcustom pdf-view-midnight-colors '("#839496" . "#002b36" ) + "Colors used when `pdf-view-midnight-minor-mode' is activated. + +This should be a cons \(FOREGROUND . BACKGROUND\) of colors." + :group 'pdf-view + :type '(cons (color :tag "Foreground") + (color :tag "Background"))) + +(defcustom pdf-view-change-page-hook nil + "Hook run after changing to another page, but before displaying it. + +See also `pdf-view-before-change-page-hook' and +`pdf-view-after-change-page-hook'." + :group 'pdf-view + :type 'hook) + +(defcustom pdf-view-before-change-page-hook nil + "Hook run before changing to another page. + +See also `pdf-view-change-page-hook' and +`pdf-view-after-change-page-hook'." + :group 'pdf-view + :type 'hook) + +(defcustom pdf-view-after-change-page-hook nil + "Hook run after changing to and displaying another page. + +See also `pdf-view-change-page-hook' and +`pdf-view-before-change-page-hook'." + :group 'pdf-view + :type 'hook) + +(defcustom pdf-view-use-dedicated-register t + "Whether to use dedicated register for PDF positions. + +If this is non-nil, the commands `pdf-view-position-to-register' +and `pdf-view-jump-to-register' use the buffer-local variable +`pdf-view-register-alist' to store resp. retrieve marked +positions. Otherwise the common variable `register-alist' is +used." + :group 'pdf-view + :type 'boolean) + +(defcustom pdf-view-image-relief 0 + "Add a shadow rectangle around the page's image. + +See :relief property in Info node `(elisp) Image Descriptors'." + :group 'pdf-view + :type '(integer :tag "Pixel") + :link '(info-link "(elisp) Image Descriptors")) + +(defcustom pdf-view-max-image-width 4800 + "Maximum width of any image displayed in pixel." + :group 'pdf-view + :type '(integer :tag "Pixel")) + +(defcustom pdf-view-use-unicode-ligther t + "Whether to use unicode symbols in the mode-line + +On some systems finding a font which supports those symbols can +take some time. If you don't want to spend that time waiting and +don't care for a nicer looking mode-line, set this variable to +nil. + +Note, that this option has only an effect when this library is +loaded." + :group 'pdf-view + :type 'boolean) + +(defcustom pdf-view-incompatible-modes + '(linum-mode linum-relative-mode helm-linum-relative-mode + nlinum-mode nlinum-hl-mode nlinum-relative-mode yalinum-mode) + "A list of modes incompatible with `pdf-view-mode'. + +Issue a warning, if one of them is active in a PDF buffer." + :group 'pdf-view + :type '(repeat symbol)) + + +;; * ================================================================== * +;; * Internal variables and macros +;; * ================================================================== * + +(defvar-local pdf-view-active-region nil + "The active region as a list of edges. + +Edge values are relative coordinates.") + +(defvar-local pdf-view--have-rectangle-region nil + "Non-nil if the region is currently rendered as a rectangle. + +This variable is set in `pdf-view-mouse-set-region' and used in +`pdf-view-mouse-extend-region' to determine the right choice +regarding display of the region in the later function.") + +(defvar-local pdf-view--buffer-file-name nil + "Local copy of remote file or nil.") + +(defvar-local pdf-view--server-file-name nil + "The servers notion of this buffer's filename.") + +(defvar-local pdf-view--next-page-timer nil + "Timer used in `pdf-view-next-page-command'.") + +(defvar-local pdf-view--hotspot-functions nil + "Alist of hotspot functions.") + +(defvar-local pdf-view-register-alist nil + "Local, dedicated register for PDF positions.") + +(defmacro pdf-view-current-page (&optional window) + ;;TODO: write documentation! + `(image-mode-window-get 'page ,window)) + +(defmacro pdf-view-current-overlay (&optional window) + ;;TODO: write documentation! + `(image-mode-window-get 'overlay ,window)) + +(defmacro pdf-view-current-image (&optional window) + ;;TODO: write documentation! + `(image-mode-window-get 'image ,window)) + +(defmacro pdf-view-current-slice (&optional window) + ;;TODO: write documentation! + `(image-mode-window-get 'slice ,window)) + +(defmacro pdf-view-current-window-size (&optional window) + ;;TODO: write documentation! + `(image-mode-window-get 'window-size ,window)) + +(defmacro pdf-view-window-needs-redisplay (&optional window) + `(image-mode-window-get 'needs-redisplay ,window)) + +(defun pdf-view-current-pagelabel (&optional window) + (nth (1- (pdf-view-current-page window)) (pdf-info-pagelabels))) + +(defun pdf-view-active-region-p nil + "Return t if there are active regions." + (not (null pdf-view-active-region))) + +(defmacro pdf-view-assert-active-region () + "Signal an error if there are no active regions." + `(unless (pdf-view-active-region-p) + (error "The region is not active"))) + +(defconst pdf-view-have-image-mode-pixel-vscroll + (>= emacs-major-version 27) + "Whether image-mode scrolls vertically by pixels.") + + +;; * ================================================================== * +;; * Major Mode +;; * ================================================================== * + +(defvar pdf-view-mode-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map image-mode-map) + (define-key map (kbd "Q") 'kill-this-buffer) + ;; Navigation in the document + (define-key map (kbd "n") 'pdf-view-next-page-command) + (define-key map (kbd "p") 'pdf-view-previous-page-command) + (define-key map (kbd "<next>") 'forward-page) + (define-key map (kbd "<prior>") 'backward-page) + (define-key map [remap forward-page] 'pdf-view-next-page-command) + (define-key map [remap backward-page] 'pdf-view-previous-page-command) + (define-key map (kbd "SPC") 'pdf-view-scroll-up-or-next-page) + (define-key map (kbd "S-SPC") 'pdf-view-scroll-down-or-previous-page) + (define-key map (kbd "DEL") 'pdf-view-scroll-down-or-previous-page) + (define-key map (kbd "C-n") 'pdf-view-next-line-or-next-page) + (define-key map (kbd "<down>") 'pdf-view-next-line-or-next-page) + (define-key map (kbd "C-p") 'pdf-view-previous-line-or-previous-page) + (define-key map (kbd "<up>") 'pdf-view-previous-line-or-previous-page) + (define-key map (kbd "M-<") 'pdf-view-first-page) + (define-key map (kbd "M->") 'pdf-view-last-page) + (define-key map [remap goto-line] 'pdf-view-goto-page) + (define-key map (kbd "M-g l") 'pdf-view-goto-label) + (define-key map (kbd "RET") 'image-next-line) + ;; Zoom in/out. + (define-key map "+" 'pdf-view-enlarge) + (define-key map "=" 'pdf-view-enlarge) + (define-key map "-" 'pdf-view-shrink) + (define-key map "0" 'pdf-view-scale-reset) + ;; Fit the image to the window + (define-key map "W" 'pdf-view-fit-width-to-window) + (define-key map "H" 'pdf-view-fit-height-to-window) + (define-key map "P" 'pdf-view-fit-page-to-window) + ;; Slicing the image + (define-key map (kbd "s m") 'pdf-view-set-slice-using-mouse) + (define-key map (kbd "s b") 'pdf-view-set-slice-from-bounding-box) + (define-key map (kbd "s r") 'pdf-view-reset-slice) + ;; Reconvert + (define-key map (kbd "C-c C-c") 'doc-view-mode) + (define-key map (kbd "g") 'revert-buffer) + (define-key map (kbd "r") 'revert-buffer) + ;; Region + (define-key map [down-mouse-1] 'pdf-view-mouse-set-region) + (define-key map [M-down-mouse-1] 'pdf-view-mouse-set-region-rectangle) + (define-key map [C-down-mouse-1] 'pdf-view-mouse-extend-region) + (define-key map [remap kill-region] 'pdf-view-kill-ring-save) + (define-key map [remap kill-ring-save] 'pdf-view-kill-ring-save) + (define-key map [remap mark-whole-buffer] 'pdf-view-mark-whole-page) + ;; Other + (define-key map (kbd "C-c C-d") 'pdf-view-dark-minor-mode) + (define-key map (kbd "m") 'pdf-view-position-to-register) + (define-key map (kbd "'") 'pdf-view-jump-to-register) + + (define-key map (kbd "C-c C-i") 'pdf-view-extract-region-image) + ;; Rendering + (define-key map (kbd "C-c C-r m") 'pdf-view-midnight-minor-mode) + (define-key map (kbd "C-c C-r p") 'pdf-view-printer-minor-mode) + map) + "Keymap used by `pdf-view-mode' when displaying a doc as a set of images.") + +(define-derived-mode pdf-view-mode special-mode "PDFView" + "Major mode in PDF buffers. + +PDFView Mode is an Emacs PDF viewer. It displays PDF files as +PNG images in Emacs buffers." + :group 'pdf-view + :abbrev-table nil + :syntax-table nil + ;; Setup a local copy for remote files. + (when (and (or jka-compr-really-do-compress + (let ((file-name-handler-alist nil)) + (not (and buffer-file-name + (file-readable-p buffer-file-name))))) + (pdf-tools-pdf-buffer-p)) + (let ((tempfile (pdf-util-make-temp-file + (concat (if buffer-file-name + (file-name-nondirectory + buffer-file-name) + (buffer-name)) + "-")))) + (write-region nil nil tempfile nil 'no-message) + (setq-local pdf-view--buffer-file-name tempfile))) + ;; Decryption needs to be done before any other function calls into + ;; pdf-info.el (e.g. from the mode-line during redisplay during + ;; waiting for process output). + (pdf-view-decrypt-document) + + ;; Setup scroll functions + (if (boundp 'mwheel-scroll-up-function) ; not --without-x build + (setq-local mwheel-scroll-up-function + #'pdf-view-scroll-up-or-next-page)) + (if (boundp 'mwheel-scroll-down-function) + (setq-local mwheel-scroll-down-function + #'pdf-view-scroll-down-or-previous-page)) + + ;; Clearing overlays + (add-hook 'change-major-mode-hook + (lambda () + (remove-overlays (point-min) (point-max) 'pdf-view t)) + nil t) + (remove-overlays (point-min) (point-max) 'pdf-view t) ;Just in case. + + ;; Setup other local variables. + (setq-local mode-line-position + '(" P" (:eval (number-to-string (pdf-view-current-page))) + ;; Avoid errors during redisplay. + "/" (:eval (or (ignore-errors + (number-to-string (pdf-cache-number-of-pages))) + "???")))) + (setq-local auto-hscroll-mode nil) + (setq-local pdf-view--server-file-name (pdf-view-buffer-file-name)) + ;; High values of scroll-conservatively seem to trigger + ;; some display bug in xdisp.c:try_scrolling . + (setq-local scroll-conservatively 0) + (setq-local cursor-type nil) + (setq-local buffer-read-only t) + (setq-local view-read-only nil) + (setq-local bookmark-make-record-function + 'pdf-view-bookmark-make-record) + (setq-local revert-buffer-function #'pdf-view-revert-buffer) + ;; No auto-save at the moment. + (setq-local buffer-auto-save-file-name nil) + ;; Disable image rescaling. + (when (boundp 'image-scaling-factor) + (setq-local image-scaling-factor 1)) + ;; No undo at the moment. + (unless buffer-undo-list + (buffer-disable-undo)) + ;; Enable transient-mark-mode, so region deactivation when quitting + ;; will work. + (setq-local transient-mark-mode t) + ;; In Emacs >= 24.4, `cua-copy-region' should have been advised when + ;; loading pdf-view.el so as to make it work with + ;; pdf-view-mode. Disable cua-mode if that is not the case. + ;; FIXME: cua-mode is a global minor-mode, but setting cua-mode to + ;; nil seems to do the trick. + (when (and (bound-and-true-p cua-mode) + (version< emacs-version "24.4")) + (setq-local cua-mode nil)) + + (add-hook 'window-configuration-change-hook + 'pdf-view-redisplay-some-windows nil t) + (add-hook 'deactivate-mark-hook 'pdf-view-deactivate-region nil t) + (add-hook 'write-contents-functions + 'pdf-view--write-contents-function nil t) + (add-hook 'kill-buffer-hook 'pdf-view-close-document nil t) + (pdf-view-add-hotspot-function + 'pdf-view-text-regions-hotspots-function -9) + + ;; Keep track of display info + (add-hook 'image-mode-new-window-functions + 'pdf-view-new-window-function nil t) + (image-mode-setup-winprops) + + ;; Issue a warning in the future about incompatible modes. + (run-with-timer 1 nil (lambda (buffer) + (when (buffer-live-p buffer) + (pdf-view-check-incompatible-modes buffer))) + (current-buffer))) + +(unless (version< emacs-version "24.4") + (defun cua-copy-region--pdf-view-advice (&rest _) + "If the current buffer is in `pdf-view' mode, call +`pdf-view-kill-ring-save'." + (when (eq major-mode 'pdf-view-mode) + (pdf-view-kill-ring-save) + t)) + + (advice-add 'cua-copy-region + :before-until + #'cua-copy-region--pdf-view-advice)) + +(defun pdf-view-check-incompatible-modes (&optional buffer) + "Check BUFFER for incompatible modes, maybe issue a warning." + (with-current-buffer (or buffer (current-buffer)) + (let ((modes + (cl-remove-if-not + (lambda (mode) (and (symbolp mode) + (boundp mode) + (symbol-value mode))) + pdf-view-incompatible-modes))) + (when modes + (display-warning + 'pdf-view + (format "These modes are incompatible with `pdf-view-mode', + please deactivate them (or customize pdf-view-incompatible-modes): %s" + (mapconcat #'symbol-name modes ","))))))) + +(defun pdf-view-decrypt-document () + "Read a password, if the document is encrypted and open it." + (interactive) + (when (pdf-info-encrypted-p) + (let ((prompt (format "Enter password for `%s': " + (abbreviate-file-name + (buffer-file-name)))) + (key (concat "/pdf-tools" (buffer-file-name))) + (i 3) + password) + (while (and (> i 0) + (pdf-info-encrypted-p)) + (setq i (1- i)) + (setq password (password-read prompt key)) + (setq prompt "Invalid password, try again: ") + (ignore-errors (pdf-info-open nil password))) + (pdf-info-open nil password) + (password-cache-add key password))) + nil) + +(defun pdf-view-buffer-file-name () + "Return the local filename of the PDF in the current buffer. + +This may be different from variable `buffer-file-name' when +operating on a local copy of a remote file." + (or pdf-view--buffer-file-name + (buffer-file-name))) + +(defun pdf-view--write-contents-function () + "Function for `write-contents-functions' to save the buffer." + (when (pdf-util-pdf-buffer-p) + (let ((tempfile (pdf-info-save pdf-view--server-file-name))) + (unwind-protect + (progn + ;; Order matters here: We need to first copy the new + ;; content (tempfile) to the PDF, and then close the PDF. + ;; Since while closing the file (and freeing its resources + ;; in the process), it may be immediately reopened due to + ;; redisplay happening inside the pdf-info-close function + ;; (while waiting for a response from the process.). + (copy-file tempfile (buffer-file-name) t) + (pdf-info-close pdf-view--server-file-name) + + (when pdf-view--buffer-file-name + (copy-file tempfile pdf-view--buffer-file-name t)) + (clear-visited-file-modtime) + (set-buffer-modified-p nil) + (setq pdf-view--server-file-name + (pdf-view-buffer-file-name)) + t) + (when (file-exists-p tempfile) + (delete-file tempfile)))))) + +(defun pdf-view-revert-buffer (&optional ignore-auto noconfirm) + "Revert buffer while preserving current modes. + +Optional parameters IGNORE-AUTO and NOCONFIRM are defined as in +`revert-buffer'." + (interactive (list (not current-prefix-arg))) + ;; Bind to default so that we can use pdf-view-revert-buffer as + ;; revert-buffer-function. A binding of nil is needed in Emacs 24.3, but in + ;; later versions the semantics that nil means the default function should + ;; not relied upon. + (let ((revert-buffer-function (when (fboundp #'revert-buffer--default) + #'revert-buffer--default)) + (after-revert-hook + (cons #'pdf-info-close + after-revert-hook))) + (prog1 + (revert-buffer ignore-auto noconfirm 'preserve-modes) + (pdf-view-redisplay t)))) + +(defun pdf-view-close-document () + "Return immediately after closing document. + +This function always succeeds. See also `pdf-info-close', which +does not return immediately." + (when (pdf-info-running-p) + (let ((pdf-info-asynchronous 'ignore)) + (ignore-errors + (pdf-info-close))))) + + +;; * ================================================================== * +;; * Scaling +;; * ================================================================== * + +(defun pdf-view-fit-page-to-window () + "Fit PDF to window. + +Choose the larger of PDF's height and width, and fits that +dimension to window." + (interactive) + (setq pdf-view-display-size 'fit-page) + (image-set-window-vscroll 0) + (image-set-window-hscroll 0) + (pdf-view-redisplay t)) + +(defun pdf-view-fit-height-to-window () + "Fit PDF height to window." + (interactive) + (setq pdf-view-display-size 'fit-height) + (image-set-window-vscroll 0) + (pdf-view-redisplay t)) + +(defun pdf-view-fit-width-to-window () + "Fit PDF size to window." + (interactive) + (setq pdf-view-display-size 'fit-width) + (image-set-window-hscroll 0) + (pdf-view-redisplay t)) + +(defun pdf-view-enlarge (factor) + "Enlarge PDF by FACTOR. + +When called interactively, uses the value of +`pdf-view-resize-factor'. + +For example, (pdf-view-enlarge 1.25) increases size by 25%." + (interactive + (list (float pdf-view-resize-factor))) + (let* ((size (pdf-view-image-size)) + (pagesize (pdf-cache-pagesize + (pdf-view-current-page))) + (scale (/ (float (car size)) + (float (car pagesize))))) + (setq pdf-view-display-size + (* factor scale)) + (pdf-view-redisplay t))) + +(defun pdf-view-shrink (factor) + "Shrink PDF by FACTOR. + +When called interactively, uses the value of +`pdf-view-resize-factor'. + +For example, (pdf-view-shrink 1.25) decreases size by 20%." + (interactive + (list (float pdf-view-resize-factor))) + (pdf-view-enlarge (/ 1.0 factor))) + +(defun pdf-view-scale-reset () + "Reset PDF to its default set." + (interactive) + (setq pdf-view-display-size 1.0) + (pdf-view-redisplay t)) + + + +;; * ================================================================== * +;; * Moving by pages and scrolling +;; * ================================================================== * + +(defun pdf-view-goto-page (page &optional window) + "Go to PAGE in PDF. + +If optional parameter WINDOW, go to PAGE in all `pdf-view' +windows." + (interactive + (list (if current-prefix-arg + (prefix-numeric-value current-prefix-arg) + (read-number "Page: ")))) + (unless (and (>= page 1) + (<= page (pdf-cache-number-of-pages))) + (error "No such page: %d" page)) + (unless window + (setq window + (if (pdf-util-pdf-window-p) + (selected-window) + t))) + (save-selected-window + ;; Select the window for the hooks below. + (when (window-live-p window) + (select-window window)) + (let ((changing-p + (not (eq page (pdf-view-current-page window))))) + (when changing-p + (run-hooks 'pdf-view-before-change-page-hook) + (setf (pdf-view-current-page window) page) + (run-hooks 'pdf-view-change-page-hook)) + (when (window-live-p window) + (pdf-view-redisplay window)) + (when changing-p + (pdf-view-deactivate-region) + (force-mode-line-update) + (run-hooks 'pdf-view-after-change-page-hook)))) + nil) + +(defun pdf-view-next-page (&optional n) + "View the next page in the PDF. + +Optional parameter N moves N pages forward." + (interactive "p") + (pdf-view-goto-page (+ (pdf-view-current-page) + (or n 1)))) + +(defun pdf-view-previous-page (&optional n) + "View the previous page in the PDF. + +Optional parameter N moves N pages backward." + (interactive "p") + (pdf-view-next-page (- (or n 1)))) + +(defun pdf-view-next-page-command (&optional n) + "View the next page in the PDF. + +Optional parameter N moves N pages forward. + +This command is a wrapper for `pdf-view-next-page' that will +indicate to the user if they are on the last page and more." + (declare (interactive-only pdf-view-next-page)) + (interactive "p") + (unless n (setq n 1)) + (when (> (+ (pdf-view-current-page) n) + (pdf-cache-number-of-pages)) + (user-error "Last page")) + (when (< (+ (pdf-view-current-page) n) 1) + (user-error "First page")) + (let ((pdf-view-inhibit-redisplay t)) + (pdf-view-goto-page + (+ (pdf-view-current-page) n))) + (force-mode-line-update) + (sit-for 0) + (when pdf-view--next-page-timer + (cancel-timer pdf-view--next-page-timer) + (setq pdf-view--next-page-timer nil)) + (if (or (not (input-pending-p)) + (and (> n 0) + (= (pdf-view-current-page) + (pdf-cache-number-of-pages))) + (and (< n 0) + (= (pdf-view-current-page) 1))) + (pdf-view-redisplay) + (setq pdf-view--next-page-timer + (run-with-idle-timer 0.001 nil 'pdf-view-redisplay (selected-window))))) + +(defun pdf-view-previous-page-command (&optional n) + "View the previous page in the PDF. + +Optional parameter N moves N pages backward. + +This command is a wrapper for `pdf-view-previous-page'." + (declare (interactive-only pdf-view-previous-page)) + (interactive "p") + (with-no-warnings + (pdf-view-next-page-command (- (or n 1))))) + +(defun pdf-view-first-page () + "View the first page." + (interactive) + (pdf-view-goto-page 1)) + +(defun pdf-view-last-page () + "View the last page." + (interactive) + (pdf-view-goto-page (pdf-cache-number-of-pages))) + +(defun pdf-view-scroll-up-or-next-page (&optional arg) + "Scroll page up ARG lines if possible, else go to the next page. + +When `pdf-view-continuous' is non-nil, scrolling upward at the +bottom edge of the page moves to the next page. Otherwise, go to +next page only on typing SPC (ARG is nil)." + (interactive "P") + (if (or pdf-view-continuous (null arg)) + (let ((hscroll (window-hscroll)) + (cur-page (pdf-view-current-page))) + (when (or (= (window-vscroll nil pdf-view-have-image-mode-pixel-vscroll) + (image-scroll-up arg)) + ;; Workaround rounding/off-by-one issues. + (memq pdf-view-display-size + '(fit-height fit-page))) + (pdf-view-next-page) + (when (/= cur-page (pdf-view-current-page)) + (image-bob) + (image-bol 1)) + (image-set-window-hscroll hscroll))) + (image-scroll-up arg))) + +(defun pdf-view-scroll-down-or-previous-page (&optional arg) + "Scroll page down ARG lines if possible, else go to the previous page. + +When `pdf-view-continuous' is non-nil, scrolling downward at the +top edge of the page moves to the previous page. Otherwise, go +to previous page only on typing DEL (ARG is nil)." + (interactive "P") + (if (or pdf-view-continuous (null arg)) + (let ((hscroll (window-hscroll)) + (cur-page (pdf-view-current-page))) + (when (or (= (window-vscroll nil pdf-view-have-image-mode-pixel-vscroll) + (image-scroll-down arg)) + ;; Workaround rounding/off-by-one issues. + (memq pdf-view-display-size + '(fit-height fit-page))) + (pdf-view-previous-page) + (when (/= cur-page (pdf-view-current-page)) + (image-eob) + (image-bol 1)) + (image-set-window-hscroll hscroll))) + (image-scroll-down arg))) + +(defun pdf-view-next-line-or-next-page (&optional arg) + "Scroll upward by ARG lines if possible, else go to the next page. + +When `pdf-view-continuous' is non-nil, scrolling a line upward +at the bottom edge of the page moves to the next page." + (interactive "p") + (if pdf-view-continuous + (let ((hscroll (window-hscroll)) + (cur-page (pdf-view-current-page))) + (when (= (window-vscroll nil pdf-view-have-image-mode-pixel-vscroll) + (image-next-line arg)) + (pdf-view-next-page) + (when (/= cur-page (pdf-view-current-page)) + (image-bob) + (image-bol 1)) + (image-set-window-hscroll hscroll))) + (image-next-line 1))) + +(defun pdf-view-previous-line-or-previous-page (&optional arg) + "Scroll downward by ARG lines if possible, else go to the previous page. + +When `pdf-view-continuous' is non-nil, scrolling a line downward +at the top edge of the page moves to the previous page." + (interactive "p") + (if pdf-view-continuous + (let ((hscroll (window-hscroll)) + (cur-page (pdf-view-current-page))) + (when (= (window-vscroll nil pdf-view-have-image-mode-pixel-vscroll) + (image-previous-line arg)) + (pdf-view-previous-page) + (when (/= cur-page (pdf-view-current-page)) + (image-eob) + (image-bol 1)) + (image-set-window-hscroll hscroll))) + (image-previous-line arg))) + +(defun pdf-view-goto-label (label) + "Go to the page corresponding to LABEL. + +Usually, the label of a document's page is the same as its +displayed page number." + (interactive + (list (let ((labels (pdf-info-pagelabels))) + (completing-read "Goto label: " labels nil t)))) + (let ((index (cl-position label (pdf-info-pagelabels) :test 'equal))) + (unless index + (error "No such label: %s" label)) + (pdf-view-goto-page (1+ index)))) + + +;; * ================================================================== * +;; * Slicing +;; * ================================================================== * + +(defun pdf-view-set-slice (x y width height &optional window) + ;; TODO: add WINDOW to docstring. + "Set the slice of the pages that should be displayed. + +X, Y, WIDTH and HEIGHT should be relative coordinates, i.e. in +\[0;1\]. To reset the slice use `pdf-view-reset-slice'." + (unless (equal (pdf-view-current-slice window) + (list x y width height)) + (setf (pdf-view-current-slice window) + (mapcar (lambda (v) + (max 0 (min 1 v))) + (list x y width height))) + (pdf-view-redisplay window))) + +(defun pdf-view-set-slice-using-mouse () + "Set the slice of the images that should be displayed. + +Set the slice by pressing `mouse-1' at its top-left corner and +dragging it to its bottom-right corner. See also +`pdf-view-set-slice' and `pdf-view-reset-slice'." + (interactive) + (let ((size (pdf-view-image-size)) + x y w h done) + (while (not done) + (let ((e (read-event + (concat "Press mouse-1 at the top-left corner and " + "drag it to the bottom-right corner!")))) + (when (eq (car e) 'drag-mouse-1) + (setq x (car (posn-object-x-y (event-start e)))) + (setq y (cdr (posn-object-x-y (event-start e)))) + (setq w (- (car (posn-object-x-y (event-end e))) x)) + (setq h (- (cdr (posn-object-x-y (event-end e))) y)) + (setq done t)))) + (apply 'pdf-view-set-slice + (pdf-util-scale + (list x y w h) + (cons (/ 1.0 (float (car size))) + (/ 1.0 (float (cdr size)))))))) + +(defun pdf-view-set-slice-from-bounding-box (&optional window) + ;; TODO: add WINDOW to docstring. + "Set the slice from the page's bounding-box. + +The result is that the margins are almost completely cropped, +much more accurate than could be done manually using +`pdf-view-set-slice-using-mouse'. + +See also `pdf-view-bounding-box-margin'." + (interactive) + (let* ((bb (pdf-cache-boundingbox (pdf-view-current-page window))) + (margin (max 0 (or pdf-view-bounding-box-margin 0))) + (slice (list (- (nth 0 bb) + (/ margin 2.0)) + (- (nth 1 bb) + (/ margin 2.0)) + (+ (- (nth 2 bb) (nth 0 bb)) + margin) + (+ (- (nth 3 bb) (nth 1 bb)) + margin)))) + (apply 'pdf-view-set-slice + (append slice (and window (list window)))))) + +(defun pdf-view-reset-slice (&optional window) + ;; TODO: add WINDOW to doctring. + "Reset the current slice. + +After calling this function the whole page will be visible +again." + (interactive) + (when (pdf-view-current-slice window) + (setf (pdf-view-current-slice window) nil) + (pdf-view-redisplay window)) + nil) + +(define-minor-mode pdf-view-auto-slice-minor-mode + "Automatically slice pages according to their bounding boxes. + +See also `pdf-view-set-slice-from-bounding-box'." + nil nil nil + (pdf-util-assert-pdf-buffer) + (cond + (pdf-view-auto-slice-minor-mode + (dolist (win (get-buffer-window-list nil nil t)) + (when (pdf-util-pdf-window-p win) + (pdf-view-set-slice-from-bounding-box win))) + (add-hook 'pdf-view-change-page-hook + 'pdf-view-set-slice-from-bounding-box nil t)) + (t + (remove-hook 'pdf-view-change-page-hook + 'pdf-view-set-slice-from-bounding-box t)))) + + +;; * ================================================================== * +;; * Display +;; * ================================================================== * + +(defvar pdf-view-inhibit-redisplay nil) +(defvar pdf-view-inhibit-hotspots nil) + +(defun pdf-view-image-type () + "Return the image type that should be used. + +The return value is either `imagemagick' (if available and wanted +or if png is not available) or `png'. + +Signal an error, if neither `imagemagick' nor `png' is available. + +See also `pdf-view-use-imagemagick'." + (cond ((and pdf-view-use-imagemagick + (fboundp 'imagemagick-types)) + 'imagemagick) + ((image-type-available-p 'image-io) + 'image-io) + ((image-type-available-p 'png) + 'png) + ((fboundp 'imagemagick-types) + 'imagemagick) + (t + (error "PNG image supported not compiled into Emacs")))) + +(defun pdf-view-use-scaling-p () + "Return t if scaling should be used." + (and (memq (pdf-view-image-type) + '(imagemagick image-io)) + pdf-view-use-scaling)) + +(defmacro pdf-view-create-image (data &rest props) + ;; TODO: add DATA and PROPS to docstring. + "Like `create-image', but with set DATA-P and TYPE arguments." + (declare (indent 1) (debug t)) + (let ((image-data (make-symbol "data"))) + `(let ((,image-data ,data)) + (apply #'create-image ,image-data (pdf-view-image-type) t ,@props + (cl-list* + :relief (or pdf-view-image-relief 0) + (when (and (eq (framep-on-display) 'mac) + (= (pdf-util-frame-scale-factor) 2)) + (list :data-2x ,image-data))))))) + +(defun pdf-view-create-page (page &optional window) + "Create an image of PAGE for display on WINDOW." + (let* ((size (pdf-view-desired-image-size page window)) + (data (pdf-cache-renderpage + page (car size) + (if (not (pdf-view-use-scaling-p)) + (car size) + (* 2 (car size))))) + (hotspots (pdf-view-apply-hotspot-functions + window page size))) + (pdf-view-create-image data + :width (car size) + :map hotspots + :pointer 'arrow))) + +(defun pdf-view-image-size (&optional displayed-p window) + ;; TODO: add WINDOW to docstring. + "Return the size in pixel of the current image. + +If DISPLAYED-P is non-nil, return the size of the displayed +image. These values may be different, if slicing is used." + (if displayed-p + (with-selected-window (or window (selected-window)) + (image-display-size + (image-get-display-property) t)) + (image-size (pdf-view-current-image window) t))) + +(defun pdf-view-image-offset (&optional window) + ;; TODO: add WINDOW to docstring. + "Return the offset of the current image. + +It is equal to \(LEFT . TOP\) of the current slice in pixel." + (let* ((slice (pdf-view-current-slice window))) + (cond + (slice + (pdf-util-scale-relative-to-pixel + (cons (nth 0 slice) (nth 1 slice)) + window)) + (t + (cons 0 0))))) + +(defun pdf-view-display-page (page &optional window) + "Display page PAGE in WINDOW." + (setf (pdf-view-window-needs-redisplay window) nil) + (pdf-view-display-image + (pdf-view-create-page page window) + window)) + +(defun pdf-view-display-image (image &optional window inhibit-slice-p) + ;; TODO: write documentation! + (let ((ol (pdf-view-current-overlay window))) + (when (window-live-p (overlay-get ol 'window)) + (let* ((size (image-size image t)) + (slice (if (not inhibit-slice-p) + (pdf-view-current-slice window))) + (displayed-width (floor + (if slice + (* (nth 2 slice) + (car (image-size image))) + (car (image-size image)))))) + (setf (pdf-view-current-image window) image) + (move-overlay ol (point-min) (point-max)) + ;; In case the window is wider than the image, center the image + ;; horizontally. + (overlay-put ol 'before-string + (when (> (window-width window) + displayed-width) + (propertize " " 'display + `(space :align-to + ,(/ (- (window-width window) + displayed-width) 2))))) + (overlay-put ol 'display + (if slice + (list (cons 'slice + (pdf-util-scale slice size 'round)) + image) + image)) + (let* ((win (overlay-get ol 'window)) + (hscroll (image-mode-window-get 'hscroll win)) + (vscroll (image-mode-window-get 'vscroll win))) + ;; Reset scroll settings, in case they were changed. + (if hscroll (set-window-hscroll win hscroll)) + (if vscroll (set-window-vscroll + win vscroll pdf-view-have-image-mode-pixel-vscroll))))))) + +(defun pdf-view-redisplay (&optional window) + "Redisplay page in WINDOW. + +If WINDOW is t, redisplay pages in all windows." + (unless pdf-view-inhibit-redisplay + (if (not (eq t window)) + (pdf-view-display-page + (pdf-view-current-page window) + window) + (dolist (win (get-buffer-window-list nil nil t)) + (pdf-view-display-page + (pdf-view-current-page win) + win)) + (when (consp image-mode-winprops-alist) + (dolist (window (mapcar #'car image-mode-winprops-alist)) + (unless (or (not (window-live-p window)) + (eq (current-buffer) + (window-buffer window))) + (setf (pdf-view-window-needs-redisplay window) t))))) + (force-mode-line-update))) + +(defun pdf-view-redisplay-pages (&rest pages) + "Redisplay PAGES in all windows." + (pdf-util-assert-pdf-buffer) + (dolist (window (get-buffer-window-list nil nil t)) + (when (memq (pdf-view-current-page window) + pages) + (pdf-view-redisplay window)))) + +(defun pdf-view-maybe-redisplay-resized-windows () + "Redisplay some windows needing redisplay." + (unless (or (numberp pdf-view-display-size) + (pdf-view-active-region-p) + (> (minibuffer-depth) 0)) + (dolist (window (get-buffer-window-list nil nil t)) + (let ((stored (pdf-view-current-window-size window)) + (size (cons (window-width window) + (window-height window)))) + (unless (equal size stored) + (setf (pdf-view-current-window-size window) size) + (unless (or (null stored) + (and (eq pdf-view-display-size 'fit-width) + (eq (car size) (car stored))) + (and (eq pdf-view-display-size 'fit-height) + (eq (cdr size) (cdr stored)))) + (pdf-view-redisplay window))))))) + +(defun pdf-view-redisplay-some-windows () + (pdf-view-maybe-redisplay-resized-windows) + (dolist (window (get-buffer-window-list nil nil t)) + (when (pdf-view-window-needs-redisplay window) + (pdf-view-redisplay window)))) + +(defun pdf-view-new-window-function (winprops) + ;; TODO: write documentation! + ;; (message "New window %s for buf %s" (car winprops) (current-buffer)) + (cl-assert (or (eq t (car winprops)) + (eq (window-buffer (car winprops)) (current-buffer)))) + (let ((ol (image-mode-window-get 'overlay winprops))) + (if ol + (progn + (setq ol (copy-overlay ol)) + ;; `ol' might actually be dead. + (move-overlay ol (point-min) (point-max))) + (setq ol (make-overlay (point-min) (point-max) nil t)) + (overlay-put ol 'pdf-view t)) + (overlay-put ol 'window (car winprops)) + (unless (windowp (car winprops)) + ;; It's a pseudo entry. Let's make sure it's not displayed (the + ;; `window' property is only effective if its value is a window). + (cl-assert (eq t (car winprops))) + (delete-overlay ol)) + (image-mode-window-put 'overlay ol winprops) + ;; Clean up some overlays. + (dolist (ov (overlays-in (point-min) (point-max))) + (when (and (windowp (overlay-get ov 'window)) + (not (window-live-p (overlay-get ov 'window)))) + (delete-overlay ov))) + (when (and (windowp (car winprops)) + (null (image-mode-window-get 'image winprops))) + ;; We're not displaying an image yet, so let's do so. This + ;; happens when the buffer is displayed for the first time. + (with-selected-window (car winprops) + (pdf-view-goto-page + (or (image-mode-window-get 'page t) 1)))))) + +(defun pdf-view-desired-image-size (&optional page window) + ;; TODO: write documentation! + (let* ((pagesize (pdf-cache-pagesize + (or page (pdf-view-current-page window)))) + (slice (pdf-view-current-slice window)) + (width-scale (/ (/ (float (pdf-util-window-pixel-width window)) + (or (nth 2 slice) 1.0)) + (float (car pagesize)))) + (height (- (nth 3 (window-inside-pixel-edges window)) + (nth 1 (window-inside-pixel-edges window)) + 1)) + (height-scale (/ (/ (float height) + (or (nth 3 slice) 1.0)) + (float (cdr pagesize)))) + (scale width-scale)) + (if (numberp pdf-view-display-size) + (setq scale (float pdf-view-display-size)) + (cl-case pdf-view-display-size + (fit-page + (setq scale (min height-scale width-scale))) + (fit-height + (setq scale height-scale)) + (t + (setq scale width-scale)))) + (let ((width (floor (* (car pagesize) scale))) + (height (floor (* (cdr pagesize) scale)))) + (when (> width (max 1 (or pdf-view-max-image-width width))) + (setq width pdf-view-max-image-width + height (* height (/ (float pdf-view-max-image-width) width)))) + (cons (max 1 width) (max 1 height))))) + +(defun pdf-view-text-regions-hotspots-function (page size) + "Return a list of hotspots for text regions on PAGE using SIZE. + +This will display a text cursor, when hovering over them." + (local-set-key [pdf-view-text-region t] + 'pdf-util-image-map-mouse-event-proxy) + (mapcar (lambda (region) + (let ((e (pdf-util-scale region size 'round))) + `((rect . ((,(nth 0 e) . ,(nth 1 e)) + . (,(nth 2 e) . ,(nth 3 e)))) + pdf-view-text-region + (pointer text)))) + (pdf-cache-textregions page))) + +(define-minor-mode pdf-view-dark-minor-mode + "Mode for PDF documents with dark background. + +This tells the various modes to use their face's dark colors." + nil nil nil + (pdf-util-assert-pdf-buffer) + ;; FIXME: This should really be run in a hook. + (when (bound-and-true-p pdf-isearch-active-mode) + (with-no-warnings + (pdf-isearch-redisplay) + (pdf-isearch-message + (if pdf-view-dark-minor-mode "dark mode" "light mode"))))) + +(define-minor-mode pdf-view-printer-minor-mode + "Display the PDF as it would be printed." + nil " Prn" nil + (pdf-util-assert-pdf-buffer) + (let ((enable (lambda () + (pdf-info-setoptions :render/printed t)))) + (cond + (pdf-view-printer-minor-mode + (add-hook 'after-save-hook enable nil t) + (add-hook 'after-revert-hook enable nil t)) + (t + (remove-hook 'after-save-hook enable t) + (remove-hook 'after-revert-hook enable t)))) + (pdf-info-setoptions :render/printed pdf-view-printer-minor-mode) + (pdf-cache-clear-images) + (pdf-view-redisplay t)) + +(define-minor-mode pdf-view-midnight-minor-mode + "Apply a color-filter appropriate for past midnight reading. + +The colors are determined by the variable +`pdf-view-midnight-colors', which see. " + + nil " Mid" nil + (pdf-util-assert-pdf-buffer) + ;; FIXME: Maybe these options should be passed stateless to pdf-info-renderpage ? + (let ((enable (lambda () + (pdf-info-setoptions + :render/foreground (or (car pdf-view-midnight-colors) "black") + :render/background (or (cdr pdf-view-midnight-colors) "white") + :render/usecolors t)))) + (cond + (pdf-view-midnight-minor-mode + (add-hook 'after-save-hook enable nil t) + (add-hook 'after-revert-hook enable nil t) + (funcall enable)) + (t + (remove-hook 'after-save-hook enable t) + (remove-hook 'after-revert-hook enable t) + (pdf-info-setoptions :render/usecolors nil)))) + (pdf-cache-clear-images) + (pdf-view-redisplay t)) + +(when pdf-view-use-unicode-ligther + ;; This check uses an implementation detail, which hopefully gets the + ;; right answer. + (and (fontp (char-displayable-p ?⎙)) + (setcdr (assq 'pdf-view-printer-minor-mode minor-mode-alist) + (list " ⎙" ))) + (and (fontp (char-displayable-p ?🌙)) + (setcdr (assq 'pdf-view-midnight-minor-mode minor-mode-alist) + (list " 🌙" )))) + + +;; * ================================================================== * +;; * Hotspot handling +;; * ================================================================== * + +(defun pdf-view-add-hotspot-function (fn &optional layer) + "Register FN as a hotspot function in the current buffer, using LAYER. + +FN will be called in the PDF buffer with the page-number and the +image size \(WIDTH . HEIGHT\) as arguments. It should return a +list of hotspots applicable to the the :map image-property. + +LAYER determines the order: Functions in a higher LAYER will +supersede hotspots in lower ones." + (push (cons (or layer 0) fn) + pdf-view--hotspot-functions)) + +(defun pdf-view-remove-hotspot-function (fn) + "Unregister FN as a hotspot function in the current buffer." + (setq pdf-view--hotspot-functions + (cl-remove fn pdf-view--hotspot-functions + :key 'cdr))) + +(defun pdf-view-sorted-hotspot-functions () + ;; TODO: write documentation! + (mapcar 'cdr (cl-sort (copy-sequence pdf-view--hotspot-functions) + '> :key 'car))) + +(defun pdf-view-apply-hotspot-functions (window page image-size) + ;; TODO: write documentation! + (unless pdf-view-inhibit-hotspots + (save-selected-window + (when window (select-window window)) + (apply 'nconc + (mapcar (lambda (fn) + (funcall fn page image-size)) + (pdf-view-sorted-hotspot-functions)))))) + + +;; * ================================================================== * +;; * Region +;; * ================================================================== * + +(defun pdf-view--push-mark () + ;; TODO: write documentation! + (let (mark-ring) + (push-mark-command nil)) + (setq deactivate-mark nil)) + +(defun pdf-view-active-region (&optional deactivate-p) + "Return the active region, a list of edges. + +Deactivate the region if DEACTIVATE-P is non-nil." + (pdf-view-assert-active-region) + (prog1 + pdf-view-active-region + (when deactivate-p + (pdf-view-deactivate-region)))) + +(defun pdf-view-deactivate-region () + "Deactivate the region." + (interactive) + (when pdf-view-active-region + (setq pdf-view-active-region nil) + (deactivate-mark) + (pdf-view-redisplay t))) + +(defun pdf-view-mouse-set-region (event &optional allow-extend-p + rectangle-p) + "Select a region of text using the mouse with mouse event EVENT. + +Allow for stacking of regions, if ALLOW-EXTEND-P is non-nil. + +Create a rectangular region, if RECTANGLE-P is non-nil. + +Stores the region in `pdf-view-active-region'." + (interactive "@e") + (setq pdf-view--have-rectangle-region rectangle-p) + (unless (and (eventp event) + (mouse-event-p event)) + (signal 'wrong-type-argument (list 'mouse-event-p event))) + (unless (and allow-extend-p + (or (null (get this-command 'pdf-view-region-window)) + (equal (get this-command 'pdf-view-region-window) + (selected-window)))) + (pdf-view-deactivate-region)) + (put this-command 'pdf-view-region-window + (selected-window)) + (let* ((window (selected-window)) + (pos (event-start event)) + (begin-inside-image-p t) + (begin (if (posn-image pos) + (posn-object-x-y pos) + (setq begin-inside-image-p nil) + (posn-x-y pos))) + (abs-begin (posn-x-y pos)) + pdf-view-continuous + region) + (when (pdf-util-track-mouse-dragging (event 0.05) + (let* ((pos (event-start event)) + (end (posn-object-x-y pos)) + (end-inside-image-p + (and (eq window (posn-window pos)) + (posn-image pos)))) + (when (or end-inside-image-p + begin-inside-image-p) + (cond + ((and end-inside-image-p + (not begin-inside-image-p)) + ;; Started selection outside the image, setup begin. + (let* ((xy (posn-x-y pos)) + (dxy (cons (- (car xy) (car begin)) + (- (cdr xy) (cdr begin)))) + (size (pdf-view-image-size t))) + (setq begin (cons (max 0 (min (car size) + (- (car end) (car dxy)))) + (max 0 (min (cdr size) + (- (cdr end) (cdr dxy))))) + ;; Store absolute position for later. + abs-begin (cons (- (car xy) + (- (car end) + (car begin))) + (- (cdr xy) + (- (cdr end) + (cdr begin)))) + begin-inside-image-p t))) + ((and begin-inside-image-p + (not end-inside-image-p)) + ;; Moved outside the image, setup end. + (let* ((xy (posn-x-y pos)) + (dxy (cons (- (car xy) (car abs-begin)) + (- (cdr xy) (cdr abs-begin)))) + (size (pdf-view-image-size t))) + (setq end (cons (max 0 (min (car size) + (+ (car begin) (car dxy)))) + (max 0 (min (cdr size) + (+ (cdr begin) (cdr dxy))))))))) + (let ((iregion (if rectangle-p + (list (min (car begin) (car end)) + (min (cdr begin) (cdr end)) + (max (car begin) (car end)) + (max (cdr begin) (cdr end))) + (list (car begin) (cdr begin) + (car end) (cdr end))))) + (setq region + (pdf-util-scale-pixel-to-relative iregion)) + (pdf-view-display-region + (cons region pdf-view-active-region) + rectangle-p) + (pdf-util-scroll-to-edges iregion))))) + (setq pdf-view-active-region + (append pdf-view-active-region + (list region))) + (pdf-view--push-mark)))) + +(defun pdf-view-mouse-extend-region (event) + "Extend the currently active region with mouse event EVENT." + (interactive "@e") + (pdf-view-mouse-set-region + event t pdf-view--have-rectangle-region)) + +(defun pdf-view-mouse-set-region-rectangle (event) + "Like `pdf-view-mouse-set-region' but displays as a rectangle. + +EVENT is the mouse event. + +This is more useful for commands like +`pdf-view-extract-region-image'." + (interactive "@e") + (pdf-view-mouse-set-region event nil t)) + +(defun pdf-view-display-region (&optional region rectangle-p) + ;; TODO: write documentation! + (unless region + (pdf-view-assert-active-region) + (setq region pdf-view-active-region)) + (let ((colors (pdf-util-face-colors + (if rectangle-p 'pdf-view-rectangle 'pdf-view-region) + (bound-and-true-p pdf-view-dark-minor-mode))) + (page (pdf-view-current-page)) + (width (car (pdf-view-image-size)))) + (pdf-view-display-image + (pdf-view-create-image + (if rectangle-p + (pdf-info-renderpage-highlight + page width nil + `(,(car colors) ,(cdr colors) 0.35 ,@region)) + (pdf-info-renderpage-text-regions + page width nil nil + `(,(car colors) ,(cdr colors) ,@region))))))) + +(defun pdf-view-kill-ring-save () + "Copy the region to the `kill-ring'." + (interactive) + (pdf-view-assert-active-region) + (let* ((txt (pdf-view-active-region-text))) + (pdf-view-deactivate-region) + (kill-new (mapconcat 'identity txt "\n")))) + +(defun pdf-view-mark-whole-page () + "Mark the whole page." + (interactive) + (pdf-view-deactivate-region) + (setq pdf-view-active-region + (list (list 0 0 1 1))) + (pdf-view--push-mark) + (pdf-view-display-region)) + +(defun pdf-view-active-region-text () + "Return the text of the active region as a list of strings." + (pdf-view-assert-active-region) + (mapcar + (apply-partially 'pdf-info-gettext (pdf-view-current-page)) + pdf-view-active-region)) + +(defun pdf-view-extract-region-image (regions &optional page size + output-buffer no-display-p) + ;; TODO: what is "resp."? Avoid contractions. + "Create a PNG image of REGIONS. + +REGIONS should have the same form as `pdf-view-active-region', +which see. PAGE and SIZE are the page resp. base-size of the +image from which the image-regions will be created; they default +to `pdf-view-current-page' resp. `pdf-view-image-size'. + +Put the image in OUTPUT-BUFFER, defaulting to \"*PDF region +image*\" and display it, unless NO-DISPLAY-P is non-nil. + +In case of multiple regions, the resulting image is constructed +by joining them horizontally. For this operation (and this only) +the `convert' program is used." + + (interactive + (list (if (pdf-view-active-region-p) + (pdf-view-active-region t) + '((0 0 1 1))))) + (unless page + (setq page (pdf-view-current-page))) + (unless size + (setq size (pdf-view-image-size))) + (unless output-buffer + (setq output-buffer (get-buffer-create "*PDF image*"))) + (let* ((images (mapcar (lambda (edges) + (let ((file (make-temp-file "pdf-view")) + (coding-system-for-write 'binary)) + (write-region + (pdf-info-renderpage + page (car size) + :crop-to edges) + nil file nil 'no-message) + file)) + regions)) + result) + (unwind-protect + (progn + (if (= (length images) 1) + (setq result (car images)) + (setq result (make-temp-file "pdf-view")) + ;; Join the images horizontally with a gap of 10 pixel. + (pdf-util-convert + "-noop" ;; workaround limitations of this function + result + :commands `("(" + ,@images + "-background" "white" + "-splice" "0x10+0+0" + ")" + "-gravity" "Center" + "-append" + "+gravity" + "-chop" "0x10+0+0") + :apply '((0 0 0 0)))) + (with-current-buffer output-buffer + (let ((inhibit-read-only t)) + (erase-buffer)) + (set-buffer-multibyte nil) + (insert-file-contents-literally result) + (image-mode) + (unless no-display-p + (pop-to-buffer (current-buffer))))) + (dolist (f (cons result images)) + (when (file-exists-p f) + (delete-file f)))))) + +;; * ================================================================== * +;; * Bookmark + Register Integration +;; * ================================================================== * + +(defun pdf-view-bookmark-make-record (&optional no-page no-slice no-size no-origin) + ;; TODO: add NO-PAGE, NO-SLICE, NO-SIZE, NO-ORIGIN to the docstring. + "Create a bookmark PDF record. + +The optional, boolean args exclude certain attributes." + (let ((displayed-p (eq (current-buffer) + (window-buffer)))) + (cons (buffer-name) + (append (bookmark-make-record-default nil t 1) + `(,(unless no-page + (cons 'page (pdf-view-current-page))) + ,(unless no-slice + (cons 'slice (and displayed-p + (pdf-view-current-slice)))) + ,(unless no-size + (cons 'size pdf-view-display-size)) + ,(unless no-origin + (cons 'origin + (and displayed-p + (let ((edges (pdf-util-image-displayed-edges nil t))) + (pdf-util-scale-pixel-to-relative + (cons (car edges) (cadr edges)) nil t))))) + (handler . pdf-view-bookmark-jump-handler)))))) + +;;;###autoload +(defun pdf-view-bookmark-jump-handler (bmk) + "The bookmark handler-function interface for bookmark BMK. + +See also `pdf-view-bookmark-make-record'." + (let ((page (bookmark-prop-get bmk 'page)) + (slice (bookmark-prop-get bmk 'slice)) + (size (bookmark-prop-get bmk 'size)) + (origin (bookmark-prop-get bmk 'origin)) + (file (bookmark-prop-get bmk 'filename)) + (show-fn-sym (make-symbol "pdf-view-bookmark-after-jump-hook"))) + (fset show-fn-sym + (lambda () + (remove-hook 'bookmark-after-jump-hook show-fn-sym) + (unless (derived-mode-p 'pdf-view-mode) + (pdf-view-mode)) + (with-selected-window + (or (get-buffer-window (current-buffer) 0) + (selected-window)) + (when size + (setq-local pdf-view-display-size size)) + (when slice + (apply 'pdf-view-set-slice slice)) + (when (numberp page) + (pdf-view-goto-page page)) + (when origin + (let ((size (pdf-view-image-size t))) + (image-set-window-hscroll + (round (/ (* (car origin) (car size)) + (frame-char-width)))) + (image-set-window-vscroll + (round (/ (* (cdr origin) (cdr size)) + (if pdf-view-have-image-mode-pixel-vscroll + 1 + (frame-char-height)))))))))) + (add-hook 'bookmark-after-jump-hook show-fn-sym) + (set-buffer (or (find-buffer-visiting file) + (find-file-noselect file))))) + +(defun pdf-view-bookmark-jump (bmk) + "Switch to bookmark BMK. + +This function is like `bookmark-jump', but it always uses the +selected window for display and does not run any hooks. Also, it +works only with bookmarks created by +`pdf-view-bookmark-make-record'." + + (let* ((file (bookmark-prop-get bmk 'filename)) + (buffer (or (find-buffer-visiting file) + (find-file-noselect file)))) + (switch-to-buffer buffer) + (let (bookmark-after-jump-hook) + (pdf-view-bookmark-jump-handler bmk) + (run-hooks 'bookmark-after-jump-hook)))) + +(defun pdf-view-registerv-make () + "Create a PDF register entry of the current position." + (registerv-make + (pdf-view-bookmark-make-record nil t t) + :print-func 'pdf-view-registerv-print-func + :jump-func 'pdf-view-bookmark-jump + :insert-func (lambda (bmk) + (insert (format "%S" bmk))))) + +(defun pdf-view-registerv-print-func (bmk) + "Print a textual representation of bookmark BMK. + +This function is used as the `:print-func' property with +`registerv-make'." + (let* ((file (bookmark-prop-get bmk 'filename)) + (buffer (find-buffer-visiting file)) + (page (bookmark-prop-get bmk 'page)) + (origin (bookmark-prop-get bmk 'origin))) + (princ (format "PDF position: %s, page %d, %d%%" + (if buffer + (buffer-name buffer) + file) + (or page 1) + (if origin + (round (* 100 (cdr origin))) + 0))))) + +(defmacro pdf-view-with-register-alist (&rest body) + "Setup the proper binding for `register-alist' in BODY. + +This macro may not work as desired when it is nested. See also +`pdf-view-use-dedicated-register'." + (declare (debug t) (indent 0)) + (let ((dedicated-p (make-symbol "dedicated-p"))) + `(let* ((,dedicated-p pdf-view-use-dedicated-register) + (register-alist + (if ,dedicated-p + pdf-view-register-alist + register-alist))) + (unwind-protect + (progn ,@body) + (when ,dedicated-p + (setq pdf-view-register-alist register-alist)))))) + +(defun pdf-view-position-to-register (register) + "Store current PDF position in register REGISTER. + +See also `point-to-register'." + (interactive + (list (pdf-view-with-register-alist + (register-read-with-preview "Position to register: ")))) + (pdf-view-with-register-alist + (set-register register (pdf-view-registerv-make)))) + +(defun pdf-view-jump-to-register (register &optional delete return-register) + ;; TODO: add RETURN-REGISTER to the docstring. + "Move point to a position stored in a REGISTER. + +Optional parameter DELETE is defined as in `jump-to-register'." + (interactive + (pdf-view-with-register-alist + (list + (register-read-with-preview "Jump to register: ") + current-prefix-arg + (and (or pdf-view-use-dedicated-register + (local-variable-p 'register-alist)) + (characterp last-command-event) + last-command-event)))) + (pdf-view-with-register-alist + (let ((return-pos (and return-register + (pdf-view-registerv-make)))) + (jump-to-register register delete) + (when return-register + (set-register return-register return-pos))))) + +(provide 'pdf-view) + +;;; pdf-view.el ends here |