(分享)eaf-pdf-viewer 支持 PDF/EPUB 文件行搜索和实时预览

我最近陆续给 eaf-pdf-viewer 提交了支持按行全文搜索和预览的代码,让 pdf 和 epub 搜索体验接近普通 emacs buffer , 目前支持 ivy ,直接看效果:

2025-03-03_12-59-43

在 700 页的 emacs manual PDF 里模糊搜索全文并实时高亮当前行.

2025-03-28_21-17-10

在中文 EPUB 里用拼音搜索,并同步页面显示 ( 由于 eaf-pdf-viewer 基于的 pymupdf 不支持对 epub 添加标注,所以用 pyqt 绘制箭头指示当前行,和 pdf ui 稍有不同,有些行没有那么精确,但偏差不大)

安装完 eaf-pdf-viewer 后执行 eaf-pdf-narrow-search 命令或者绑定特定按键就可以使用

(eaf-bind-key eaf-pdf-narrow-search "/" eaf-pdf-viewer-keybinding)

对我来说,这体验超过了大部分系统级别的 pdf 或 epub 预览应用了,加上很方便打开 pdf 目录,跳转和用 org mode 编辑目录,用起来很顺畅, 也取代了用了很久的 pdf-tools + nov ,感谢 @manateelazycat , @tumashu 及其他开源贡献者开发 :+1:

使用其他 minibuffer 搜索框架(比如 consult, helm)的道友有兴趣可以尝试把这些前端也加进去, 核心是参考以下函数把 eaf-pdf-narrow–update, eaf-pdf-narrow–quit , eaf-pdf-narrow–done 三个接口串起来:

