npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-viewer

Vite

包内导出的是一段 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.aliasoptimizeDeps.include,不要直接覆盖现有配置。

快速开始

推荐只传 optionseditorOptionsfeatures 仍兼容,但已作为低层/旧写法保留。

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 | ChapterPdfVieweroptions | 推荐:manifest、笔记/书签回调、重叠页、features | | ChapterViewerConfig | options.features | 功能开关与 UI 样式(划线、图标、缩放、选区工具栏) | | ChapterPdfViewer props | 组件 | classNamebuildSelectionMenu 等(见下文) | | usePdfiumEngine | 引擎 hook | wasmUrlworker |

省略 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;
};

globalPageRangelocalPageRange 都是闭区间,且页数必须一致。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。至少配置 onCreateNoteonRequestCreateNote 之一,否则划词「添加笔记」无法落库。

| 回调 | 说明 | | --- | --- | | 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 / NoteAnchorselectedTextcontentposition位置 JSON)、可选 nodeId(业务库主键,导入时写入)。创建时直接用 record 存库,无需 toRecord。详见 04-notes

bookmarks(段落书签回调)

类型 ParagraphBookmarkCallbacks

| 回调 | 说明 | | --- | --- | | load | 初次加载书签列表 ParagraphBookmark[] | | onAdded | 新增书签后触发;参数 { bookmark, position }position 为位置 JSON | | persist | 任意增删改后回调(onAdded 之后仍会调),便于写回存储 | | onRequestRemove | 用户点删除:BookmarkActionPayload{ record, nodeId? }须返回 true 才从界面移除 | | onRemoveSuccess | 已从内存删除后的通知(可选) |

ParagraphBookmarkid(阅读器内 ID)、可选 nodeId(业务库主键)、labelmetadata?anchorchapterIdlocalPageIndexrectPdfCoordrectsPdfCoord?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。运行时 documentIdchapterId#s0…;业务库仍用 chapterId + localPageIndex

教程:docs/get-started/03-manifest.md · 12-segmented-pdf-and-per-chapter-storage.md

按章持久化(笔记 / 书签 / 划线备份)

| 数据 | 阅读器 ID | 业务 nodeId | 分段章注意 | | --- | --- | --- | --- | | 笔记 | noteIdcomplete 时你指定) | 导入时写在 NoteAnchor.nodeId;编辑/删除回调带上 | chapterId 始终是 manifest 的 chapterId | | 书签 | id(插件自动生成 bm-xxx,可自传) | 导入时写在 ParagraphBookmark.nodeId | 勿用 chapterId#sN 分表 | | 划线 | annotation.id | 导入时写在 markup[].nodeId;新建时在 onCreatedrecord.nodeIdreturn { nodeId } | 导出见 exportChapterAnnotations |

nodeId 不参与渲染,仅便于你在编辑/删除时调后端 API。创建时可写入 record.nodeId 或回调返回值(笔记 complete / onCreateNote 返回、划线 onCreated、书签 onAdded);从数据库导入时把 nodeId 一并写入上述字段即可。

markup record 顶层额外带 localPageIndexglobalPageIndexglobalPageNumber,便于业务直接落库与外部滚动。已有 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)。回调入参为 ChapterAnnotationsLoadContextchapterId + 完整 chapter: ChapterDescriptor),便于按 titleglobalPageRangesegmentPageThreshold 等拉取后端数据。启用后勿在 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,把渲染器内存里的对应标记移除。这里不会再触发 onRequestDeleteNoteonRequestRemovefeatures.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) → 返回密码重试;nullpassword-required

import { CallbackPasswordProvider } from '@embedpdf-editor/react-chapter-viewer';

// 须在 createPdfChapterEditor / 自定义 bundle 中传入,非 options 顶层字段
const passwordProvider = new CallbackPasswordProvider(async (chapter, attempt) =>
  askPassword(chapter.chapterId, attempt),
);

还可使用 StaticPasswordProviderUiPromptPasswordProviderchapter-core)。


features 配置

