ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

react-pdf 重新引入 @react-pdf/svgkit:用 pdfkit 形状的绘制上下文把文档渲染为 SVG 字符串

react-pdf 重新引入 @react-pdf/svgkit:用 pdfkit 形状的绘制上下文把文档渲染为 SVG 字符串 react-pdf 重新引入 react-pdf/svgkit用 pdfkit 形状的绘制上下文把文档渲染为 SVG 字符串【免费下载链接】react-pdf Create PDF files using React项目地址: https://gitcode.com/gh_mirrors/re/react-pdf导读本篇文章围绕变更集 .changeset/svgkit-revival.md 展开解读 react-pdf 重新引入的react-pdf/svgkit包以及react-pdf/render新增的可选ctx.glyphs能力接缝。你将了解到为什么一个长得像 pdfkit的绘制上下文能把 react-pdf 文档渲染成 SVG 字符串、ctx.glyphs接缝如何在不影响 PDF 输出的前提下让非 PDF 后端拿到原始字形数据以及如何在浏览器里用十几行代码搭建一个轻量文档预览。变更集说了什么.changeset/svgkit-revival.md是一份典型的 changesets 发布声明它标记了两个包的 minor 版本变更并用一句话概括了本次改动的全部内容Reintroduce react-pdf/svgkit: a pdfkit-shaped drawing context that renders documents to SVG strings. react-pdf/render gains an optionalctx.glyphscapability seam so non-PDF backends receive raw glyphs; pdf output is unchanged.拆解这句话可以得到三个关键技术要点重新引入Reintroducereact-pdf/svgkit该包并非全新项目而是重新回到 monorepo 的既有设计react-pdf/svgkit的定位一个pdfkit 形状pdfkit-shaped的绘制上下文把文档渲染为 SVG 字符串而不是 PDF 操作符react-pdf/render新增ctx.glyphs能力接缝可选实现让非 PDF 后端直接接收原始 glyph字形同时PDF 输出保持不变——这是本次变更中向后兼容的明确承诺。react-pdf/svgkit为什么要有一个pdfkit 形状的上下文要理解 svgkit先要理解 react-pdf 的渲染管线。在 packages/render/src/index.ts 中render()函数遍历布局树后调用renderNode完成绘制最后调用ctx.end()const render (ctx: Context, doc: SafeDocumentNode) { const pages doc.children || []; const options { imageCache: new Map(), fieldSets: [] }; pages.forEach((page) renderNode(ctx, page, options)); addBookmarks(ctx, doc); ctx.end(); return ctx; };关键在于render并不关心ctx具体是什么它只要求ctx具备 pdfkit 风格的接口fill、stroke、moveTo、text、addPage等。react-pdf/svgkit的SVGDocument就实现了同一个上下文形状但把每次绘制调用累积成 DOM 节点最终序列化为 SVG 字符串——因此渲染文档完全不需要引入 pdfkit 或任何 PDF 查看器。这在源码层面有直接印证在 packages/svgkit/src/document.ts 中SVGDocument实现了完整的 pdfkit 风格方法面例如路径 APImoveTo/lineTo/bezierCurveTo/quadraticCurveTo/closePath/rect/roundedRect/ellipse/circle/polygon/path全部累积进currentPath字符串样式 APIfillColor、strokeColor、fillOpacity、strokeOpacity、opacity、lineWidth、lineCap、lineJoin、miterLimit、dash、undash变换 APIsave/restore维护容器与样式栈transform/translate/rotate/scale通过openGroup生成带transform属性的g元素。最终由 packages/svgkit/src/serialize.ts 把节点树序列化为字符串并对属性值、、和文本内容、、做正确的 XML 转义const serialize (element: SVGElementNode): string { const attributes Object.entries(element.attributes) .map(([name, value]) ${name}${escapeAttribute(String(value))}) .join(); if (element.children.length 0) return ${element.name}${attributes}/; const children element.children .map((child) typeof child string ? escapeText(child) : serialize(child), ) .join(); return ${element.name}${attributes}${children}/${element.name}; };适用场景轻量级浏览器内预览svgkit 的官方定位见 packages/svgkit/README.md是轻量浏览器内预览例如 REPL 或文档站点。仓库里就有真实用例packages/examples/vite示例中的 packages/examples/vite/src/svg-render.ts 实现了完整的React 元素 → SVG 页面字符串管线。基本用法layout → render → ctx.pages安装yarn add react-pdf/svgkit在 packages/svgkit/package.json 中可以看到它声明为pdfkit-compatible drawing context that renders react-pdf documents to SVGmain指向./lib/index.js并带有browser字段./lib/index.browser.js说明它同时面向 Node 与浏览器环境。核心调用链最精简的用法来自 READMEimport FontStore from react-pdf/font; import layout from react-pdf/layout; import render from react-pdf/render; import SVGDocument from react-pdf/svgkit; const fontStore new FontStore(); const resolved await layout(documentTree, fontStore); const ctx new SVGDocument({ idPrefix: doc1- }); render(ctx, resolved); ctx.pages; // string[] — one svg…/svg per page几个值得注意的约定render()会自行调用ctx.end()见上文render源码调用方永远不需要手动调end()render()返回后ctx.pages就是每个页面对应的序列化 SVG 字符串数组ctx的构造可传SVGDocumentOptions其中idPrefix与info两个选项在下一节说明。仓库中的 packages/examples/vite/src/svg-render.ts 展示了完整版它复用react-pdf/renderer的 reconciler 单例挂载 React 元素再依次调用layoutDocument和render最后把ctx.pagesresolve 出去const layout await layoutDocument(container.document, Font); const ctx new SVGDocument({ idPrefix: svg-${idSeq}- }); render(ctx, layout); resolve(ctx.pages);idPrefix多文档共存时的 ID 防冲突clip path 和渐变的 id 是按文档顺序生成的clip-1、grad-1……命名目标named destination的 id 则由文档自身的idprop 派生dest-id。如果要把多个文档的 SVG 输出嵌入同一个 DOM就必须传入不同的idPrefix否则这些 id 会互相碰撞const ctx new SVGDocument({ idPrefix: doc2- });在 packages/svgkit/src/document.ts 中nextId()的实现为${this.idPrefix}${kind}-${this.idCounter}而goTo/addNamedDestination生成的目标 id 为${this.idPrefix}dest-name都遵循同样的前缀机制。文档元信息info 选项info接受与 pdfkit 相同的键Title、Author、Subject、Keywords、Creator、Producer、CreationDate、ModificationDate。构造函数里Object.assign(this.info, options.info)即可注入也可以在调用render()之前直接给ctx.info赋值。元信息会被编码进 SVGTitle输出为title元素Subject输出为desc元素Title、Author、Keywords、Subject、CreationDate还会以 Dublin Core RDF 形式写入metadata每页一份与 Inkscape、Illustrator 的约定一致见buildDublinCoreMetadata未设置的键直接省略空info不会产生任何额外标记。const ctx new SVGDocument({ info: { Title: Invoice #42, Author: Acme Inc. }, });页面与会话模型addPage({ size })的默认尺寸为[612, 792]即美式 Letter 的点单位生成带viewBox0 0 w h的svg根元素并创建每页独立的defs、重置样式栈与路径缓冲区end()则把所有页根节点逐一序列化到ctx.pages。ctx.glyphsreact-pdf/render 的能力接缝这是本次变更集中对react-pdf/render的唯一改动也是整个设计的关键在不改变 PDF 输出的前提下让非 PDF 后端拿到原始 glyph。接缝的定义在 packages/render/src/types.ts 中Context类型新增了一个可选方法/** SVG-backend capability: receives raw textkit glyphs instead of PDF operators. Implemented by react-pdf/svgkits SVGDocument; when unset, the pdfkit glyph-encoding path runs. */ glyphs?: ( glyphs: Glyph[], positions: Position[], x: number, y: number, ) unknown;类型注释把语义说得非常清楚ctx.glyphs是SVG 后端能力接收的是原始 textkit glyph 而不是 PDF 操作符当它不存在时走的是 pdfkit 字形编码路径。运行时分支在 packages/render/src/primitives/renderGlyphs.ts 中分支发生在最前面const renderGlyphs ( ctx: Context, glyphs: Glyph[], positions: Position[], x: number, y: number, ) { if (ctx.glyphs) return ctx.glyphs(glyphs, positions, x, y); // ...pdfkit 路径encodeGlyphs TJ 命令缓冲 };PDF 路径默认对字形做encodeGlyphs编码换算advanceWidthScale随后通过BT/Tm/Tf/TJ/ET等 PDF 内容流操作符逐段输出——这正是 pdfkit 渲染文本的过程SVG 路径当ctx.glyphs存在时直接调用ctx.glyphs(glyphs, positions, x, y)把原始 textkit 字形交给后端自行处理。由于分支基于可选方法是否存在而非任何环境判断PDF 后端pdfkit根本感知不到这个接缝输出逐字节不变——与变更集pdf output is unchanged的承诺一致。调用链上packages/render/src/primitives/renderText.ts 中的renderGlyphs(ctx, glyphs, run.positions!, 0, 0)是 glyph 渲染的入口从这里可以追踪到 textkit 的 shaping 输出如何流入两种后端。文本渲染的两条路径字形轮廓 vs CSS 字体SVGDocument.glyphs()packages/svgkit/src/document.ts是接缝的落地实现它根据字体是否注册分为两种渲染策略已注册字体逐字形轮廓 path 可选中覆盖层如果 glyph 带有path来自嵌入字体的轮廓数据glyphs()会为每个字形生成独立的path元素通过translate(...) scale(fontSize/unitsPerEm, -fontSize/unitsPerEm)变换定位并利用 textkit shaping 的xAdvance/xOffset/yOffset计算结果排布。注释特别说明offsetScale fontSize / 1000的偏移缩放是为了与 PDF 侧renderGlyphs.ts保持一致让 SVG 输出与 PDF 输出对齐。由于轮廓 path 是不可选中的矢量图形svgkit 还会在其上叠一层fill-opacity0的透明textpdf.js 风格使文本保持可选中、可复制、可搜索、对屏幕阅读器可访问——注释强调fill-opacity0而非 fillnone才能保持命中测试与可选中性。标准 14 字体text 元素 CSS 字体回退Helvetica、Times、Courier 及变体没有嵌入轮廓数据因此回退为text元素每个字形一个 x 坐标per-glyph x并带上 CSS 字体回退栈。映射逻辑在 packages/svgkit/src/text.tsconst STANDARD_FAMILIES: Recordstring, string { Helvetica: Helvetica, Arial, sans-serif, Courier: Courier New, Courier, monospace, Times: Times New Roman, Times, serif, };resolveFontFace还会从字体名解析bold/italic标志正则匹配Bold、Italic|Oblique输出为font-weight/font-style属性unitsPerEmOf在字体缺少unitsPerEm时回退为 1000。这种路径的文本可直接选中但精确字形取决于查看器本机安装的字体。样式细节渐变、裁剪与路径svgkit 对 pdfkit 风格 API 的翻译不止于路径渐变linearGradient/radialGradient返回SVGGradient实例packages/svgkit/src/gradient.ts首次使用时惰性写入页面defsresolvePaint中paint.emitted标志防重复填充/描边通过url(#grad-1)引用gradientUnitsuserSpaceOnUse保证用户空间坐标语义与 PDF 一致setTransform支持把 pdfkit 的渐变矩阵翻译为gradientTransform裁剪clip()把当前路径放入clipPath支持evenodd/nonzero规范化并通过openGroup({ clip-path: url(#...) })应用路径填充规则fill(even-odd)等调用会映射为fill-rule属性isWindingRule/normalizeRule负责识别与规范化图像image()支持fit、align、valign缩放对齐输出preserveAspectRationone的image元素并遵循fillOpacity * opacity合成透明度。注解Annotations链接、书签、表单与备注svgkit不发射可交互的a元素或真正的表单控件而是输出惰性注解一个pointer-events: none、不绘制的rect备注则是在图标g上加data-rpdf-note标注位置并携带足够的数据让宿主host自行构建真实交互。可见绘制始终照常输出注解只是叠加。链接data-rpdf-link外部链接把 URL 原样写入data-rpdf-link内部链接goTo写入片段如data-rpdf-link#doc1-dest-chapter可直接document.querySelector定位到addNamedDestination输出的g iddoc1-dest-chapter>rect x0 y-10 width50 height10 fillnone pointer-eventsnone>metadata rpdf:outline xmlns:rpdfhttps://react-pdf.org/ns rpdf:item titleChapter 1 page0 href#bookmark-1 rpdf:item titleSection 1.1 page0 href#bookmark-2/ /rpdf:item /rpdf:outline /metadata消费方用DOMParser解析该片段遍历rpdf:item读取title/page/expanded用href跳转到对应标记——注意要跳到page属性指示的那一页而不一定是大纲元数据所在的第一页。表单字段静态近似 属性注解TextInput、Select、List、Checkbox仍会绘制PDF 查看器看到的样子字段值、密码掩码*、选中勾号同时每个字段追加一个注解 rect。字段相关属性如下AttributeMeaningPresent ondata-rpdf-fieldField type:text,checkbox,comboorlistalldata-rpdf-field-nameField nameall, when nameddata-rpdf-field-valueCurrent value (masked forpassword)text, combo, listdata-rpdf-field-checkedtrue/falsecheckbox onlydata-rpdf-field-optionsJSON array of choicescombo, listdata-rpdf-field-multilinetruewhen settext onlydata-rpdf-field-passwordtruewhen settext onlydata-rpdf-field-readonlytruewhen setany, when read-only例如rect x40 y80 width120 height20 fillnone pointer-eventsnone>document.querySelectorAll([data-rpdf-field]).forEach((el) { const box el.getBoundingClientRect(); // position a real control here const { rpdfField: type, rpdfFieldValue, rpdfFieldChecked, rpdfFieldReadonly, } el.dataset; const input document.createElement(type text ? input : select); if (type checkbox) input.checked rpdfFieldChecked true; else input.value rpdfFieldValue || ; input.disabled rpdfFieldReadonly true; // position input over box and append it });仓库中 packages/examples/vite/src/svg-viewer.tsx 提供了完整实现读取注解矩形几何在其上叠加真实的input/select/textarea控件与备注弹层内部链接平滑滚动、外部链接新标签打开。独立的.svg文件直接打开时天然无交互——交互必须由宿主按此模式实现。版本与构建minor 变更的发布语义变更集标记react-pdf/svgkit与react-pdf/render均为minor版本。从 packages/svgkit/package.json 看当前版本号是0.0.0预发布状态devDependencies 依赖同仓库的react-pdf/font、react-pdf/layout、react-pdf/render构建脚本使用 rollupbuild: rimraf ./lib rollup -c测试使用 vitesttest: vitest并配有tsc --noEmit类型检查。也就是说svgkit 的定位是作为 react-pdf 渲染后端的可选实现存在与 layout、font、render 共同组成解析 → 布局 → 绘制的完整链路而 PDF 主链路不受任何影响。小结svgkit-revival这次变更的本质是给 react-pdf 补上一条不走 pdfkit的渲染支路react-pdf/svgkit以 pdfkit 形状的SVGDocument上下文把同一份布局树渲染成每页一个svg字符串支持路径、渐变、裁剪、图像、文本字形轮廓或 CSS 字体、书签与各类注解react-pdf/render的ctx.glyphs接缝是这条支路的关键松耦合点可选方法存在时接收原始 textkit glyph不存在时照旧走 pdfkit 编码路径PDF 输出不变。对于想在浏览器里做 REPL、文档站点预览或富交互 PDF 前端的开发者这条支路意味着不需要 pdfkit、不需要 PDF 查看器用同一个 React 文档定义就能得到可嵌入、可选中、可交互扩展的 SVG 预览。【免费下载链接】react-pdf Create PDF files using React项目地址: https://gitcode.com/gh_mirrors/re/react-pdf创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表