;;; 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 ;; 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 . ;;; 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 " " #'pdf-view-pages-next-command " " #'pdf-view-pages-previous-command " " #'pdf-view-pages-next-command " " #'pdf-view-pages-previous-command " " #'pdf-view-pages-goto-page) ;;;###autoload (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))) ;;;###autoload (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 " " #'pdf-view-offset-goto-page " " #'pdf-view-offset-goto-page " " #'pdf-view-offset-first-page) ;;;###autoload (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))) ;;;###autoload (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