ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

H5 PDF 预览高亮实战:文本层与坐标换算两种方案详解

H5 PDF 预览高亮实战:文本层与坐标换算两种方案详解 做 H5 里的 PDF 预览很多人第一反应就是“把 PDF 转成图片放上去”。但这个思路在遇到“高亮标注”需求时立刻就会卡住转成图片后文字选不中、搜不到标注位置一缩放就错位。这篇文章我会从实战角度拆解两条真正可落地的方案一是基于 pdf.js 的文本层高亮适合关键词定位、文字批注二是图片化渲染 坐标换算的框选高亮适合自由涂鸦、扫面件标注。两种方案我都已经在真实项目中跑通过附上可直接参考的核心源码和避坑记录。不管你是做合同审批、在线阅卷、图纸批注还是给自媒体平台做资料预览工具这篇内容都值得看完。尤其是移动端 H5 嵌入微信公众号、企业微信的场景文本层错位、Canvas 模糊、触摸事件冲突这些坑我会一并讲透。1. 方案选型前的关键思考先搞清楚你到底要“高亮”什么1.1 两种渲染路线的本质差异PDF 这个格式很特殊。它既不是一张大图片也不是一份纯文本文档而是一套“页面坐标 文字内容 矢量图形”的混合描述。一个 PDF 文件里文字是带坐标的文字图形是带坐标的图形。这就给了前端开发者两条完全不同的渲染路径。第一条路径是用 pdf.js 在 Canvas 上按原始数据把整页画出来再在上层叠加一个“文本层”textLayer。文本层里每一个字都是真实的 HTML span位置经过计算后绝对定位到 Canvas 上方。用户看到的和 Canvas 渲染出来的内容完全重合但这些 HTML 文字是可以被鼠标选中、被 JS 搜索、被 CSS 加上背景色的。高亮天然支持而且精度可以精确到单个字符。第二条路径是先把 PDF 的每一页渲染成一张图片PNG 或 JPEG扔到页面上当底图然后允许用户在这一层图片上面画框、划线、涂荧光笔。这一方案不关心 PDF 内部有没有文字结构只关心“标记的坐标怎么和图片对得上”。扫描件、PPT 导出的 PDF、图纸类文件走这条路最合适。理解了这个差异后面所有技术选型就顺理成章。你要做的是文字语义级别的高亮比如合同里搜“违约责任”并标黄还只是视觉标记级别的批注比如手动画一个圈决定了你选哪条路。1.2 常见业务需求与推荐方案对照业务场景核心需求推荐方案原因合同/法律文书自动搜索关键词并标黄用户选中文字做批注方案一textLayer 文本高亮必须依赖文本结构支持语义匹配和复制论文/报告在线审阅自由框选、划线、箭头标注方案二图片化坐标高亮标注要“像在纸上画一样”不关心文字本身扫描件/历史档案手写批注、框选重点区域方案二扫描件本身没有文本层方案一直接失效混合型资料库既有文本 PDF 又有扫描件两种方案结合先用getTextContent判断页面有无文本有文本走方案一无文本降级方案二做产品规划的时候我习惯把高亮需求拆成“文本级”和“几何级”两层。文本级高亮考虑的是“这句话/这个词怎么找到”几何级高亮考虑的是“这个区域怎么圈出来”。一旦你想让用户同时拥有这两种能力就得准备两套渲染系统而不是硬用一套方案二打天下。2. 方案一基于 pdf.js 文本层实现文本级高亮2.1 环境准备与依赖引入方案一的核心依赖是 Mozilla 的 pdf.js。这个库既能解析 PDF、把内容绘制到 Canvas也提供了内置的 TextLayer 实现用来生成覆盖在 Canvas 上方的可选中文字层。我建议直接用 npm 包引入方便打包工具做版本管理和代码分割npm install pdfjs-dist如果你的项目是 Vite 构建可以这样配置 worker。worker 负责在后台线程完成 PDF 解析避免阻塞 UI 渲染线程不配的话控制台会一直报错import * as pdfjsLib from pdfjs-dist; // Vite 环境下通过 ?url 拿到 worker 文件的最终地址 import PdfWorker from pdfjs-dist/build/pdf.worker.min.mjs?url; pdfjsLib.GlobalWorkerOptions.workerSrc PdfWorker;如果是用 CDN直接在 HTML 里引入script srchttps://unpkg.com/pdfjs-dist3.11.174/build/pdf.min.js然后单独设置 workerSrc 指向同版本目录下的pdf.worker.min.js。版本必须严格一致否则会报“API version”不匹配的错。2.2 第一页渲染Canvas 与 DPR 适配先把 PDF 第一页渲出来。这一步看似简单但有一个所有前端工程师都会踩的坑Canvas 模糊。直接按 CSS 尺寸设置 canvas.width 和 canvas.height 会导致高分屏上发虚。正确做法是按devicePixelRatio放大绘图缓冲区再把 Canvas 的 CSS 尺寸设回正常大小async function renderPage(pdf, pageNum, canvas, containerWidth) { const page await pdf.getPage(pageNum); const baseScale containerWidth / page.getViewport({ scale: 1 }).width; const viewport page.getViewport({ scale: baseScale }); const dpr window.devicePixelRatio || 1; canvas.width Math.floor(viewport.width * dpr); canvas.height Math.floor(viewport.height * dpr); canvas.style.width ${viewport.width}px; canvas.style.height ${viewport.height}px; const ctx canvas.getContext(2d); ctx.scale(dpr, dpr); await page.render({ canvasContext: ctx, viewport: viewport, }).promise; return { page, viewport }; }这里的思路是PDF 内部默认以 ptpoint为单位计算尺寸。getViewport({ scale: 1 })得到的是 1:1 的原始尺寸scale越大渲染出的 Canvas 像素就越大。我们根据容器宽度动态算scale保证 PDF 页面宽度撑满容器。同时把 DPR 乘进去这样在 iPhone 上看着也锐利不会糊成一片。2.3 叠加文本层让文字“可选中、可搜索”pdf.js 的 TextLayer 是整套方案的灵魂。它接收解析出的文本内容和 viewport在指定容器内生成一系列绝对定位的 span每个 span 承载一个词或字符并用 CSS 的 transform 精确定位到 Canvas 上方。新版 pdf.js3.x 以上用法如下import { TextLayer } from pdfjs-dist; async function createTextLayer(page, viewport, container) { const textLayerDiv document.createElement(div); textLayerDiv.classList.add(text-layer); container.appendChild(textLayerDiv); const textLayer new TextLayer({ textContentSource: page.streamTextContent(), // 流式读取文本性能更好 container: textLayerDiv, viewport: viewport, }); await textLayer.render(); return textLayerDiv; }配套的 CSS 是固定的那套几乎不需要改.text-layer { position: absolute; top: 0; left: 0; overflow: hidden; opacity: 0.25; /* 平时很浅鼠标选中时会显示系统选区样式 */ line-height: 1; text-size-adjust: none; -webkit-text-size-adjust: none; } .text-layer span { position: absolute; white-space: pre; transform-origin: 0% 0%; color: transparent; }关键点在于.text-layer span的文字颜色是透明的所以要设置 opacity 用来调试。.text-layer层的 pointer-events 默认是 auto用户才能在这层上面选中文字。当用户选择文本时浏览器的高亮背景色会覆盖住底下 Canvas 里画出来的那一层从而产生“选中 PDF 文字”的真实效果。2.4 关键词自动高亮遍历 span 加标记文本层渲染完成后获取到的是一堆绝对定位的 span。我们可以遍历这些 span用正则匹配文本内容给命中部分包一层mark。因为 span 本来就定位在 Canvas 的正确坐标上内部加标记不会引起布局偏移function escapeRegExp(str) { return str.replace(/[.*?^${}()|[\]\\]/g, \\$); } function highlightKeyword(textLayerDiv, keyword) { const spans textLayerDiv.querySelectorAll(span); const reg new RegExp((${escapeRegExp(keyword)}), gi); spans.forEach(span { if (!span.dataset.fontSize) return; // 只处理文本节点 const text span.textContent; if (!text || !reg.test(text)) return; reg.lastIndex 0; span.innerHTML text.replace(reg, mark classpdf-hl$1/mark); }); }样式上给.pdf-hl设置黄色背景加圆角位置自然就对得上.pdf-hl { background: rgba(255, 235, 59, 0.7); border-radius: 2px; box-shadow: 0 0 0 1px rgba(255, 235, 59, 0.3); color: transparent; }这里有几个坑要提醒一下。第一span.dataset.fontSize是 pdf.js 生成的标记能用来过滤非文本节点。第二正则 lastIndex 重置很重要否则在全局模式下二次调用会跳过部分匹配。第三如果关键词跨了两个 span比如一个词被 pdf.js 拆到两个 span 里上面这段代码会漏命中。要处理这种情况就必须把相邻 span 的文本拼起来做整体匹配匹配到后按字符偏移量再拆分到各 span 上。实现稍复杂但用在合同条款这类长文本上非常值。2.5 用户手动选择文字添加高亮除了搜索自动高亮更常见的是用户用鼠标框选一段文字然后点击“高亮”按钮。原理是拿到浏览器Selection对象的Range遍历 Range 覆盖到的文本层 span给它们加背景色。核心代码document.getElementById(btn-highlight).addEventListener(click, () { const selection window.getSelection(); if (!selection || selection.rangeCount 0) return; const range selection.getRangeAt(0); const textLayer document.querySelector(.text-layer); // 判断选区是否完全落在文本层内 if (!textLayer.contains(range.startContainer) || !textLayer.contains(range.endContainer)) { alert(请选中 PDF 文本后再操作); return; } // 遍历选中范围内的每一段文本 const highlightNodes []; const walker document.createTreeWalker(textLayer, NodeFilter.SHOW_TEXT); let node; while ((node walker.nextNode())) { if (!node.parentElement.dataset.fontSize) continue; if (range.intersectsNode(node)) { // 简单处理给整个 span 加背景 node.parentElement.classList.add(user-highlight); highlightNodes.push(node.parentElement); } } // 保存高亮记录便于后续回显 saveHighlightRecords(highlightNodes); });.user-highlight样式换成橙色或你业务定的主题色.user-highlight { background: rgba(255, 152, 0, 0.6); border-radius: 2px; color: transparent; }注意这个写法是为演示做了简化。实际生产里用户选中的可能只是 span 的一半给整个 span 加背景会导致高亮范围比选中范围大。想做到精确需要 cloneRange 后用extractContents按文档结构拆出选中片段再把片段包进 mark 元素插回 span 里。这里不再展开有兴趣可以自行研究 Range API思路和我上面用的 TreeWalker 一致。2.6 方案一的边界问题扫描件、大文件和字体缺失文本层方案有一个天然的物理边界PDF 里必须真的有文本。扫描件本质上是一张图片包在 PDF 容器里pdf.js 解析出的textContent几乎为空。遇到这种文件getTextContent 返回的 items 数组里的每一项都是一个空字符串文本层渲染出来是透明的、什么都选不中。其次是性能问题。一个几百页的 PDF如果一次性把所有页面全部渲染成 Canvas 再加文本层移动端浏览器会直接崩溃。我的做法是“懒加载 页回收”只渲染当前可视区和前后两页滚出视口就销毁 Canvas 并清除 textLayer 的 innerHTML。这个优化对长篇文档是必须的。还有一类问题是字体。某些 PDF 使用自定义字体子集或者字符映射表损坏提取出的文本会变成乱码或方块。这种情况不是代码能解决的只能建议用户去源头修正文档。所以方案一上线前一定要准备降级策略否则会被真实业务里的“奇怪 PDF”反噬。3. 方案二图片化渲染 坐标框选高亮3.1 核心思路不依赖文本的纯几何标注如果需求允许不选中文字只允许用户在 PDF 任意位置画框、画线、涂荧光笔那方案二更简单粗暴也更能抗造。做法是先用 pdf.js 把每一页渲染到一个离屏 Canvas然后导出为图片 URL放到页面里当底图。之后的所有高亮都是在这张图片上面叠加一层透明的 SVG 或 Canvas用户画什么就记录什么坐标。这套方案的优点很明显不依赖 PDF 内容结构扫描件、图纸、老档案都可以处理高亮形态自由矩形、圆形、自由画笔、箭头只有想不到没有做不到渲染开销可控图片可缓存重复打开不重新解析 PDF标注数据天然是 JSON好存储也好同步缺点也直接文字不能选中、不能搜索、不能复制而且图片放大到高倍率会模糊。所以它适合批注型场景不适合阅读型场景。3.2 把 PDF 页面变成图片用 pdf.js 拿到页面后生成图片的代码很简单。但有几个优化点要讲一下async function renderPageToImage(pdf, pageNum, scale 2) { const page await pdf.getPage(pageNum); const viewport page.getViewport({ scale: scale }); const canvas document.createElement(canvas); canvas.width viewport.width; canvas.height viewport.height; const ctx canvas.getContext(2d); await page.render({ canvasContext: ctx, viewport }).promise; // 推荐用 blob URL避免超长 dataURL 撑爆内存 const blob await new Promise(resolve canvas.toBlob(resolve, image/png)); return URL.createObjectURL(blob); }scale直接取 2 是因为一般 H5 页面宽度在 375px 到 768px 之间PDF 原始页面宽度可能只有 600 pt2 倍缩放渲染出来的图片在大多数屏幕上够清晰也不会太大。如果是 A3 图纸或超宽表格可以按业务调高。注意scale不要无限调高图片体积和渲染耗时都会爆炸。展示时用一个容器包住 img外面包一层 wrapper然后让图片宽度自适应容器.pdf-viewer { position: relative; margin: 0 auto; } .pdf-viewer img { display: block; width: 100%; height: auto; pointer-events: none; } .pdf-viewer .annotation-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; /* 防止标注层挡住图片的缩放手势 */ }3.3 用户绘制高亮坐标换算才是最大难点很多人在这一步以为“画个矩形很简单”真正动手才发现坐标换算能把人绕晕。我在方案二里维护的标注数据结构是这样的{ page: 1, type: rect, // rect | highlight | pen | arrow color: #ffcc00, opacity: 0.5, strokeWidth: 3, points: [ { x: 120, y: 340 }, { x: 480, y: 560 } ] }points 里的 x, y 存的是 PDF 页面坐标单位是 pt也就是viewport.scale 1时的坐标。这样最保险无论用户当前缩放到多少倍、容器宽多少页面坐标都是固定的锚点展示时随时可以换算回屏幕坐标。用户绘制时鼠标事件拿到的坐标是屏幕像素坐标。如果图片当前刚好宽度等于渲染时 viewport 宽度那么换算公式很简单function getDrawingXY(e) { const rect viewer.getBoundingClientRect(); const pointPage1 viewport.convertToPdfPoint( (e.clientX - rect.left), (e.clientY - rect.top) ); // convertToPdfPoint 返回的是 PDF 页面坐标 return { x: pointPage1[0], y: pointPage1[1] }; }但实际页面往往缩放了图片显示宽度和 viewport 宽度不一致。此时要先把屏幕坐标换算成 viewport 坐标再交给 convertToPdfPointfunction screenToPdfPoint(clientX, clientY) { const rect viewer.getBoundingClientRect(); const scaleX rect.width / viewport.width; // 当前缩放比 const scaleY rect.height / viewport.height; const viewportX (clientX - rect.left) / scaleX; const viewportY (clientY - rect.top) / scaleY; const pdfPoint viewport.convertToPdfPoint(viewportX, viewportY); return { x: pdfPoint[0], y: pdfPoint[1] }; }反过来渲染已有标注时把 PDF 坐标转成屏幕 CSS 坐标function pdfPointToScreen(pdfX, pdfY) { const viewportPoint viewport.convertToViewportPoint(pdfX, pdfY); const rect viewer.getBoundingClientRect(); const scaleX rect.width / viewport.width; const scaleY rect.height / viewport.height; return { x: viewportPoint[0] * scaleX, y: viewportPoint[1] * scaleY }; }所以每一次用户绘制都走“屏幕坐标 → PDF 坐标 → 存入数组”每一次刷新视图都走“PDF 坐标 → 屏幕坐标 → 绘制 SVG”。中间不要省略任何一次换算。我见过很多项目直接存屏幕坐标缩放下全乱套然后再去“对位修复”最后修到怀疑人生。3.4 用 SVG 还是 Canvas 绘制高亮方案二的高亮绘制层我更推荐 SVG不推荐再套一个 Canvas。原因有三点。第一SVG 天然是基于矢量坐标的缩放不会变糊和“底图是点阵图片”形成互补。第二SVG 图形是 DOM 节点可以单独绑定事件、修改属性、删除节点调试方便。第三小数据量的标注图形几十个矩形/线条SVG 性能完全够用没必要上 Canvas。核心渲染逻辑function renderAnnotations() { const svg document.getElementById(annotationSvg); svg.innerHTML ; // 简易清空实际项目建议用虚拟列或局部更新 annotations.forEach(ann { if (ann.type rect) { const p1 pdfPointToScreen(ann.points[0].x, ann.points[0].y); const p2 pdfPointToScreen(ann.points[1].x, ann.points[1].y); const rect document.createElementNS(http://www.w3.org/2000/svg, rect); rect.setAttribute(x, p1.x); rect.setAttribute(y, p1.y); rect.setAttribute(width, p2.x - p1.x); rect.setAttribute(height, p2.y - p1.y); rect.setAttribute(fill, ann.color); rect.setAttribute(fill-opacity, ann.opacity); rect.setAttribute(stroke, #333); rect.setAttribute(stroke-width, 1.5); svg.appendChild(rect); } }); }注意.annotation-layer的 SVG 要设置pointer-events: none避免挡住底部图片的滚轮缩放和触摸滑动。如果允许用户编辑已有标注拖动、删除再在单个图形上开启pointer-events: all。3.5 标注数据的保存与回显方案二的一大优势是标注即数据。每次绘制结束把 annotation 对象 push 到数组可以立刻回显也可以等待用户统一保存时发给后端。保存时不需要传图片只传页面号和标注集合的 JSON。后端如果想生成带标注的最终 PDF比如在合同上盖“已审阅”的矩形可以基于这份 JSON 在服务端用 PDF 库重新叠加图形。前端只负责采集坐标和预览。重新打开时流程是先渲染 PDF 页面为图片或从缓存里取后端返回标注 JSON调用renderAnnotations()把每一条标注从 PDF 坐标换算成当前屏幕坐标画出来这个方案天然支持多人协作A 标注的数据同步给 BB 打开页面后能看到 A 的批注而且标注数据量极小几十页 PDF 的标注撑死也就几十 KB。4. 两条路线怎么选性能、兼容与体验对比4.1 一图看懂两种方案的差异下面这张表是我在项目里最常用来跟产品和同事对齐的基本照抄就能用对比维度方案一textLayer 文本高亮方案二图片化坐标高亮文字可选/可搜/可复制支持不支持关键字自动定位高亮支持准确到字符需要额外 OCR成本高扫描件支持不支持支持自由画笔/箭头/任意几何需额外叠加层天然支持标注数据量需要记录字符区间/文本内容记录坐标点集极小开发难度中高需处理文本层细节中难点在坐标换算移动端兼容文本层在 iOS 上偶发选择失效稳定内存占用需同时维护 canvas 文本 DOM图片缓存后更省适合场景合同、论文、书籍阅读批注图纸、扫描件、表单、PPT4.2 主流项目的选型建议我从今年实际接的几个需求里总结了一个比较实用的判断办法拿三份文件丢给同一套代码看getTextContent返回的数据量同时确认产品要不要“文字语义级”的交互。如果产品希望用户能搜索 PDF 里的内容或者高亮后还能看到原文字直接锁定方案一如果产品只要求“像在纸上用荧光笔画一遍”方案二更稳如果产品是混合资料库建议两套并存。先尝试方案一如果发现文本层内容为空items.length 0自动切换成方案二如果对标注结果要生成最终盖章 PDF方案二是唯一合理选择因为坐标数据可以直接对接服务端 PDF 叠加逻辑很多团队在这里犯的致命错误是一开始觉得方案二简单就上了产品过两周提了一个新需求“把搜索到的文本高亮出来”于是整个坐标体系崩盘重做。反过来也有团队硬要用方案一做扫描件批注最后逼得自己引入 OCR 识别文本层复杂度翻倍。技术选型永远先问业务再谈架构。4.3 混合方案的协同策略我现在的默认做法是做一个统一渲染器先识别 PDF 每页是否有文本有文本则挂载 textLayer 和键盘搜索没文本则自动降级为图片模式并开放框选工具。这样同一个页面能同时处理电子合同和纸质扫描件存档用户在界面上感知不到切换只有开发知道背后是两套系统。技术上实现也不复杂pdf.js 的getPage()后调用getTextContent()拿到 items 数组长度超过 3 就认为有文本。判定靠的是实际内容数而非页数因为有可能整页只有一个图片说明字段这种情况下即使算“有文本”文本高亮价值也不高。5. 实操实录常见问题与排查技巧5.1 pdf.worker 加载失败解析卡白屏报错信息通常是这样的Failed to fetch dynamically imported module: .../pdf.worker.min.mjs DOMException: Failed to set the src attribute on a script element解决办法就一条GlobalWorkerOptions.workerSrc必须指向可访问的静态文件地址。Vite 下用?url引入最省心Webpack 5 下可以用new URL(pdfjs-dist/build/pdf.worker.min.js, import.meta.url)。如果你的项目 CDN 部署路径带前缀务必在 workerSrc 里也用/your-prefix/pdf.worker.min.js否则打包后路径是错的。5.2 Canvas 永远模糊字体像蒙了一层雾九成原因是忽略了 devicePixelRatio。devicePixelRatio是 2 的手机上如果 Canvas 像素尺寸等于 CSS 尺寸那一个 CSS 像素点只有 1x1 物理像素支撑自然糊。解决办法就是我 2.2 节写的canvas.width 乘以 dpr再用 ctx.scale(dpr, dpr) 放大坐标系。还有一小撮情况是底图用了width: 100%但源图分辨率太低。这种情况只能提高渲染 scale没有什么后端技巧能无中生有。5.3 文本层和 Canvas 错位高亮和字对不上文本层错位是方案一最磨人的地方。我排查过几次后总结了三个高频原因canvas 和 textLayer 的容器尺寸不一致。textLayer 应该被放进一个绝对定位的 wrapper 内和 canvas 同时作为子元素不要让它们各自撑开父容器设置了容器 padding 或 margin导致坐标基准点偏移。解决办法是让 textLayer、canvas 都position: absolute; top: 0; left: 0;放在同一个position: relative的父级里手动缩放浏览器页面时没有及时按新尺寸重置 viewport。需要在 resize 时重新渲染整个页面如果排查完还是差一两个像素多半是字体渲染行高差异引起。可以在 textLayer 样式里设置line-height: 1并把transform-origin设为0% 0%基本能修复。5.4 移动端触摸画高亮时页面跟着滚H5 里用户想用手指在 PDF 上画矩形手指一滑页面先滚动了标注画到一半飞走。这个问题的根源是画布默认的 touch-action 是 auto浏览器认为你的拖动是一个滚动手势。解决方案是在标注工具激活时给 SVG 层设置.annotation-layer.enabled { touch-action: none; pointer-events: all; }这样手指触摸 SVG 时页面不会滚动事件会被 SVG 捕获绘制完成后把类名移除恢复正常滚动。如果是鼠标 触摸支持的双端场景还要注意 mouse 事件和 touch 事件不要重复触发建议用 Pointer Events 统一处理。5.5 长文档内存飙升页面接近崩溃方案一、方案二大文档都会遇到。方案一更严重因为每个页面是一张 Canvas 加一堆 span。我的经验是必须做虚拟页面管理只渲染可视区前后 2 页其余页面销毁翻页/滚动时复用已有的页码容器不能无脑 append页面销毁时置空 canvas.width调用 pdf.destroy() 释放底层资源图片方案下不要用 base64 dataURL 长存改用URL.createObjectURL(blob)用完后revokeObjectURL回收把这些做进去后300 页的 PDF 在 iPhone 上也能流畅滑动代价是每次翻页有几百毫秒的渲染等待。交互上配一个骨架屏或 loading 提示用户普遍能接受。一些小体会做了这么多 PDF 相关的 H5 项目后我最深的感觉是PDF 渲染本身不是什么难事难的是你永远不知道用户会传上来什么样的 PDF。有坏掉的、有加密的、有字体嵌不进的、有纯扫描的、有直接把网页打印成异常长宽的。因此不管选哪条方案我都强烈建议提前做“文件体检”解析前先测页数、测文本量、测页面尺寸再决定走哪套渲染器绝对比运行时再降级舒服得多。最后再分享一个非常实用的扩展方向如果方案一的文本层已经做通叠加一个“高亮单词/关键词的悬浮释义”功能只需要几十行代码。用户在 PDF 里框选一个词查询接口返回释义弹一个小气泡卡片。这个能力在很多教育、法律、医疗阅读场景里都特别吃香而且完全不需要改底层渲染架构。顺着这个思路你手里的这套 PDF 渲染能力还能长出一大片产品功能祝你顺利。
返回列表