summaryrefslogtreecommitdiff
path: root/pdf-view-reader.el
diff options
context:
space:
mode:
Diffstat (limited to 'pdf-view-reader.el')
-rw-r--r--pdf-view-reader.el203
1 files changed, 203 insertions, 0 deletions
diff --git a/pdf-view-reader.el b/pdf-view-reader.el
new file mode 100644
index 0000000..f5b1265
--- /dev/null
+++ b/pdf-view-reader.el
@@ -0,0 +1,203 @@
+;;; pdf-view-reader.el --- Quality-of-life utils for using PDF-Tools as a PDF viewer -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Simon Parri
+
+;; Author: Simon Parri <simonparri@ganzeria.com>
+;; Keywords: multimedia, convenience
+;; Version: 0.50
+;; Package-Requires: ((emacs "24.3") (compat "29.1") (pdf-tools "1"))
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This package comes in 3 parts:
+
+;; 1. pdf-view-desktop :: `desktop-mode' integration for `pdf-tools'. Call
+;; `pdf-view-desktop-setup' to install the appropriate hooks.
+
+;; 2. pdf-view-pages :: Dual-page mode for `pdf-tools'. Enable
+;; `pdf-view-pages-mode' mode to get behavior similar to `follow-mode' for
+;; normal buffers or call `pdf-view-book' to toggle both
+;; `pdf-view-pages-mode' and a dual-page window configuration.
+
+;; 3. pdf-view-offset :: Page offset for `pdf-tools'. Useful for when the
+;; page numbers on the PDF pages don't match the real page numbers.
+;; Enabling `pdf-view-offset-mode' will prompt you for the page number
+;; printed on the page, and will calculate and set `pdf-view-offset'
+;; appropriately. `pdf-view-offset-set' will instead prompt you for the
+;; offset directly, and will also enable `pdf-view-offset-mode' afterwards.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'pdf-tools)
+
+;; Desktop
+(defun pdf-view--save-buffer (_desktop)
+ ;; This is an alist just in case we want more information here
+ ;; later. As long as `pdf-view-current-page' is a macro, keep
+ ;; this expansion up to date.
+ "`desktop-mode' function to save `pdf-view-mode' buffers."
+ `((page . ,(image-mode-window-get 'page))))
+(defun pdf-view--restore-buffer (file buffer misc)
+ "`desktop-mode' function to restore `pdf-view-mode' buffers.
+
+See `(elisp) Desktop Save Mode' for meanings of FILE, BUFFER and MISC."
+ (when-let ((buf (desktop-restore-file-buffer file buffer misc)))
+ (with-current-buffer buf
+ (pdf-view-goto-page (cdr (assq 'page misc)))
+ buf)))
+
+;;;###autoload
+(defun pdf-view-desktop-setup ()
+ "Set up `desktop-mode' integration for `pdf-view-mode'."
+ (add-to-list 'desktop-buffer-mode-handlers
+ '(pdf-view-mode . pdf-view--restore-buffer))
+ (add-hook 'pdf-view-mode-hook
+ (lambda ()
+ (setq desktop-save-buffer
+ #'pdf-view--save-buffer))))
+
+;; Multi-page
+;; TODO: Should use advice?
+
+ (defun pdf-view-pages-windows (&optional buffer)
+ (let ((b (or buffer (current-buffer))))
+ (with-current-buffer b
+ (unless (derived-mode-p 'pdf-view-mode)
+ (error "Buffer is not a pdf-view buffer: %s" b))
+ (seq-filter (lambda (x) (eq (window-buffer x) b))
+ (window-list)))))
+
+(cl-macrolet ((m (dir page)
+ `(defun ,(intern (format "pdf-view-pages-%s" dir)) (&optional n)
+ (let ((ww (pdf-view-pages-windows)))
+ (dolist (w ww)
+ (with-selected-window w
+ (ignore-errors
+ (pdf-view-next-page ,page))))))))
+ (m next (or n (length ww)))
+ (m previous (- (or n (length ww)))))
+
+(cl-macrolet ((m (dir)
+ `(defun ,(intern (format "pdf-view-pages-%s-command" dir)) (n)
+ (declare (interactive-only t))
+ (interactive "P" pdf-view-pages-mode)
+ (,(intern (format "pdf-view-pages-%s" dir)) n))))
+ (m next)
+ (m previous))
+
+(defun pdf-view-pages-goto-page (n)
+ "Go to page N in the current window, scrolling others by the same amount."
+ (interactive "NPage: " pdf-view-pages-mode)
+ (let ((s (pdf-view-current-page)))
+ (dolist (w (pdf-view-pages-windows))
+ (with-selected-window w
+ (let ((d (- (pdf-view-current-page) s)))
+ (pdf-view-goto-page (+ n d)))))))
+
+(defvar-keymap pdf-view-pages-mode-map
+ "<remap> <pdf-view-next-page-command>" #'pdf-view-pages-next-command
+ "<remap> <pdf-view-previous-page-command>" #'pdf-view-pages-previous-command
+ "<remap> <pdf-view-scroll-up-or-next-page>" #'pdf-view-pages-next-command
+ "<remap> <pdf-view-scroll-down-or-previous-page>" #'pdf-view-pages-previous-command
+ "<remap> <pdf-view-goto-page>" #'pdf-view-pages-goto-page)
+
+(define-minor-mode pdf-view-pages-mode
+ "Minor mode for a multi-page view in `pdf-view-mode'."
+ :lighter " Pages"
+ :interactive (pdf-view-mode))
+
+(defun pdf-view-book-layout-p (win)
+ "Return t if WIN has nearby windows such that the layout is book-like.
+
+Book-like means that WIN displays the same buffer as the window to the
+right, and that the buffer in question has `pdf-view-pages-mode'
+enabled."
+ (let* ((b (window-buffer win))
+ (pdf?
+ (with-current-buffer b
+ (derived-mode-p 'pdf-view-mode)))
+ (pages?
+ (with-current-buffer b
+ pdf-view-pages-mode))
+ (ow (window-in-direction 'right)))
+ (and ow pdf? pages?
+ (eq b (window-buffer ow))
+ ow)))
+
+(defun pdf-view-book (&optional win)
+ "Set up WIN (or current window, if nil) in a book-like layout.
+
+Book-like means that WIN will display the same buffer as the window to
+the right (which will be created), and that the buffer in question will
+have `pdf-view-pages-mode' enabled."
+ (interactive nil pdf-view-mode)
+ (let ((win (or win (selected-window))))
+ (if-let ((ow (pdf-view-book-layout-p win)))
+ (progn
+ (delete-window ow)
+ (pdf-view-pages-mode -1))
+ (with-selected-window (split-window-right)
+ (call-interactively #'pdf-view-next-page-command))
+ (pdf-view-pages-mode 1))
+ (with-selected-window win
+ (pdf-view-fit-height-to-window))))
+
+;; Offset
+;; TODO: Should use advice?
+
+(defvar-local pdf-view-offset 0
+ "Current page offset.")
+(add-to-list 'desktop-locals-to-save 'pdf-view-offset)
+
+(defun pdf-view-offset-goto-page (n)
+ "Go to page N, offset according to `pdf-view-offset'."
+ (interactive "NPage: " pdf-view-offset-mode)
+ (funcall (if pdf-view-pages-mode
+ #'pdf-view-pages-goto-page
+ #'pdf-view-goto-page)
+ (+ n pdf-view-offset)))
+
+(defun pdf-view-offset-first-page ()
+ "Go to page 0, taking `pdf-view-offset' into account."
+ (interactive nil pdf-view-offset-mode)
+ (pdf-view-offset-goto-page 0))
+
+(defvar-keymap pdf-view-offset-mode-map
+ "<remap> <pdf-view-goto-page>" #'pdf-view-offset-goto-page
+ "<remap> <goto-line>" #'pdf-view-offset-goto-page
+ "<remap> <pdf-view-first-page>" #'pdf-view-offset-first-page)
+
+(define-minor-mode pdf-view-offset-mode
+ "Minor mode to have an offset for PDF pages in `pdf-view-mode'."
+ :lighter (:eval (format " Off(%d)" pdf-view-offset))
+ :interactive (pdf-view-mode)
+ (if pdf-view-offset-mode
+ (thread-last
+ (read-number "Current page: " (pdf-view-current-page))
+ (- (pdf-view-current-page))
+ (setq pdf-view-offset))
+ (setq pdf-view-offset 0)))
+
+(defun pdf-view-offset-set (n)
+ "Set offset for `pdf-view-offset-mode' to N."
+ (interactive "NOffset: " pdf-view-mode)
+ (unless pdf-view-offset-mode
+ (pdf-view-offset-mode 1))
+ (setq pdf-view-offset n))
+
+(provide 'pdf-view-reader)
+;;; pdf-view-reader.el ends here