类型 ChapterViewerConfigoptions.features)。每项支持:省略(默认开)、truefalse(关)、对象(开并覆盖字段)。

features.markup

| 字段 | 说明 | | --- | --- | | enabled | 划线能力总开关 | | styles | highlight / underline / squiggly / strikeoutcolorthicknessoffsetYopacity | | annotationMenu | 点击已有划线:enabled(默认 true)、renderMenu({ pageIndex, annotationId, onDelete })onClick 互斥 | | onClick | 点击已有划线/高亮:{ record, nodeId?, pageIndex };配置后不弹默认删除浮窗,保留选中外框 | | onCreated | 划词创建后单条通知,含 kind / record / position / selectedText 等 | | onRequestDelete | 请求删除:{ record, nodeId? } => Promise<boolean>返回 true 才真删 | | onDeleted | 划线删除完成后通知,参数同上 |

默认 offsetYhighlight 0、underline 2.5、squiggly 4strikeout 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(borderboxShadowopacity 等);与 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)。回调参数 SelectionToolbarExtraActionContextselectedText、章节页码、rectsPdfCoord。详见 07-selection-toolbar

features.pageOverlays

页内矩形叠加层:按 PDF 坐标画框,框内内容可自定义;支持导入导出与后端 QR 比例坐标。

| 字段 | 说明 | | --- | --- | | enabled | 默认 true | | defaultBoxStyle | 全局默认样式;支持任意 CSS(borderboxShadow 等)及简写 borderColor/borderWidth/borderStyle | | renderContent | (ctx: PageOverlayRenderContext) => ReactNodectxoverlaypayloadcssRect | | onClick | (ctx) => void,点击框;payloadcontent.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 | — | 推荐ChapterViewerOptionsmanifest.chapters 可动态改(内部 setManifest) | | className / viewportClassName | — | 布局 class | | onInitialized | — | (registry) => void,插件就绪(标注 IO 等) | | onActiveChapterChange | — | 滚动焦点变化:{ chapterId, globalPageIndex, globalPageNumber, localPageIndex } | | onChapterAnnotationsLoading / Loaded / Error | — | 按需标注加载生命周期(须配置 options.annotations) | | onPdfPageBackgroundChange | — | PDF 页背景色变更:{ background }(含 ref.setPdfPageBackground) | | refChapterPdfViewerExpose) | — | scrollToChapterloadChapterAnnotations(chapterId)isChapterAnnotationsLoadedsetPdfPageBackgroundgetPdfPageBackground | | 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);
  }}
/>

类型导出:MarkupStylesConfigMarkupKindStyleNoteMarkerUiConfigBookmarkMarkerUiConfigPageOverlayRecordPageOverlayRenderContextPageOverlaysFeatureConfigqrTargetToPageOverlayRecord 等来自 @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)、exportedAtchapters[chapterId] 下含 bookmarksnotesmarkuppageOverlays(矩形叠加层,可选)。

| 选项 | 说明 | | --- | --- | | export / importbookmarks / notes / markup / pageOverlays | 默认均为 true,可单独关闭 | | ensureChapterLoaded | 默认 true;导出/导入 markup 时,分段章会加载 全部段 再合并页码 | | mode | replace 清空后导入;merge 合并(书签按 anchor 去重) | | persistNotes / persistBookmarks | 导入完成后写回业务存储的回调 |

归档按 chapterId 分桶,不是 chapterId#s0。详见 docs/get-started/10-annotations-io.md12-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 可访问 | | 页码或滚动错位 | 检查 globalPageRangelocalPageRange 页数是否一致 | | 划词笔记没有保存 | 实现 notes.onCreateNote 或自定义 onRequestCreateNote 流程 | | 书签删除无效 | bookmarks.onRequestRemove 需要返回 true 才会删除 | | 分段章导出划线不全 | 确认各段 urls 可访问;导出含 markup 会拉全部分段 | | 误用 chapterId#s1 存业务数据 | 对外 API 只用 manifest 的 chapterId + localPageIndex |