我是如何构建 PDF Ink 来舒服地阅读 PDF 电子书并在线学习的

为什么读 PDF 却不能在上面写写画画?

大家好!我是 Albert,我热衷于让工具变得更加方便实用。

如果你曾尝试过自学、研究编程教科书,或者在笔记本电脑/平板电脑上阅读复杂的 PDF 技术文档,你一定经历过这种痛苦。当你打开一本书,想要高亮显示一段核心代码,在解释旁画个简单的示意图箭头,或者在页边空白处写下笔记——却发现标准的 PDF 阅读器要么臃肿、收费,要么在你一不小心刷新标签页时,就把你的手写涂鸦丢得精光。

我想要解决这个问题,于是开发了 PDF Ink——一个轻量、快速的 Web 应用程序,可直接在浏览器中阅读 PDF 书籍并进行交互式批注。


构想与技术栈

核心目标是打造一个满足以下条件的工具:

  1. 在任何现代浏览器中一键启动。
  2. 允许加载任何本地 PDF 教科书。
  3. 支持使用鼠标、触控笔或触控操作在页面上绘图,并键入文本注释。
  4. 最重要的一点: 能够在本地安全保存当前阅读的页面、缩放比例、图书列表以及所有绘图笔记,方便随时继续学习。

在技术选型上,我选择了现代的前端栈:React + TypeScript + Vite + TailwindCSS

对于 PDF 渲染引擎,我集成了 Mozilla 可靠的 pdfjs-dist (PDF.js),它将文档页面直接渲染到 HTML5 <canvas> 元素上。


双层画布架构

为了保证用户在鼠标或触控交互时,应用能保持极高的性能和流畅响应,我将工作区拆分为两个独立的图层:

  1. PDF 图层 (PDF Layer):一个静态背景 <canvas>,根据当前缩放比例渲染 PDF 页面图像。
  2. 批注图层 (Annotation Layer):一个绝对定位的顶层透明 <canvas>,负责拦截用户的指针事件(PointerEvents)、绘制矢量笔迹并叠加文本笔记。

这种双层画布架构极大地提升了绘制性能:当用户画线时,应用仅重绘轻量级的矢量笔迹层,而无需触动渲染开销较大的 PDF 画布。

针对触摸屏设备,我还增加了:

  • 多点触控捏合缩放(pinch-to-zoom)支持。
  • 文本笔记放置支持,利用触摸拖放偏移量映射到页面宽高比坐标。

数据库演进:升级至 IndexedDB v2

原型机的初始版本将当前 PDF 文件和所有涂鸦作为一个整体,保存在 IndexedDB 中(对应 last-opened 键名)。

然而,随着书籍体积增大,我们遇到了性能瓶颈:

问题所在: 用户每次添加笔迹或移动文本框时,应用都会进行自动保存。每隔 250 毫秒向磁盘写入一次 20MB 的 PDF 原始文件二进制缓存,会导致界面卡顿,并加速磨损 SSD 固态硬盘。

解决方案:文件与批注分离存储

我重写了 IndexedDB 管理器,并将数据库模式升级到了版本 2:

  1. pdfs 存储库:存储原始的二进制文件缓冲(ArrayBuffer),以文档的 UUID 为键名。该操作在向书架上传新文件时仅执行一次
  2. pdfs_metadata 存储库:存储轻量的 JSON 元数据,包括页码、缩放比例、涂鸦轨迹和文本框坐标。

现在,当你在页面上书写时,只有几百字节的元数据会被更新。写入过程在几分之一毫秒内即可完成,完全消除了卡顿!

我还开发了一个自动迁移脚本。如果用户使用旧版(版本 1)的数据库打开应用,系统会自动提取二进制数据、生成新的 UUID、将元数据分离,并在后台无缝迁移他们的绘图会话。


细节打磨:书架、多语言以及折叠式控件

为了让 PDF Ink 具有专业生产力工具的高级感,我融入了三个亮点功能:

  • 自适应 PDF 书架:一个可折叠的侧边栏,以交互卡片的形式展示已上传的 PDF 列表、阅读进度和批注统计数据(画笔线条数、文本标注数)。
  • 多语言切换器:内置了本地化系统,支持英文(默认,EN 优先)和俄文(RU),设置持久化存储在 localStorage 中。
  • 折叠式画笔设置栏:将画笔参数行(颜色、粗细滑块、不透明度)收纳进一个可通过“ sliders”图标切换的折叠区,使用户可以在小屏设备上隐藏它以腾出最大的阅读空间。

即刻体验!

你可以在这里试用应用的在线版本:
👉 PDF Ink 在线体验地址


结语

学习工具应该帮助你专注于吸收知识,而不是与界面作斗争。开发 PDF Ink 让我看到了现代浏览器 API(IndexedDB、PointerEvents、HTML5 Canvas)是多么强大,它能让开发者构建出尊重用户隐私的优秀离线优先工具。

祝大家学习愉快,写出让生活变得更简单的代码!