Emacs: 翻动其它 buffer 中的 PDF 文件

写了两个简单的 elisp 函数, 让我在 Emacs 中边读 PDF 边做笔记的体验更好一些.

当前窗口只显示两个 buffer: 左边的 buffer 中打开 PDF 文件, 右边的 buffer 用 org-mode 做笔记. 想要翻动 PDF 的时候, 直接在笔记 buffer 中按 f8 即可. 效果如下:

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

1 elisp 代码

运行下面的 elisp 代码 (然后放到 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)

然后在笔记 buffer 里按 f8 使 PDF buffer 里的内容向下翻, 按 f9 上翻.

2 hack 过程

早上在火车上读 深入浅出 Node.js 一书, 用 Zathura 打开 PDF 文件放在屏幕左侧, 用 Emacs 打开一个 org-mode 文件放在右侧, 边看书边做笔记. 但是在 Zathura 和 Emacs 之间不停切换, 实在麻烦.

试了一下直接在 Emacs 中打开 PDF 文件, 发现速度和渲染效果都可以接受 (很久之前用的时候速度还比较慢, 可能是我当时用的电脑太老). 左边的 buffer 打开 PDF, 右边的 buffer 记笔记, 光标一直在笔记 buffer 里, 然后按 C-M-v (也就是 scroll-other-window) 来翻动 PDF buffer.

问题是按 C-M-v 翻到页面底部的时候, 不会自动进入下一页.

在 PDF buffer 里, 可以按 n 翻到下一页, 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.

也就说 n 对应的命令是 doc-view-next-page, 名字很直观.

写个小 elisp 命令, 假设目前只有笔记 buffer 和 PDF buffer, 而光标在笔记 buffer 中. 翻页的时候, 先跳到 PDF buffer, 执行 doc-view-next-page, 然后再跳回来:

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

绑定到 f8:

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

可以用了, 但是一会儿就发现个问题, 我用 C-M-v 移动到页尾的时候, 按 f8, 会直接翻到下一页的页尾, 而不是页首.

直接跳到 PDF buffer, 按 n, 发现也是同样的问题, 所以 bug 不是我引入的.

看看 doc-view-next-page 是怎么实现的吧, 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.

点进 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))))

看看 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))))

一时找不到哪儿有问题, 但是阅读代码的时候无意中发现了 doc-view-scroll-up-or-next-page 的定义:

(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 是把 PDF 转换成图片然后显示, 所以我猜 image-bob 应该就是移动到图片顶端 (也就是页首) 的函数, 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.

如我所料, 那么我在 wenshan-other-docview-buffer-next-page 加上 (image-bob) 应该就可以解决问题了:

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

试了一下, 好了!

但是看 doc-view-scroll-up-or-next-page (为什么叫 scroll-up?) 的注释, 这个函数应该可以在翻到页尾的时候直接翻到下一页, 所以没必要单独写一个翻页的函数, 改动一下, 加个注释:

(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))

还是绑定到 f8:

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

然后依样写个向上翻的, 绑定到 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)

搞定!

3 思考

除此之外, 还可以考虑写一些其他命令, 比如获取当前正在阅读的页数.

甚至可以深入了解一下 Emacs 的图片处理能力, 看能不能实现类似于 Repligo Reader 的功能, 估计是比较难.

Leave a Reply