(defun eaf-pdf-narrow--ivy (cache-file-name eaf-buffer-id &optional current-page) 
  (let* ((candidates (split-string
                      (with-temp-buffer
                        (set-buffer-multibyte t)
                        (insert-file-contents-literally cache-file-name)
                        (decode-coding-region (point-min) (point-max) 'utf-8)
                        (buffer-string)) "\n" t)))
    (if (require 'ivy nil t)
        (ivy-read
         "Narrow Search: "
         candidates
         :update-fn (lambda ()
                      (eaf-pdf-narrow--update
                       eaf-buffer-id
                       ivy-text
                       (ivy-state-current ivy-last)
                       ivy--index ivy--old-cands))
         :require-match t
         :preselect (format "%s:" current-page)
         :action (lambda (selection) (eaf-pdf-narrow--done eaf-buffer-id))
         :unwind (lambda () (unless ivy-exit (eaf-pdf-narrow--quit eaf-buffer-id)))
         :caller 'eaf-pdf-narrow--ivy)
      (message "Please install ivy first."))))
20 个赞

手动点赞👍

太牛了大佬!

看着体验真不错,已经超越很多 PDF 阅读器了。不知道 eaf-pdf-viewer 在 Mac OS 上现在的使用体验如何,好久没有尝试了。

@MatthewZMD @manateelazycat @twiddling

给大佬们提需求来啦:

由于我这边使用 Mac eaf-pdf,使用鼠标操作很不顺手(鼠标选择文字飘忽不定)。希望对于 Text 类型的 pdf 能有一个类似的 Meow mode 的操作。能够在 eaf 当中快捷的进行文本移动/选择的操作。 这样就可以全键盘的操作了。

虽然可以通过 eaf-pdf-extract-page-text 获取当前页面的 text,但这个需要跳转到另一个 buffer,操作不自然。

我进行了一轮调研和学习,感觉应该可以实现。

pymupdf 可以通过 words = page.get_text(“words”),抽取当前页面的所有单词 格式为:

(x0, y0, x1, y1, "word", block_no, line_no, word_no)

这样就可以快速的通过 word line block 这三种类型,进行快速的导航/选择。这样可以全键盘操作了。

糊了一下通过 word 导航的 demo,测试了一下,还是挺快的。

ezgif-66e830f844bdb2

未来:

  1. 可以设定一个 mode,
  2. 未进入mode时,jk 是翻页,
  3. 进入 mode h/j/k/l 按照单词导航,H/J/K/L 选择多个单词/行等
  4. 选择后,就可以快速的翻译、记笔记、ai 等等其他的 Emacs 能力

坐等大佬们有时间实现一下。由于我比较菜,要是我来实现,估计遥遥无期,先把需求和想法提一下。

4 个赞

我甚至憧憬在pdf里面用ace-jump

之前的全文搜索功能对论文之类的小的文档很好,但对于大的 pdf 就太慢了,同时考虑到 emacs 本身 minibuffer 框架的过滤搜索就非常好,于是加了一个 cache ,先让 ivy 搜出结果再把具体页码传递过去高亮,这样 eaf 那边只需要在一页里面搜索并显示出来,所以大部分都是基于前人写的搜索的代码,改得代码比我想象的少。

发了一个新 PR,合并之后 epub 里就可以绘制一个箭头来指示当前搜索行了。展示效果的 gif 已经更新到主帖中。

其实pymupdf原本的搜索速度很快,只不过eaf-pdf-viewer包装后变慢了,尝试了以下代码,搜索速度很快,在_search_in_pages中修改

    def _search_in_pages(self, text, page_list):
        # PdfDocument在fitz.Document基础上增加了功能,导致self.document循环时很慢
        for page_index in page_list:
            # Search from the current page
            page = self.document.document[page_index]
            if page_index < self.current_page_index:
                self.search_text_index = len(self.search_text_quads_list)
            
            if support_hit_max:
                quads_list = self.document.document.search_page_for(page_index, text, hit_max=999, quads=True)
            else:
                quads_list = self.document.document.search_page_for(page_index, text, quads=True)
            if quads_list:
                for index, quad in enumerate(quads_list):
                    search_text_offset = (page_index * self.page_height + quad.ul.y) * self.scale
                    self.search_text_offset_list.append(search_text_offset)
                    self.search_text_quads_list.append(quad)
                self.search_page_history.add(page)
        return quads_list

还得把search_text中page.cleanup_search_text()删除掉,否则弹出错误。

修改之后速度很快,大文件也没问题,不知道这样改有没有问题,大佬可以验证一下

follow link 的功能似乎就是类似 ace-jump 的效果,我以前用过 chrome 插件里 surfingkey 可以按一个键把 chrome 里打开 pdf 视图里的每个单词前标注一两个英文字母(这导致满屏都是附加英文字符),然后按字符把光标移过去,但感觉这类功能更适合要精确编辑的场景,体验过就没再用了,但你说到这功能,确实有人实现,你可以去体验一下试试

这个模式和我在上一楼提到的 chrome 的 surfingkey 插件 带的功能是类似的,它内置了一个 pdf 阅览器,基本就是可以用 vim 按键全键盘操作,比如按一个按键后进入 visual mode, 然后用 vim 按键移动选择,你可以去体验一下,看用起来是否顺畅,js 能实现,python 应该也可以。(我自己用了感觉不是很舒服,所以都不再用这个插件了,还是鼠标直接选更好,我在 linux 上用 eaf-pdf-viewer 鼠标也容易飘,能先解决这个是最好的,但目前用 extract 文本来选择也挺方便,有时候比鼠标选还好,因为可以 emacs 按键多行去选,而且可以在 extract buffer 里基于原文临时编辑成自己的话,然后复制到笔记里)

是把 self.document 改成 self.document.document 吗?用原始的 fitz.Document?

如果你改了效率更高,也可以直接优化它给 eaf 发 PR,因为这个 ivy 搜索和原本的 C-s 搜索都基于这个函数,二者都获益,而且功能上也算是互补的,C-s 在按关键字精确搜索的场景下很好用,ivy 搜索主要是利用minibuffer 的那些模糊搜索机制来查,还可以导出或者复制搜索候选项等。

但我感觉原来的慢是因为每次重新去搜整个文档,所以即便单页很快,遇上百甚至上千页的文档也很慢,可以改成每次只搜当前页面,超过页面再去搜下一页是更合理的,后面再具体看一看。

刚试了下1700页的pdf文件,确实会慢点,等待大概1到2秒吧,可以接受吧

这个插件看着挺好玩的。

我在一台 2020 年左右的 AMD Ryzen 7 4800H 的 cpu 机器上,ubuntu 系统, elisp 文档(1300 多页)搜 debugging 这个词,结果有 100 多个选项,用现在的 self.document 搜得卡 6,7 十秒,改成 self.document.document 搜可能 40s 左右,是更快,但还是用不起来(20 页以下pdf基本是即时返回结果)。

可能是以下 PdfDocument getitem 使得从 document 里索引 page 更慢了

class PdfDocument(fitz.Document):
  ...
    def __getitem__(self, index):
        if index in self._page_cache_dict:
            page = self._page_cache_dict[index]
            if not self._is_trim_margin:
                return page

            if page.cropbox == self._document_page_clip:
                return page

        page = PdfPage(self.document[index], index, self.document.is_pdf)
       ...

确实,可能跟pdf内容有关系,我上面用的那个1700页的pdf都是英文的,就卡顿一两秒。换一个中文质量不高的文档就卡顿很久。

还有现在的搜索机制是随输入内容不断更新页面,例如搜“search”,输入s就开始全文搜索,输入se又全文搜索,等全输入了就快卡死了。我写了个包装,使得完成全部输入之后才发送到eaf-pdf-viewer侧,感觉可以接受

(defun eaf-pdf-search-with-text (text)
  (eaf-call-sync "execute_function_with_args" eaf--buffer-id "send_input_message" "Search Text: " "search_text" "search" text))
(defun eaf-pdf-search-text-wrap (fn)
  (let ((text (unless eaf-search-input-active-p
                (read-from-minibuffer "Search Text: " nil (let ((input-text-map (copy-keymap minibuffer-local-map)))
                                                            (define-key input-text-map "\C-s" #'exit-minibuffer)
                                                            (define-key input-text-map "\C-r" #'exit-minibuffer)
                                                            input-text-map)))))
    (if (and text (not (string-empty-p text)))
        (eaf-pdf-search-with-text text)
      (eaf-call-async "eval_function" eaf--buffer-id fn (key-description (this-command-keys-vector))))))
(defun eaf-pdf-search-text-forward ()
  (interactive)
  (eaf-pdf-search-text-wrap "search_text_forward"))
(defun eaf-pdf-search-text-backward ()
  (interactive)
  (eaf-pdf-search-text-wrap "search_text_backward"))
1 个赞

这种情况确实快不少,我这在 elisp 文档里搜 debugging 能 10s 内了,在小几百页的场景下基本能实时用了,:+1:

1 个赞

欢迎大佬继续补丁呀,我虽然不用ivy,但是看着搜索好丝滑,点赞

1 个赞