1. 为什么我们最终决定在Web端自建PDF编辑能力
先交代一下背景。我们是一个业务系统偏重的团队,手头有一套在线文档管理平台,用户在系统里上传合同、标书、图纸,原来只能下载后拿去本地改,改了再传回来。业务方的需求单写得很简单:"能不能直接在网页上改PDF?"结果需求评审一开,需求膨胀成了六页:要能写字、要能画矩形、要能加签名、要能插图片、要能高亮标注、还要能导出打印。
当时我也想过直接集成市面上成熟的付费SDK,但一比对授权费用和私有化部署要求,果断放弃。最后定下来的路线是:基于开源的PDF解析渲染引擎,加上自研的编辑交互层,把整个能力收敛成一套可复用的Web端PDF编辑器组件。
这篇文章不是纯代码教学,更多是把我们从零到一的设计思路、选型理由和踩坑经历讲清楚。如果你也在做网页端PDF相关的功能——不管是文档预览、编辑批注,还是后端生成PDF后的前端展示——这篇中的很多细节应该能帮你少走不少弯路。我会把技术要点、关键实现路径和实测数据都摆出来,尽量做到可以直接照着评估甚至落地。
先划一下能力的边界。我们最终交付的编辑器支持以下操作:全文文本选择和复制、批注框(文本注释)、矩形/椭圆/箭头绘图、自由画笔、图像粘贴插入、PDF页面合并拆分、文字水印与图片水印、导出下载、浏览器打印。不支持的是:对已有PDF内文字的语义级修改(也就是像Word那样改写原文字),做过的朋友都知道,这属于PDF编辑的天花板级难题,大部分在线编辑器其实也是通过"覆盖白底"或者"重排渲染"的方式实现的假编辑。
为什么先明确边界?因为PDF格式本身的设计目标就是"固定排版、不可变",它跟HTML/CSS这种流式文档的思路完全相反。所有编辑功能本质上都运行在渲染之上,每一次编辑操作都是在原内容上叠加新图层。想清楚这个底层逻辑,后续的技术路线就不会跑偏。
2. 技术选型对比:渲染引擎、编辑库和自研交互层各自解决什么问题
2.1 渲染层选型:pdf.js不可替代,但别只用它的默认Viewer
Web端处理PDF,绕不开Mozilla的pdf.js。几乎市面上所有开源的Web PDF方案,包括一些商业产品,底层都在用它。为什么?因为它在浏览器里实现了一套完整的PDF规范解析器,从词法解析、字体嵌入、图片解码到Canvas渲染,覆盖得非常全面,而且持续在维护。
但很多人会默认使用它的扩展版——pdfjs-dist自带的PDFViewer类,它提供滚动浏览、缩放、目录这些开箱即用的功能。可如果要做编辑层,用这个默认Viewer会有个问题:它的事件体系和DOM结构是围绕"阅读"设计的,编辑操作涉及的坐标定位、图层覆盖、对象拾取跟它的渲染机制耦合度很高,改起来很费劲。
我们的做法是:直接使用pdf.js的底层API——getDocument加getPage,自己管理页面渲染和缩放逻辑。也就是用PDFPageProxy.render()把页面画到Canvas上,然后自研一个层级管理模块。这样功能裁剪、事件监听、批量操作都很自由。代价是你要自己处理一堆细节,比如设备像素比(DPR)、滚动容器、虚拟列表。这些后面细说。
2.2 编辑库的取舍:为什么没选pdf-lib做前端核心编辑
开源社区还有个常用库叫pdf-lib,它可以创建、修改PDF文档,支持添加文本、绘制图形、旋转页面。很多团队想用它直接做编辑。我们做过技术预研,做了一个小Demo:用pdf-lib在PDF上写字、画线,然后保存导出。单看功能Demo,效果不错,代码量也很少。
但一放到完整的编辑器场景里,问题就出来了。第一,pdf-lib没有渲染能力,你必须在界面上先通过pdf.js把页面渲染出来,然后把用户在Canvas上的操作坐标映射回pdf-lib的数据模型。这个双重维护的复杂度很高。第二,编辑的"所见即所得"很难保证。比如你在界面上拖拽一个文本框,界面显示用的是HTML/CSS字体渲染,但导出时pdf-lib用的是嵌入PDF的字型渲染,两边的字形度量(字体宽高、基线位置)是两套体系,字稍微一多就出现错位。第三,pdf-lib对中文字体的嵌入支持不友好,默认字体不覆盖CJK字符,要自己准备外部字体文件,还得控制好子集化,否则导出的PDF体积会暴涨。
最后我们还是选择了一条更务实的路线:编辑层用Canvas图像合成思维——所有编辑操作都在渲染层叠加,导出时以"原PDF页面图片为底、编辑图层按坐标绘制上去"的方式生成新PDF。这种方式没有pdf-lib处理矢量字体的麻烦,导出的产物视觉上跟编辑器里所见完全一致。当然代价是导出结果是扁平化的,相当于图片型PDF,文本不可再编辑。但结合我们的业务场景——合同批注、流程审批、图纸标注——这个代价是可以接受的。
2.3 自研交互层的设计原则
选型完成后我们把整个模块分成了三层:
| 层次 | 职责 | 关键技术点 |
|---|---|---|
| 渲染层 | PDF页面解析与Canvas绘制 | pdf.js、离屏Canvas、DPR适配 |
| 编辑层 | 操作交互、图形绘制、历史记录 | Pointer Events、矩阵变换、可撤销栈 |
| 导出层 | 将编辑内容合成导出 | Canvas合成、PDF生成、打印控制 |
编辑层是工作量最大的部分,设计上我们定了几个原则:所有编辑对象(Shape、Note、ImageItem)都是独立的模型对象,存储的是"抽象坐标",界面上的Canvas尺寸变化不影响数据;编辑对象与渲染逻辑解耦——同一个对象在导出和界面预览时走两套渲染器,但数据源一致;操作历史只记录模型层的变更事件,不记录像素。
这个分层的好处是,后续如果要把导出层从"Canvas合成"换成"pdf-lib矢量绘制",只需要替换导出渲染器,编辑层的核心模型不用动。
3. PDF解析与Canvas渲染:把PDF准确"画"到浏览器里的实操细节
3.1 加载、解析与页面管理
用pdf.js加载PDF的基本流程是:
import * as pdfjsLib from 'pdfjs-dist'; import workerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'; pdfjsLib.GlobalWorkerOptions.workerSrc = workerUrl; const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer, cMapUrl: 'https://unpkg.com/pdfjs-dist@3.11.174/cmaps/', cMapPacked: true, standardFontDataUrl: 'https://unpkg.com/pdfjs-dist@3.11.174/standard_fonts/', }); const pdfDocument = await loadingTask.promise; const totalPages = pdfDocument.numPages;这里有几个容易踩的坑。第一,一定要设置cMapUrl和standardFontDataUrl,尤其是处理中文PDF时,部分嵌入字体不全的PDF会依赖CMap来映射字符编码,不配的话会出现乱码。第二,Worker的注册方式跟构建工具版本强相关,Webpack/Vite下用?url引入worker文件是目前比较干净的处理方式。
页面渲染核心代码大概是:
const page = await pdfDocument.getPage(pageNumber); const baseViewport = page.getViewport({ scale: 1.5 }); const outputScale = window.devicePixelRatio || 1; canvas.width = Math.floor(baseViewport.width * outputScale); canvas.height = Math.floor(baseViewport.height * outputScale); canvas.style.width = `${baseViewport.width}px`; canvas.style.height = `${baseViewport.height}px`; const renderContext = { canvasContext: canvas.getContext('2d', { alpha: false }), viewport: baseViewport, transform: outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null, }; await page.render(renderContext).promise;注意canvas.width设置的是物理像素尺寸,而canvas.style.width是CSS尺寸。如果不区分这两个,在高分屏(如MacBook的Retina屏)上渲染出来的PDF会发虚模糊。这个DPr适配是最容易忽略又最影响观感的问题。
3.2 虚拟滚动:大PDF文件不卡死的必备手段
如果PDF有几百页,全部创建Canvas会直接吃崩浏览器内存。一个A4页面在2倍DPR下,Canvas物理像素大约是1654 x 2339,占用内存接近15MB,100页就是1.5GB,这在大多数电脑上都会出问题。
我们的方案是虚拟滚动:维护一个可视区域(Viewport),根据滚动容器的当前偏移量计算哪些页面的上边界落在可视区内,只渲染可视区前后各1到2页,其他页面卸载或复用Canvas。页面离开可视区后,把Canvas尺寸置为0释放GPU内存,保留该页的渲染数据(PDFPageProxy对象),下次滑回来时重新渲染。
这个过程的性能数据供参考:在我们的实现中,一个150页、单页约200KB的PDF,首次加载加首屏渲染耗时约1.2秒,切换到相距较远的页(比如从第3页跳到第120页)渲染耗时约200ms,内存稳定在250MB左右。没有虚拟滚动前,全量渲染300MB左右的PDF,Chrome直接崩溃。所以虚拟滚动不是优化项,是必备项。
const containerRect = scrollContainer.getBoundingClientRect(); const scrollTop = scrollContainer.scrollTop; const pageHeights = pdfDocument.pages.map(p => p.height * currentScale); let startPage = 0; let accHeight = 0; for (let i = 0; i < pageHeights.length; i++) { if (accHeight + pageHeights[i] > scrollTop - containerRect.height) { startPage = i; break; } accHeight += pageHeights[i]; }首屏如果能第一页秒开,用户就会觉得这个系统很流畅;一旦要让用户等白屏,后续怎么优化体验都很被动。
3.3 渲染清晰度与加载速度的权衡
PDF页面渲染的scale参数直接决定清晰度。我们通过一个简单的公式控制:renderScale = baseRenderScale × zoomLevel,基础缩放用的是1.5(相当于150%),用户手动缩放时按0.5到4之间调整。
有个细节值得提一下:如果是纯预览模式(不编辑),可以用canvas.getContext('2d', { alpha: false })来加速,关闭透明通道能显著减少合成开销。但一旦加入编辑层,因为需要叠加图片、批注等内容,Canvas的alpha就必须设为true,否则编辑内容会与PDF内容一起被背景覆盖。这个取舍,实测性能差距大约在10%到15%左右,但在编辑场景下必须付出这个代价。
4. 编辑交互的实现:从指针坐标到PDF坐标的正确姿势
4.1 坐标系统:这是所有编辑功能的基石
Web端PDF编辑最容易出问题的地方就是坐标转换。PDF的坐标系原点在页面左下角,x轴向右,y轴向上;而浏览器DOM坐标系原点在左上角,y轴向下。pdf.js的getViewport方法已经帮我们处理了翻转,但需要注意——它返回的坐标是"CSS像素",不是"物理像素"。在做点击拾取、绘制映射时,必须对坐标进行变换。
我们定义了三层坐标:
- PDF抽象坐标:以PDF原始尺寸(即PDF页面的point单位)为基准,原点在左下角,这是文档保存时的标准坐标。
- 视口坐标:经过scale缩放和滚动偏移后的CSS像素坐标,用于界面显示和事件处理。
- Canvas像素坐标:Canvas的物理像素坐标,用于绘制。
用户交互时,事件层拿到的是浏览器clientX/clientY,要转到PDF抽象坐标,公式是:
function screenToPdf(clientX, clientY, page, containerRect, scrollTop, scrollLeft, scale) { const cssX = (clientX - containerRect.left + scrollLeft) / scale; const cssY = (clientY - containerRect.top + scrollTop) / scale; // 转PDF坐标,注意Y轴翻转 return { x: cssX, y: page.viewport.height - cssY, }; }这个y轴翻转几乎每个人都会写错一次。记住一个原则:PDF坐标向上增长,屏幕坐标向下增长。所有涉及拾取、绘制、命中检测的地方,都要先完成翻转再做逻辑判断。我们在开发时封装了一个统一坐标变换模块,所有图层都通过它转换,这样即便后面DPI变化、缩放变化,也不用改业务代码。
4.2 批注框与自由画笔的交互设计
批注框是我们使用频率最高的功能。它的实现不算复杂,但有个产品层面容易忽略的问题:编辑框的文本编辑是依赖DOM的(contenteditable或者textarea),而其他图形元素是绘制在Canvas上的,两套体系如何协调?
我们的做法是:批注框分"查看态"和"编辑态"。查看态用Canvas绘制一个圆角矩形加文本内容(Canvas不能直接排版文本,我们用measureText做了简单的自动换行);双击进入编辑态后,在Canvas上方浮出一个绝对定位的textarea,它的位置和尺寸通过pdfToScreen逆变换算出来,字体、行高与Canvas绘制时的参数保持一致。失焦后,将文本内容写回模型对象,销毁DOM节点。
这种方式维护了两个渲染分支,但体验上比"一直用DOM叠在Canvas上"更可控。因为Canvas与DOM的叠加层层级处理,往往比想象中更麻烦——尤其当用户缩放页面时,DOM层的字号和位置需要重新计算,不做就是错位。
自由画笔稍微简单些:pointerdown时记录起点,pointermove时不断往当前路径里推点,每个点都是PDF抽象坐标,pointerup时闭合。但要注意优化点:如果每个mousemove事件都重绘一次整个Canvas,会有明显卡顿。我们的优化是分两个Canvas:交互过程中在临时Canvas上绘制,"笔迹+原有内容"作为一个整体,鼠标抬起时才把临时Canvas合成到主Canvas。这也是大多数绘图应用的标准做法。
4.3 图层叠加顺序与撤销重做
编辑对象之间的叠放顺序,我们给每个编辑对象维护一个zIndex字段,渲染时按zIndex排序。对于大量批注的场景(比如一个页面有20多个批注),每重绘一次就排序一次成本较高,我们的方案是只在对象添加和移动时触发排序,重绘时不再重复计算。
历史记录(撤销/重做)的实现遵循一条原则:只记录操作命令,不记录状态快照。每个操作实现为{ type: 'add' | 'remove' | 'update', payload: {...} },栈顶保存的是操作命令,撤销时执行反操作。这样即便一个批注框文本被修改了100次,内存里也只存100个小对象,不会因为保存完整页面快照导致内存爆掉。
实测中,单个页面100个编辑操作的撤销/重做在毫秒级完成,而如果采用快照方式,每操作一次就得保存整个Canvas像素,100步之后内存必然爆炸。
4.4 文本水印:看起来简单但细节很多
水印功能是需求方最坚持的一个点。文本水印有两个关键参数:旋转角度和透明度。
ctx.save(); ctx.translate(x, y); ctx.rotate(angle * Math.PI / 180); ctx.globalAlpha = opacity; ctx.fillText(text, 0, 0); ctx.restore();水印的排版策略很重要:水平平铺,两两间距设为水印文本宽度的两个字符宽,行间距等于两倍的文本高度。如果间距太密,会遮住正文;太疏,起不到防复印的作用。
另外,水印必须独立成一个图层对象。原因是我们做导出时要把水印位置精确复刻到新PDF上,如果水印逻辑和画布绘制耦合在一起,导出层就得重新算一遍位置,很容易出错。独立出对象模型后,导出时遍历水印对象列表,逐个绘制即可。
5. 导出、打印与常见坑:编辑结果如何回到PDF世界
5.1 导出为PDF:Canvas合成方案详解
我们最终的导出方案是"新版PDF = 原PDF页面矢量内容 + 编辑图层图像化覆盖"。具体做法是:先复制一份原PDF的数据(用pdf-lib的PDFDocument.load()加载原始ArrayBuffer),对每一页,将原页面渲染到Canvas(scale设为2,保证导出清晰度),然后在Canvas上绘制所有编辑对象,最后把Canvas导出为PNG图片,替换到新PDF的页面中。
const modifiedPdf = await PDFDocument.load(originalBuffer); const pages = modifiedPdf.getPages(); const canvas = document.createElement('canvas'); canvas.width = pages[0].getWidth() * 2; canvas.height = pages[0].getHeight() * 2; const ctx = canvas.getContext('2d'); // 绘制原页面(用pdf.js渲染) await originalPage.render({ canvasContext: ctx, viewport }).promise; // 绘制编辑层 editorLayer.render(ctx); // 写入新PDF const pngImage = await modifiedPdf.embedPng(canvas.toDataURL('image/png')); pages[0].drawImage(pngImage, { x: 0, y: 0, width, height });这里有个细节:导出的DPI。Canvas纹理按2倍像素导出,对应PDF约为144DPI,在屏幕阅读和一般打印场景下足够清晰。如果业务要求300DPI印刷级清晰度,需要把scale提高到4倍以上,但导出耗时也会成倍增加,内存占用要提前做好准备。我们实测过一次300DPI导出,渲染一张A4页面耗时约700ms,导出6页文档总时长约5秒,还在可接受范围内。
5.2 浏览器打印的两种方案对比
打印PDF,我们试过两种方案。方案一是直接用浏览器原生的iframe加载PDF,用户调浏览器打印对话框。优点是零开发量,但缺点很致命:无法控制打印范围,用户选"打印当前页"或者"打印选定区域",浏览器不会按PDF的页面结构来输出,经常出现打印内容看不全。方案二是前端生成好新PDF后,将二进制数据转成Blob,喂给浏览器打印:
const blob = new Blob([pdfBytes], { type: 'application/pdf' }); const url = URL.createObjectURL(blob); const printWindow = window.open(url); printWindow.onload = () => { printWindow.print(); URL.revokeObjectURL(url); };方案二唯一的问题是window.open会被浏览器弹窗拦截器拦截,必须放在用户手势的同步流程里。我们的处理是:用户点击"打印"按钮时先同步拿到Blob URL,保存到一个全局变量中,再window.open(此时open事件栈里还有用户手势上下文),随后打印窗口加载时自行调用print()。
5.3 中文乱码与字体问题排查实录
导出PDF时遇到过一个很诡异的bug:界面预览一切正常,导出的PDF打开后中文文本全部变成了方框。排查链路是这样的:
- 第一步检查pdf.js渲染层——预览正常说明解析没问题。
- 第二步怀疑pdf-lib嵌入PNG图片时丢字——但导出的是图片型PDF,PDF里并没有文本对象,理论上不存在字体问题。
- 第三步用
pdf-lib的embedFont测试直接添加文字,发现默认的StandardFonts.Helvetica确实不支持中文,中文全部变方框。 - 最终定位到:部分入职时间较早的业务人员使用的是旧版浏览器,那个浏览器的Canvas在
toDataURL('image/png')时对非嵌入的系统字体渲染存在兼容问题,导致页面中的系统字体实际没画上去。
解决办法有两个,任选其一:页面里的文本对象都使用网络中立的字体加载方案,统一走@font-face引入字体文件;或者强制使用系统字体栈并锁定浏览器内核版本。我们最后选的是前者,问题彻底解决。如果你也遇到"界面正常但导出异常"的情况,建议先怀疑Canvas合成环节的字形渲染,而不是PDF生成环节。
5.4 图像增强需求:偏斜校正与清晰度处理
有人在我们社区留言提到"pdf歪斜校正纠偏、漂白加深清晰"的需求,这个我们确实做过一版。原理是在导出前对Canvas做图像处理:
- 偏斜校正:通过Hough变换检测文本行的倾斜角度,然后用Canvas的
rotate进行反向旋转,角度精度能控制在0.1度以内。 - 漂白:遍历像素,提高亮度、降低饱和度,实现“去底纹”效果。
- 加深清晰:用卷积核做锐化,突出文本边缘。
这些处理适合扫描件PDF的二次加工场景。但性能要吃不少——一个2000x3000像素的Canvas,纯遍历像素做漂白大约需要150ms到300ms,如果用Web Worker并行处理,可以压到80ms左右。处理完后要交给后台线程去执行,避免阻塞主线程导致页面卡死。
6. 性能优化与兼容性排坑:我们踩过的和你们会踩的
6.1 Service Worker注册失败的坑
这个坑跟PDF本身无关,但做Web端PDF编辑时很容易碰到。我们为了缓存PDF解析器的CMap和字体资源,注册了Service Worker。上线后监控系统报了一个异常:
加载 web 视图时出错: error: could not register service worker: invalidstatee翻译过来就是"InvalideStateError"。排查过程整理一下:
- 第一步,在本地跑,一切正常,Chrome无报错。因为localhost是Secure Context,Service Worker允许注册。
- 第二步,测试环境也没问题,因为我们测试环境强制HTTPS。
- 第三步,生产环境部分用户上报异常,而且集中在某个老旧浏览器内核,发现它不支持
navigator.serviceWorker的部分API,注册后抛错。
但根源不只是浏览器版本问题。我们的代码没有做兜底:注册SW是用if ('serviceWorker' in navigator)判断的,这个条件在某些WebView环境下是true,但后续navigator.serviceWorker.register()却会因为跨域或安全策略抛出异常。老内核的WebView容易踩这个坑。
修复方案是在注册外层加了一层完整的安全检查,统一catch异常并降级为不缓存:
if ('serviceWorker' in navigator && window.isSecureContext) { navigator.serviceWorker.register('/sw.js') .then(() => console.log('SW registered')) .catch(err => { console.warn('SW registration failed, fallback to no-cache mode', err); }); }注意window.isSecureContext这个判断很关键,它能拦住所有非HTTPS上下文下的SW尝试。所有依赖解析器资源的操作都要有"SW不生效也能跑"的后备方案。PDF解析器相关的CMap资源,我后来做了本地嵌入到JS包里的方案,不依赖外部URL,这样即便SW失败,解析行为不会失效。
6.2 内存泄漏与离屏Canvas复用
长时间操作编辑器后,页面会越来越卡,最典型的原因是Canvas对象没有释放。我们在代码评审中发现的几类问题:
- 编辑器关闭时只移除了DOM节点,但没有把Canvas的尺寸置为0,浏览器回收Canvas内存依赖GC,不及时。
- 虚拟列表中离开可视区的Canvas没有主动
width = 0,每页十几个MB,用户滚几十页就攒了几百MB。 - 撤销操作中的编辑对象绑定了事件监听器,GC无法回收闭包引用的编辑对象。
改进方案是统一封装一个CanvasPool:所有Canvas对象创建和销毁都走池子管理,销毁时统一清空事件、置0尺寸、移除引用。同时,编辑器销毁时做一次全量清理,把canvas清除,防止内存长期驻留。这个优化上线后,连续编辑1小时后再看内存,稳定值比之前低约35%。
6.3 大文件与超高页数PDF的限制策略
单页超大(比如图纸类的A0幅面)或页数超多(数千页的扫描件),纯前端渲染不太现实。我们的做法是加一道"预检门槛":
- 文件大小超过100MB,直接提醒走后端解析,前端只做展示编辑后的结果。
- 页数超过500页,前端默认只渲染前20页,用户按需加载下一页。列表式载入可以有效避免首屏白屏几秒。
- 单页像素超过4096px时,渲染scale从1.5降到1.2,防止Canvas超出浏览器最大纹理尺寸限制。
这道门槛不是限制能力,而是保护体验。真遇到超大文件,把重活丢给后端服务是最合理的解法。前端的定位是"轻量编辑",而不是"万能阅读器"。
6.4 生成PDF体积控制
导出时如果把原PDF页面和编辑层合成到一张PNG图片,体积会比原文件大好几倍。我们针对这个做过体积优化:检测原PDF页面是否白底、是否纯文本页,如果是,则导出时不再嵌入PNG图,而是用pdf-lib重新绘制文本图层,体积会小很多。实测一个600KB的中文合同导出后,走了文本重绘路径的体积约1.2MB,而走全图嵌入路径的体积会到8MB以上。如果你的编辑场景主要是批注和签名,强烈建议考虑用pdf-lib做矢量重建,对最终文件的体积和可搜索性都有很大帮助。
7. 我个人的几个经验总结,以及后续扩展方向
做这套Web端PDF编辑器,前后投入大约三个月,核心代码量不算大,真正的难度全在细节上。挑几个人印象最深的点说一下。
坐标系统的统一是开发阶段花费时间最多的部分。PDF坐标系和屏幕坐标系的转换、Canvas的DPR适配、滚动偏移量累积误差,任何一个环节少一个变换,绘制出来的图形就会偏移。建议在项目早期就封装好一套独立的坐标变换工具,并给每个变换函数写单元测试,避免后期调试时怀疑人生。
第二点是关于需求的边界把控。技术上能做的、业务上需要的和用户体验上可接受的,三者要严格区分。我们中途差点被带偏去做"PDF文本原样编辑",后来发现投入产出比实在太低,果断放弃,转而把体验打磨得足够顺滑。如果你的团队也面临类似需求,我建议一开始就明确"底层是图层叠加而非文本重排",尽早说服业务方,这样可以节省大量开发成本。
第三点是性能优化一定不能放在最后统一做,从架构设计那天起就要考虑。虚拟滚动、Canvas复用、历史记录的内存策略,这些应该在写第一行业务逻辑之前就想好方案,否则后面改造的成本比新写还大。
关于后续扩展,短期内我们打算做的是:把导出的PDF尺寸控制做成可配置项,让用户选择"清晰优先"还是"体积优先";把批注的样式模板做成可复用组件;此外,我们还在调研OCR识别和PDF转Word的混合能力,把"编辑"从图形层延伸到内容层。这个方向的前景很好,但复杂度也在翻倍,需要一步步来。
最后一个实用小技巧分享给做这块的朋友:调试Canvas里绘制出来的PDF文字时,在Chrome DevTools里勾选Rendering -> Emulate CSS media type = print,能帮你提前看到打印时会遇到的样式排版问题。很多"导出PDF和预览不一致"的坑,都是出在这个点上。