Emacs: scroll PDF in other buffer

I wrote two simple elisp commands to make the experience of taking notes while reading PDF in Emacs better.

There are two visible buffers: the PDF file is opened in the left buffer, the org-mode buffer on the right hand side is for notes taking. You only need to press f8 in the org-mode buffer when you want to scroll the PDF. Here is a GIF format screencast:

emacs-scroll-pdf-in-other-buffer-demo.gif

1 elisp code

Evaluate the following elisp code (and put them in your init.el):

(defun wenshan-other-docview-buffer-scroll-down ()
  "There are two visible buffers, one for taking notes and one
for displaying PDF, and the focus is on the notes buffer. This
command moves the PDF buffer forward."
  (interactive)
  (other-window 1)
  (doc-view-scroll-up-or-next-page)
  (other-window 1))

(defun wenshan-other-docview-buffer-scroll-up ()
  "There are two visible buffers, one for taking notes and one
for displaying PDF, and the focus is on the notes buffer. This
command moves the PDF buffer backward."
  (interactive)
  (other-window 1)
  (doc-view-scroll-down-or-previous-page)
  (other-window 1))

(global-set-key (kbd "<f8>") 'wenshan-other-docview-buffer-scroll-down)
(global-set-key (kbd "<f9>") 'wenshan-other-docview-buffer-scroll-up)

Then you can press f8 in the org-mode buffer to scroll down the PDF and press f9 to scroll it up.

2 hack log

I start to read 深入浅出 Node.js on the train this morning. I have the PDF opened in Zathura on the left and an org-mode buffer opened in Emacs on the right hand side. I take notes while reading and it turns out to be really annoying to switch between Zathura and Emacs constantly.

Then I open the PDF in Emacs and find that both the speed and the rendering quality are acceptable (it was quite slow the last time I tried it, perhaps I was using an too old laptop). I leave the PDF buffer open on the left and keep the org-mode buffer on the right. The cursor can always be in the org-mode buffer – I just press C-M-v (scroll-other-window) to scroll the PDF buffer downward.

The problem is C-M-v doesn’t get to the next page when it reaches the end of the current page.

I know pressing n in the PDF buffer can get me to its next page, C-h k n:

n runs the command doc-view-next-page, which is an interactive compiled Lisp
function in `doc-view.el'.

It is bound to n, <next>, C-x ].

(doc-view-next-page &optional ARG)

Browse ARG pages forward.

Which means the command `n` bound to is doc-view-next-page, straightforward enough.

Write a small elisp command, assuming there are only two buffers (the PDF buffer and the org-mode buffer) and the cursor is in the org-mode buffer. To get to the next page, Emacs needs to switch its focus to the PDF buffer, executes doc-view-next-page, then switch its focus back to the org-mode buffer:

(defun wenshan-other-docview-buffer-next-page ()
  (interactive)
  (other-window 1)
  (doc-view-next-page)
  (other-window 1))

Bind this command to f8:

(global-set-key (kbd "<f8>") 'wenshan-other-docview-buffer-next-page)

It’s useful now. However, a problem arise quickly: after moving to the end of a page, pressing f8 scrolls it to the end rather than the beginning of the next page.

Switch to the PDF buffer and press n, same problem, so the bug isn’t introduced by me.

Let’s see how doc-view-next-page is implemented, C-h-f doc-view-next-page:

doc-view-next-page is an interactive compiled Lisp function in `doc-view.el'.

(doc-view-next-page &optional ARG)

Browse ARG pages forward.

Click doc-view.el:

(defun doc-view-next-page (&optional arg)
  "Browse ARG pages forward."
  (interactive "p")
  (doc-view-goto-page (+ (doc-view-current-page) (or arg 1))))

Hmm, read the source code of doc-view-goto-page:

(defun doc-view-goto-page (page)
  "View the page given by PAGE."
  (interactive "nPage: ")
  (let ((len (doc-view-last-page-number)))
    (if (< page 1)
    (setq page 1)
      (when (and (> page len)
                 ;; As long as the converter is running, we don't know
                 ;; how many pages will be available.
                 (null doc-view--current-converter-processes))
    (setq page len)))
    (force-mode-line-update)            ;To update `current-page'.
    (setf (doc-view-current-page) page
      (doc-view-current-info)
      (concat
       (propertize
        (format "Page %d of %d." page len) 'face 'bold)
       ;; Tell user if converting isn't finished yet
       (if doc-view--current-converter-processes
           " (still converting...)\n"
         "\n")
       ;; Display context infos if this page matches the last search
       (when (and doc-view--current-search-matches
              (assq page doc-view--current-search-matches))
         (concat (propertize "Search matches:\n" 'face 'bold)
             (let ((contexts ""))
               (dolist (m (cdr (assq page
                         doc-view--current-search-matches)))
             (setq contexts (concat contexts "  - \"" m "\"\n")))
               contexts)))))
    ;; Update the buffer
    ;; We used to find the file name from doc-view--current-files but
    ;; that's not right if the pages are not generated sequentially
    ;; or if the page isn't in doc-view--current-files yet.
    (let ((file (expand-file-name
                 (format doc-view--image-file-pattern page)
                 (doc-view--current-cache-dir))))
      (doc-view-insert-image file :pointer 'arrow)
      (when (and (not (file-exists-p file))
                 doc-view--current-converter-processes)
        ;; The PNG file hasn't been generated yet.
        (funcall doc-view-single-page-converter-function
         doc-view--buffer-file-name file page
         (let ((win (selected-window)))
           (lambda ()
             (and (eq (current-buffer) (window-buffer win))
              ;; If we changed page in the mean
              ;; time, don't mess things up.
              (eq (doc-view-current-page win) page)
              ;; Make sure we don't infloop.
              (file-readable-p file)
              (with-selected-window win
                (doc-view-goto-page page))))))))
    (overlay-put (doc-view-current-overlay)
         'help-echo (doc-view-current-info))))

Can’t see where the problem is immediately, but I found doc-view-scroll-up-or-next-page by accident during the process of code reading:

(defun doc-view-scroll-up-or-next-page (&optional arg)
  "Scroll page up ARG lines if possible, else goto next page.
When `doc-view-continuous' is non-nil, scrolling upward
at the bottom edge of the page moves to the next page.
Otherwise, goto next page only on typing SPC (ARG is nil)."
  (interactive "P")
  (if (or doc-view-continuous (null arg))
      (let ((hscroll (window-hscroll))
        (cur-page (doc-view-current-page)))
    (when (= (window-vscroll) (image-scroll-up arg))
      (doc-view-next-page)
      (when (/= cur-page (doc-view-current-page))
        (image-bob)
        (image-bol 1))
      (set-window-hscroll (selected-window) hscroll)))
    (image-scroll-up arg)))

Emacs converts PDF pages into PNG format pictures and then display them, so I guess image-bob (bob: beginning of buffer) should be the function that scrolls the image window to its very beginning. C-h f image-bob:

image-bob is an interactive compiled Lisp function in `image-mode.el'.

(image-bob)

Scroll to the top-left corner of the image in the current window.

As I thought. Therefore, in theory we can solve the problem by adding (image-bob) to wenshan-other-docview-buffer-next-page:

(defun wenshan-other-docview-buffer-next-page ()
  (interactive)
  (other-window 1)
  (doc-view-next-page)
  (image-bob)
  (other-window 1))

Give it a go. It works!

However, the name of doc-view-scroll-up-or-next-page (though I don’t quite understand why it’s named scroll-up rather than scroll-down) indicates it could do what we want. Modify our command:

(defun wenshan-other-docview-buffer-scroll-down ()
  "There are two visible buffers, one for taking notes and one
for displaying PDF, and the focus is on the notes buffer. This
command moves the PDF buffer forward."
  (interactive)
  (other-window 1)
  (doc-view-scroll-up-or-next-page)
  (other-window 1))

Again, bind it to f8:

(global-set-key (kbd "<f8>") 'wenshan-other-docview-buffer-scroll-down)

Then write a similar command for scrolling up and bind it to f9:

(defun wenshan-other-docview-buffer-scroll-up ()
  "There are two visible buffers, one for taking notes and one
for displaying PDF, and the focus is on the notes buffer. This
command moves the PDF buffer backward."
  (interactive)
  (other-window 1)
  (doc-view-scroll-down-or-previou-page)
  (other-window 1))

(global-set-key (kbd "<f9>") 'wenshan-other-docview-buffer-scroll-up)

Done!

3 Thoughts

Apart from what I’ve wrote, I think commands such as getting the page number of the page I’m currently reading could be useful.

I can even learn more about Emacs’s ability on image processing, who knows, it might be possible to implement “Repligo Reader”-like functionality.

Leave a Reply