@embedpdf-editor/react-chapter-viewer
v1.2.20
Published
React 版章节 PDF 阅读器。它把多份章节 PDF 组织成一本连续滚动的书,并内置划线/高亮、附注、段落书签、章节目录和标注导入导出能力。
Downloads
8,556
Readme
@embedpdf-editor/react-chapter-viewer
React 版章节 PDF 阅读器。它把多份章节 PDF 组织成一本连续滚动的书,并内置划线/高亮、附注、段落书签、章节目录和标注导入导出能力。
底层使用 @embedpdf/core 插件体系和 PDFium 引擎;React 包只负责提供 React 组件、hooks 和类型导出。
安装
pnpm add @embedpdf-editor/react-chapter-viewerVite
包内导出的是一段 Vite 配置,不是 Vite plugin。它用于补齐 scheduler alias 和 PDFium 引擎的预构建配置;@embedpdf/engines 已经是本包依赖,业务项目不需要单独安装。
// vite.config.ts
import { defineConfig, mergeConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { chapterViewerViteResolve } from '@embedpdf-editor/react-chapter-viewer/vite';
export default mergeConfig(
defineConfig({
plugins: [react()],
}),
chapterViewerViteResolve(),
);如果不用 mergeConfig,需要手动合并 resolve.alias 和 optimizeDeps.include,不要直接覆盖现有配置。
快速开始
推荐只传 options。editorOptions 和 features 仍兼容,但已作为低层/旧写法保留。
import {
ChapterPdfViewer,
usePdfiumEngine,
type ChapterViewerOptions,
} from '@embedpdf-editor/react-chapter-viewer';
const options: ChapterViewerOptions = {
manifest: {
chapters: [
{
chapterId: 'ch-1',
title: '第一章',
globalPageRange: [1, 12],
localPageRange: [0, 11],
source: { url: '/pdfs/chapter-1.pdf' },
},
],
},
notes: {
loadNotes: async () => [],
onCreateNote: async () => ({ noteId: crypto.randomUUID() }),
onUpdateNote: async () => {},
// 两阶段删除:业务先确认外部数据,返回 true 才让插件擦 PDF 笔记
onRequestDeleteNote: async () => true,
},
bookmarks: {
load: async () => [],
persist: async () => {},
onRequestRemove: async () => true,
},
features: {
markup: true,
bookmarks: true,
notes: true,
selectionToolbar: true,
zoom: { pageWidth: 720 },
},
};
export function Reader() {
const { engine, isLoading, error } = usePdfiumEngine();
if (error) return <div>PDF engine failed: {error.message}</div>;
if (isLoading || !engine) return <div>Loading PDF engine...</div>;
return (
<ChapterPdfViewer
engine={engine}
options={options}
className="reader"
viewportClassName="reader-viewport"
/>
);
}容器需要稳定高度:
.reader {
height: 100vh;
min-height: 0;
}配置总览
| 层级 | 适用 | 说明 |
| --- | --- | --- |
| ChapterViewerOptions | ChapterPdfViewer 的 options | 推荐:manifest、笔记/书签回调、重叠页、features |
| ChapterViewerConfig | options.features | 功能开关与 UI 样式(划线、图标、缩放、选区工具栏) |
| ChapterPdfViewer props | 组件 | className、buildSelectionMenu 等(见下文) |
| usePdfiumEngine | 引擎 hook | wasmUrl、worker |
省略 features 时等价于全部开启并使用内置默认样式(normalizeChapterViewerConfig)。
ChapterViewerOptions
options 是三端渲染器(React / Vue3 / chapter-snippet)的统一配置入口。
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| manifest | 是 | 章节清单 ChapterManifest(可响应式更新,见 ChapterPdfViewer 一节) |
| notes | 是 | 附注回调 NoteCallbacks |
| bookmarks | 是 | 段落书签回调 ParagraphBookmarkCallbacks |
| chapterPdfLoader | 否 | 全局 PDF 加载器 IChapterPdfLoader(各章无 source 时使用) |
| overlapStrategy | 否 | 重叠全局页的 owner 策略,默认 { kind: 'first-wins' } |
| features | 否 | 阅读器 UI 与能力开关,见 features 配置 |
Manifest
type ChapterManifest = {
chapters: Array<{
chapterId: string;
title: string;
globalPageRange: [number, number];
localPageRange: [number, number];
source?:
| { url: string }
| { buffer: ArrayBuffer | Uint8Array | DataView }
| {
load: () => Promise<
{ url: string } | { buffer: ArrayBuffer | Uint8Array | DataView }
>;
}
| { urls: string[]; segmentPageThreshold: number }; // legacy 静态多段
segmentPageThreshold?: number;
encrypted?: boolean;
ownedGlobalPages?: number[];
}>;
totalGlobalPages?: number;
};globalPageRange 和 localPageRange 都是闭区间,且页数必须一致。chapterId 在整本书内唯一;单 URL 章时引擎 documentId === chapterId,分段章内部为 chapterId#s0、#s1…(业务存储仍只用 chapterId)。
| 字段 | 说明 |
| --- | --- |
| chapterId | 业务唯一 ID;笔记/书签/归档 JSON 的主键 |
| title | 展示标题(目录树等) |
| globalPageRange | 在「整本」中的全局页闭区间,允许相邻章重叠 |
| localPageRange | 该章 PDF 内 0-based 闭区间;页数须与 global 区间一致 |
| source | 静态 PDF;动态详情见 PDF 加载 |
| segmentPageThreshold | 章内分段阈值;URL 由 loadChapterUrls 拉取 |
| encrypted | 可选业务标记;实际解密依赖 PDF 密码流 + passwordProvider |
| ownedGlobalPages | 仅 overlapStrategy: { kind: 'explicit' } 时使用:声明拥有的全局页号 |
| totalGlobalPages | 可选;未传时由全局页并集自动推导 |
notes(附注回调)
类型 NoteCallbacks。至少配置 onCreateNote 或 onRequestCreateNote 之一,否则划词「添加笔记」无法落库。
| 回调 | 说明 |
| --- | --- |
| loadNotes | 初次加载已有附注,返回 NoteAnchor[] |
| onCreateNote | 内置流程:持久化草稿 NoteDraft,返回 { noteId } |
| onRequestCreateNote | 自定义创建:传出 record(可 JSON 直存,缺 noteId);宿主弹窗后 complete({ noteId, content?, nodeId? });配置后不再调用 onCreateNote |
| onRequestEditNote | 自定义编辑:NoteActionPayload → { record, nodeId? } |
| onUpdateNote | 更新附注正文等 |
| onRequestDeleteNote | 请求删除:{ record, nodeId? } => Promise<boolean>;返回 true 才真删;false / 抛错都会取消 |
| onDeleteSuccess | 附注从内存移除后通知(业务收尾) |
NoteDraft / NoteAnchor 含 selectedText、content、position(位置 JSON)、可选 nodeId(业务库主键,导入时写入)。创建时直接用 record 存库,无需 toRecord。详见 04-notes。
bookmarks(段落书签回调)
类型 ParagraphBookmarkCallbacks。
| 回调 | 说明 |
| --- | --- |
| load | 初次加载书签列表 ParagraphBookmark[] |
| onAdded | 新增书签后触发;参数 { bookmark, position },position 为位置 JSON |
| persist | 任意增删改后回调(onAdded 之后仍会调),便于写回存储 |
| onRequestRemove | 用户点删除:BookmarkActionPayload → { record, nodeId? };须返回 true 才从界面移除 |
| onRemoveSuccess | 已从内存删除后的通知(可选) |
ParagraphBookmark 含 id(阅读器内 ID)、可选 nodeId(业务库主键)、label、metadata?、anchor(chapterId、localPageIndex、rectPdfCoord、rectsPdfCoord?、markerAnchor? 等)、createdAt。
嵌套目录树与重叠页
ChapterTreePanel 支持 ChapterTreeNode.children 多级目录。manifest.chapters 为扁平列表;重叠全局页只渲染 一个 owner 章节的 PDF。
import {
buildChapterViewerCatalog,
overlapStrategyForSamePageOwner,
} from '@embedpdf-editor/react-chapter-viewer';
const { tree, manifest } = buildChapterViewerCatalog([/* ChapterTreeInput[] */]);
const options = {
manifest,
overlapStrategy: overlapStrategyForSamePageOwner('last'),
notes: { /* ... */ },
bookmarks: { /* ... */ },
};| 策略 | 重叠页 |
| --- | --- |
| first-wins(默认) | manifest 先出现的章节 |
| last-wins | 后出现的章节(常以当前页最后一节为准) |
| explicit | 各章通过 ownedGlobalPages 声明归属 |
| custom + resolve | 完全自定义 |
PDF 加载与预处理
推荐 三步(目录 → 章节详情 → 可选处理 PDF):
| 步骤 | 方法 | 说明 |
| --- | --- | --- |
| 1 | manifest.chapters | 仅 chapterId、页码;可选 segmentPageThreshold |
| 2 | chapterPdfLoader.loadChapterUrls | 按章请求详情,返回 string[] |
| 3 | chapterPdfLoader.openPdf(可选) | 对 ctx.url 解密/下载;省略则直接打开 |
import type { ChapterPdfLoadContext } from '@embedpdf-editor/react-chapter-viewer';
chapterPdfLoader: {
async loadChapterUrls(chapter) {
const res = await getOneChap(chapter.chapterId);
const raw = res.data.resourceUrl;
return Array.isArray(raw) ? raw : raw ? [raw] : [];
},
async openPdf(ctx: ChapterPdfLoadContext) {
return { url: ctx.url };
},
},去重:loadChapterUrls 每章只调 1 次(引擎缓存);打开 PDF 按段各 1 次。勿在 loader 外再请求详情。
静态单文件仍可用 source.url / buffer / load()。兼容单步 loadPdf(chapter, segmentIndex)。
章内多 PDF 分段
segmentPageThreshold 写在章节上(不要放进 source):
{
chapterId: 'ch-1',
globalPageRange: [1, 13],
localPageRange: [0, 12],
segmentPageThreshold: 5,
}配合 loadChapterUrls 返回 3 个 URL。运行时 documentId 为 chapterId#s0…;业务库仍用 chapterId + localPageIndex。
教程:docs/get-started/03-manifest.md · 12-segmented-pdf-and-per-chapter-storage.md
按章持久化(笔记 / 书签 / 划线备份)
| 数据 | 阅读器 ID | 业务 nodeId | 分段章注意 |
| --- | --- | --- | --- |
| 笔记 | noteId(complete 时你指定) | 导入时写在 NoteAnchor.nodeId;编辑/删除回调带上 | chapterId 始终是 manifest 的 chapterId |
| 书签 | id(插件自动生成 bm-xxx,可自传) | 导入时写在 ParagraphBookmark.nodeId | 勿用 chapterId#sN 分表 |
| 划线 | annotation.id | 导入时写在 markup[].nodeId;新建时在 onCreated 写 record.nodeId 或 return { nodeId } | 导出见 exportChapterAnnotations |
nodeId 不参与渲染,仅便于你在编辑/删除时调后端 API。创建时可写入 record.nodeId 或回调返回值(笔记 complete / onCreateNote 返回、划线 onCreated、书签 onAdded);从数据库导入时把 nodeId 一并写入上述字段即可。
markup record 顶层额外带 localPageIndex、globalPageIndex、globalPageNumber,便于业务直接落库与外部滚动。已有 ChapterPdfViewer ref 时,可用 scrollToMarkupRecord(viewerRef.current, markupRecord)、scrollToNoteRecord(viewerRef.current, noteRecord)、scrollToBookmarkRecord(viewerRef.current, bookmarkRecord) 回到对应位置。
// 笔记创建(record 已含 position,可直接存库)
onRequestCreateNote: ({ record, complete }) => {
openDialog(record.selectedText).then(async (content) => {
const noteId = crypto.randomUUID();
const nodeId = await api.createNote({ ...record, content });
await complete({ noteId, content, nodeId });
});
},
// 划线新建:写 record.nodeId 或 return { nodeId }(回调结束后自动登记,删除时带上)
onCreated: async ({ record }) => {
const nodeId = await api.saveMarkup(record);
record.nodeId = nodeId;
// 或: return { nodeId };
},
onRequestDelete: async ({ record, nodeId }) => {
await api.deleteMarkup(nodeId);
return true;
},按章按需加载:配置 options.annotations.loadChapterAnnotations,滚动到焦点章自动导入;或 ref.current.loadChapterAnnotations(chapterId) 手动触发(ref 仍只传 ID)。回调入参为 ChapterAnnotationsLoadContext(chapterId + 完整 chapter: ChapterDescriptor),便于按 title、globalPageRange、segmentPageThreshold 等拉取后端数据。启用后勿在 loadNotes / bookmarks.load 一次拉全书。
annotations: {
async loadChapterAnnotations({ chapterId, chapter }) {
// chapter:manifest 中的 ChapterDescriptor(title、页码范围、segmentPageThreshold…)
const res = await fetch(
`/api/chapters/${chapterId}/annotations?threshold=${chapter.segmentPageThreshold ?? ''}`,
).then((r) => r.json());
return res; // { notes?, bookmarks?, markup? } 或 null
},
importOptions: { mode: 'replace' },
},单 URL 章节与分段章节 共用同一套回调字段;已有只按章存页码的数据 无需迁移。
外部删除后同步渲染器
如果业务侧在外部列表里删除了笔记 / 书签 / 划线高亮,删除接口成功后再调用 ChapterPdfViewer ref,把渲染器内存里的对应标记移除。这里不会再触发 onRequestDeleteNote、onRequestRemove 或 features.markup.onRequestDelete,避免二次请求后端。
import { useRef } from 'react';
import {
ChapterPdfViewer,
type ChapterPdfViewerExpose,
type SerializableAnnotationTransferItem,
} from '@embedpdf-editor/react-chapter-viewer';
const viewerRef = useRef<ChapterPdfViewerExpose>(null);
async function deleteNote(noteId: string) {
await api.deleteNote(noteId);
viewerRef.current?.removeNote(noteId);
}
async function deleteBookmark(bookmarkId: string) {
await api.deleteBookmark(bookmarkId);
viewerRef.current?.removeBookmark(bookmarkId);
}
async function deleteMarkup(record: SerializableAnnotationTransferItem) {
await api.deleteMarkup(record.nodeId ?? record.annotation.id);
await viewerRef.current?.removeMarkupAnnotation(record);
}
<ChapterPdfViewer ref={viewerRef} engine={engine} options={options} />;加密 PDF 与 passwordProvider
需 PDF 用户密码时:打开失败 → passwordProvider.resolvePassword(chapter, attempt) → 返回密码重试;null 则 password-required。
import { CallbackPasswordProvider } from '@embedpdf-editor/react-chapter-viewer';
// 须在 createPdfChapterEditor / 自定义 bundle 中传入,非 options 顶层字段
const passwordProvider = new CallbackPasswordProvider(async (chapter, attempt) =>
askPassword(chapter.chapterId, attempt),
);还可使用 StaticPasswordProvider、UiPromptPasswordProvider(chapter-core)。
features 配置
类型 ChapterViewerConfig(options.features)。每项支持:省略(默认开)、true、false(关)、对象(开并覆盖字段)。
features.markup
| 字段 | 说明 |
| --- | --- |
| enabled | 划线能力总开关 |
| styles | highlight / underline / squiggly / strikeout 的 color、thickness、offsetY、opacity |
| annotationMenu | 点击已有划线:enabled(默认 true)、renderMenu({ pageIndex, annotationId, onDelete });与 onClick 互斥 |
| onClick | 点击已有划线/高亮:{ record, nodeId?, pageIndex };配置后不弹默认删除浮窗,保留选中外框 |
| onCreated | 划词创建后单条通知,含 kind / record / position / selectedText 等 |
| onRequestDelete | 请求删除:{ record, nodeId? } => Promise<boolean>;返回 true 才真删 |
| onDeleted | 划线删除完成后通知,参数同上 |
默认 offsetY:highlight 0、underline 2.5、squiggly 4、strikeout 0。
markup: {
onRequestDelete: async ({ record, nodeId }) => {
const ok = await api.deleteMarkup(nodeId ?? record.annotation.id);
return ok; // false 时插件不会擦除 PDF 上的标记
},
onDeleted: ({ record, nodeId }) =>
toast.success(`已删除 ${nodeId ?? record.annotation.id}`),
},章节级全量持久化用
subscribeChapterMarkupChanges(registry, listener);它在任何变更后都会推该章节的最新 markup 快照,与上面单条事件互补,不冲突。
features.bookmarks
| 字段 | 说明 |
| --- | --- |
| enabled | 书签总开关 |
| marker.renderIcon / iconSize | 页内书签图标 |
| marker.onClick | 点击书签图标:{ record, nodeId? };配置后不弹默认删除浮窗 |
| hover.renderAddIcon / iconSize | 悬停行末添加书签 |
features.notes
| 字段 | 说明 |
| --- | --- |
| enabled | 附注总开关 |
| marker.renderIcon | 笔记图标(无默认圆底) |
| marker.renderMenuActions | 自定义编辑/删除菜单 |
| marker.iconSize | 图标尺寸 |
| marker.offsetX / offsetY | 角标相对默认位置的水平/垂直偏移(CSS 像素,正值向右/下;菜单跟随角标) |
| marker.highlightColor | 默认 rgba(254, 240, 138, 0.5) |
| marker.highlightStyle | 笔记高亮矩形任意 CSS(border、boxShadow、opacity 等);与 highlightColor 合并,后者优先 |
| marker.alwaysVisible | boolean,默认 false(hover 高亮时才显示图标);置 true 后图标常驻显示,hover 感知区只覆盖图标本身,不影响该段落划词 |
| marker.onClick | 点击笔记高亮区或图标:{ record, nodeId? };同一回调;配置后不弹编辑/删除浮窗 |
| marker.selectedOutline | 点击选中后的外边框:color / width / offset(默认接近高亮标注选中样式) |
features: {
notes: {
marker: {
alwaysVisible: true, // 一眼就能看到哪些段落已有笔记
iconSize: 18,
},
},
}features.scrollViewport
[data-chapter-scroll-viewport] 滚动容器样式(PDF 页外侧区域)。
| 字段 | 默认 | 说明 |
| --- | --- | --- |
| background | #f1f5f9 | 滚动视口背景色 |
features: {
scrollViewport: { background: '#e2e8f0' },
}features.page
单页 PDF 画布背景(页壳 + 渲染层外侧),与 scrollViewport 区分。
| 字段 | 默认 | 说明 |
| --- | --- | --- |
| background | #ffffff | 每页 PDF 背景色 |
features: {
page: { background: '#fafafa' },
}运行时可通过 ref 更新:
viewerRef.current?.setPdfPageBackground('#1e293b');
const current = viewerRef.current?.getPdfPageBackground();features.zoom
| 字段 | 默认 | 说明 |
| --- | --- | --- |
| enabled | true | 交互缩放(Ctrl+滚轮 / 双指捏合) |
| min / max | 0.5 / 3 | 配置的 scale 范围 |
| initial | 1 | 初始 scale |
| pageWidth | — | 版心 CSS 宽度,按 PDF 页宽推导 scale |
缩放实际上限由 [data-chapter-scroll-viewport] 的 clientWidth 决定:当 PDF 按 fit-width 渲染时的 scale 小于 max 时,以 fit-width 为准,避免内容撑破容器。视口宽度变化(窗口 resize、侧栏展开等)时会通过 ResizeObserver 自动重算边界并 clamp 当前 scale。
features.selectionToolbar
划词后出现两行浮窗:
第一行:[高亮][下划线][波浪][删除] | ○ ○ ○ ○ ○ ← 色盘
第二行: 笔记 复制 翻译 释义 … ← extraActions| 字段 | 说明 |
| --- | --- |
| enabled | 划词工具条;未配置时随 markup / notes / copy / extraActions 自动开启 |
| selectionBackground | 划词拖动时的选区高亮底色(CSS);默认 rgba(33, 150, 243) |
| hiddenBuiltinActions | 隐藏内置项:copy | highlight | underline | squiggly | strikeout | note |
| markupColors / defaultMarkupColor | 第一行色盘;选中色会记住,下次划词仍选中 |
| renderCopyIcon | () => ReactNode,自定义复制图标 |
| extraActions | { id, label, order?, onClick? } 扩展按钮 |
| onExtraAction | 扩展按钮统一点击回调(单项未写 onClick 时走这里) |
配置 vs 事件:复制、划线、笔记走内置逻辑;只有 扩展按钮 会回调宿主(extraActions[].onClick / onExtraAction / 组件 onExtraSelectionAction)。回调参数 SelectionToolbarExtraActionContext 含 selectedText、章节页码、rectsPdfCoord。详见 07-selection-toolbar。
features.pageOverlays
页内矩形叠加层:按 PDF 坐标画框,框内内容可自定义;支持导入导出与后端 QR 比例坐标。
| 字段 | 说明 |
| --- | --- |
| enabled | 默认 true |
| defaultBoxStyle | 全局默认样式;支持任意 CSS(border、boxShadow 等)及简写 borderColor/borderWidth/borderStyle |
| renderContent | (ctx: PageOverlayRenderContext) => ReactNode,ctx 含 overlay、payload、cssRect |
| onClick | (ctx) => void,点击框;payload 在 content.kind === 'qr' 或 metadata.payload 时可用 |
import {
importChapterAnnotations,
qrTargetToPageOverlayRecord,
} from '@embedpdf-editor/react-chapter-viewer';
features: {
pageOverlays: {
defaultBoxStyle: {
border: '2px dashed #ef4444',
backgroundColor: 'rgba(239, 68, 68, 0.08)',
cursor: 'pointer',
},
renderContent: ({ payload }) => <span>{payload ?? 'QR'}</span>,
onClick: ({ overlay, payload }) => {
console.log(overlay.overlayId, payload);
},
},
},
// 注入数据
await importChapterAnnotations(registry, {
chapterId: 'ch-1',
pageOverlays: [
qrTargetToPageOverlayRecord({
target: { id: 'qr-1', payload: 'https://…', x: 0.71, y: 0.51, width: 0.08, height: 0.057 },
chapterId: 'ch-1',
backendPageIndex: 1,
pageWidthPt: 531.39,
pageHeightPt: 746.85,
}),
],
}, { mode: 'merge' });完整说明:13-page-overlays.md。
ChapterPdfViewer 组件 Props
| Prop | 默认 | 说明 |
| --- | --- | --- |
| engine | — | 必填,usePdfiumEngine() |
| options | — | 推荐,ChapterViewerOptions;manifest.chapters 可动态改(内部 setManifest) |
| className / viewportClassName | — | 布局 class |
| onInitialized | — | (registry) => void,插件就绪(标注 IO 等) |
| onActiveChapterChange | — | 滚动焦点变化:{ chapterId, globalPageIndex, globalPageNumber, localPageIndex } |
| onChapterAnnotationsLoading / Loaded / Error | — | 按需标注加载生命周期(须配置 options.annotations) |
| onPdfPageBackgroundChange | — | PDF 页背景色变更:{ background }(含 ref.setPdfPageBackground) |
| ref(ChapterPdfViewerExpose) | — | scrollToChapter、loadChapterAnnotations(chapterId)、isChapterAnnotationsLoaded、setPdfPageBackground、getPdfPageBackground |
| buildSelectionMenu | — | 包装划词菜单 |
| annotationSelectionMenu | — | 点击已有划线标注的菜单(优先于 features.markup.annotationMenu) |
| redactionSelectionMenu | — | Redaction 层菜单 |
| showNoteMarkers | true | 笔记角标 |
| showBookmarkMarkers | true | 书签角标 |
| showRedactionLayer | false | 脱敏层 |
| onExtraSelectionAction | — | 扩展工具条按钮 |
| renderPageOverlay | — | 每页叠加层 |
| renderPluginsLoading | null | 插件未 ready 时的占位 UI |
| children | — | 视口内子节点 |
| editorOptions / features | — | 已废弃,请用 options |
动态换章
const [options, setOptions] = useState<ChapterViewerOptions>({
manifest: { chapters: [] },
notes: { /* ... */ },
bookmarks: { /* ... */ },
});
// 接口返回后更新,无需重建 ChapterPdfViewer
setOptions((prev) => ({
...prev,
manifest: { chapters: listFromApi },
}));
<ChapterPdfViewer engine={engine} options={options} onInitialized={(reg) => { registryRef.current = reg; }} />不要在 options 变化时重新 createChapterViewerBundle 并换 plugins。裸 EmbedPDF 组合时用 useSyncChapterManifest(manifest)。
Worker 与 WASM
usePdfiumEngine() 默认使用 @embedpdf/engines 内置的 PDFium CDN wasm 地址,并默认启用 worker:
usePdfiumEngine();如果业务要求内网部署、固定版本或自定义 CDN,再传入自托管地址(完整 URL 或站点相对路径):
// 相对路径(pdfium.wasm 放在 public/)
usePdfiumEngine({ wasmUrl: '/assets/pdfium.wasm', worker: true });
// 自有 OSS / CDN(示例)
usePdfiumEngine({
wasmUrl:
'https://hep-editor.oss-cn-beijing.aliyuncs.com/public/editor-public/js/pdfium.wasm',
worker: true,
});详见 docs/get-started/01-installation.md。
使用 worker: true 时,部署环境需要允许 worker 加载 wasm。若只在 worker 模式失败,可先用 worker: false 定位资源或响应头问题。
features 配置示例
完整字段见上文 features 配置。以下为常用示例。
划线颜色、粗细与位置
划词后通过选区工具栏创建的高亮/下划线/波浪线/删除线,可在创建时套用样式:
features: {
markup: {
styles: {
highlight: { color: '#fef08a', opacity: 0.45 },
underline: { color: '#dc2626', thickness: 1.5, offsetY: 2.5 },
// offsetY 为 PDF 点,正值向下;波浪线默认 4,避免盖住文字
squiggly: { color: '#dc2626', thickness: 1.5, offsetY: 4 },
strikeout: { color: '#64748b', offsetY: 0 },
},
},
},| 字段 | 说明 |
| --- | --- |
| color | 描边/填充色,如 #dc2626 |
| thickness | 线宽(PDF 点) |
| offsetY | 相对选区矩形向下偏移(PDF 点) |
| opacity | 高亮透明度 |
markup: false 关闭划线;markup: true 使用引擎默认样式。
划词划线后的回调 onCreated
import { setMarkupAnnotationNodeId } from '@embedpdf-editor/react-chapter-viewer';
features: {
markup: {
onCreated: async ({ kind, record, position, strokeColor }) => {
const nodeId = await api.saveMarkup({ kind, record, position, strokeColor });
setMarkupAnnotationNodeId(record.annotation.id, nodeId);
},
},
},色盘选中的颜色在 strokeColor;跨页划词可能触发多次。导出 JSON 的 markup[].annotation.strokeColor 会保留颜色。详见 06-markup。
点击已有划线:删除
选中页面上的划线标注后,默认在标注下方显示「删除」按钮:
features: {
markup: {
annotationMenu: {
enabled: true, // 默认 true
renderMenu: ({ annotationId, pageIndex, onDelete }) => (
<button type="button" onClick={onDelete}>
移除
</button>
),
},
},
},进阶用法若向 PdfChapterViewport 传入 annotationSelectionMenu,宿主菜单优先;宿主返回 null/undefined 时再显示上述默认删除菜单。
笔记、书签图标与菜单
features: {
notes: {
marker: {
renderIcon: ({ note }) => <img src="/icons/note.svg" alt="" width={20} height={20} />,
iconSize: 22,
highlightColor: 'rgba(245, 158, 11, 0.1)',
renderMenuActions: ({ onEdit, onDelete }) => (
<div style={{ display: 'flex', gap: 4 }}>
<button type="button" onClick={onEdit}>编辑</button>
<button type="button" onClick={onDelete}>删除</button>
</div>
),
},
},
bookmarks: {
marker: {
renderIcon: () => <img src="/icons/bookmark.svg" alt="" width={18} height={18} />,
iconSize: 20,
},
hover: {
renderAddIcon: () => <span aria-hidden>+</span>,
iconSize: 18,
},
},
},提供 renderIcon 后不再使用默认圆角底;renderMenuActions 可完全替换笔记上的编辑/删除区域。
选区工具栏(划词后)
复制(默认开启,浮窗最左侧)
features: {
selectionToolbar: {
selectionBackground: 'rgba(255, 193, 7, 0.35)', // 划词拖动时的底色
// 隐藏复制:hiddenBuiltinActions: ['copy'],
renderCopyIcon: () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden>
<rect x="8.5" y="8.5" width="11" height="13" rx="2.25" fill="#f1f5f9" stroke="#94a3b8" />
<rect x="4.5" y="3.5" width="11" height="13" rx="2.25" fill="#fff" stroke="#334155" />
</svg>
),
},
},程序化复制(不经过 UI):
import { copyTextToClipboard } from '@embedpdf-editor/react-chapter-viewer';
await copyTextToClipboard('要写入剪贴板的文本');扩展按钮(含划选文本)
方式一:写在 options 里(推荐,与组件 prop 二选一即可):
features: {
selectionToolbar: {
extraActions: [
{
id: 'translate',
label: '翻译',
onClick: ({ selectedText, chapterId, localPageIndex, rectsPdfCoord }) => {
void openTranslate({ text: selectedText, chapterId, localPageIndex, rectsPdfCoord });
},
},
],
},
},方式二:组件 prop onExtraSelectionAction(与 onExtraAction 等价,后配置优先)。
<ChapterPdfViewer
options={options}
onExtraSelectionAction={({ actionId, selectedText }) => {
if (actionId === 'cite') cite(selectedText);
}}
/>类型导出:MarkupStylesConfig、MarkupKindStyle、NoteMarkerUiConfig、BookmarkMarkerUiConfig、PageOverlayRecord、PageOverlayRenderContext、PageOverlaysFeatureConfig、qrTargetToPageOverlayRecord 等来自 @embedpdf-editor/react-chapter-viewer(与 chapter-viewer 一致)。
进阶组合
内置 ChapterPdfViewer 已完成插件注册和章节视口渲染。需要插入自定义 shell、目录、工具栏或直接访问 registry 时,可以使用低层组合:
import {
EmbedPDF,
PdfChapterViewport,
createChapterViewerBundle,
useSyncChapterManifest,
} from '@embedpdf-editor/react-chapter-viewer';
const { plugins, features } = createChapterViewerBundle(options);
function ChapterShell({ manifest }: { manifest: ChapterManifest }) {
useSyncChapterManifest(manifest); // 须在 EmbedPDF 子树内
return <PdfChapterViewport features={features} />;
}
<EmbedPDF engine={engine} plugins={plugins} onInitialized={onReady}>
{({ pluginsReady }) =>
pluginsReady ? <ChapterShell manifest={options.manifest} /> : null
}
</EmbedPDF>;createChapterViewerEditorOptions() 只生成旧式 editor 配置,不含 features;新代码优先 ChapterPdfViewer + 响应式 options。
标注导入导出
包内导出章节标注 IO(需 useRegistry() 或 EmbedPDF 就绪后):
import {
exportChapterAnnotations,
exportAllChapterAnnotations,
importChapterAnnotations,
importChapterAnnotationsArchive,
chapterAnnotationsArchiveToJson,
parseChapterAnnotationsArchiveJson,
downloadChapterAnnotationsArchive,
CHAPTER_ANNOTATIONS_ARCHIVE_VERSION,
useRegistry,
} from '@embedpdf-editor/react-chapter-viewer';归档 JSON:version(固定 1)、exportedAt、chapters[chapterId] 下含 bookmarks、notes、markup、pageOverlays(矩形叠加层,可选)。
| 选项 | 说明 |
| --- | --- |
| export / import 的 bookmarks / notes / markup / pageOverlays | 默认均为 true,可单独关闭 |
| ensureChapterLoaded | 默认 true;导出/导入 markup 时,分段章会加载 全部段 再合并页码 |
| mode | replace 清空后导入;merge 合并(书签按 anchor 去重) |
| persistNotes / persistBookmarks | 导入完成后写回业务存储的回调 |
归档按 chapterId 分桶,不是 chapterId#s0。详见 docs/get-started/10-annotations-io.md、12-segmented-pdf-and-per-chapter-storage.md。
完整教程索引
| 主题 | 文档 |
| --- | --- |
| 安装、WASM、wasmUrl | docs/get-started/01-installation.md |
| ChapterViewerOptions 全集 | docs/get-started/02-chapter-viewer-options.md |
| manifest / 分段 urls[] | docs/get-started/03-manifest.md |
| 划词复制、selectionToolbar | docs/get-started/07-selection-toolbar.md |
| 事件与回调对照 | docs/get-started/11-events-callbacks-and-component-api.md |
| 分段 + 按章存储 | docs/get-started/12-segmented-pdf-and-per-chapter-storage.md |
| 页内矩形叠加层 pageOverlays | docs/get-started/13-page-overlays.md |
常见问题
| 现象 | 处理 |
| --- | --- |
| 一直停在引擎加载 | 如果传了自定义 wasmUrl,检查地址是否 200、MIME/跨源头是否正确 |
| 只显示空白 | 确认外层容器有高度,且章节 source.url 可访问 |
| 页码或滚动错位 | 检查 globalPageRange 与 localPageRange 页数是否一致 |
| 划词笔记没有保存 | 实现 notes.onCreateNote 或自定义 onRequestCreateNote 流程 |
| 书签删除无效 | bookmarks.onRequestRemove 需要返回 true 才会删除 |
| 分段章导出划线不全 | 确认各段 urls 可访问;导出含 markup 会拉全部分段 |
| 误用 chapterId#s1 存业务数据 | 对外 API 只用 manifest 的 chapterId + localPageIndex |
