@embedpdf/plugin-annotation

中文文档
3.2.0发布于 2026/11/12Apache-2.0TypeScriptESM + CJS近 30 天下载 180.4万

Annotation feature plugin: the pure annotation-core wired to the engine repository (optimistic create/patch/delete) + the interaction hub (ambient edit + draw tools). Behaviors (forms) plug in here.

原始使用文档
文档目录5 节
  1. Three kinds of data, one owner each
  2. The life of one change
  3. How a record renders
  4. Where things live
  5. Tests

@embedpdf/plugin-annotation

The annotation plugin: drawing, selecting, moving, restyling, typing into and deleting annotations, over a document the engine owns. It wires the pure @embedpdf/core-annotation to the engine and to the interaction hub. It has no framework code; the adapters render its reads.

Three kinds of data, one owner each

What the user sees is built from three kinds of data. Each has exactly one owner, and nothing else writes it.

DataWhat it isOwnerChanged by
confirmedevery annotation record the engine confirmed, with its appearance versionthe records mirror (sync/records.ts), records.get()confirmed document events, from any session, and loads
pendingthis session's changes the engine hasn't answered yet, one per user actionthe kernel's change queue (ctx.changes)a user action stages one; it leaves once the engine answered
sessionselection, hover, the gesture in progress, text editing, tool settings, the object numbers held for new recordssession in the plugin state (model.ts)the core's update

The records mirror shows the pending changes on top of what the engine confirmed (records.view()): it replays them, op by op, through predictRecords, the same rules the engine applies. The view (read/view.ts) turns those records into the core's records, each drawn as the change on it says, and composes them with the session into the core's Model. Every read and every gesture takes that model. It is memoized per record, so a change to one record does not recompute the others.

The life of one change

A user drags a square to a new place:

 pointer up
    │
    ▼
 store.commit({ type: 'editPointer', phase: 'up', … })          services/store.ts
    │
    ├─ update(model, message)                                   core-annotation
    │     → session   (the drag is over)
    │     → change    (put: the square at its new place)
    │     → effects   ([{ type: 'patch', id: 'obj:12', patch: { box } }])
    │
    ├─ op builders                                              write/ops.ts
    │     patch → { type: 'annotations.update', ref, patch }
    │     (an annotation's link children follow it: write/links.ts)
    │
    └─ ctx.changes.stage({ label, ops, undo })                  the kernel
          the records mirror predicts it: the view shows the square at its
          new place at once. The queue sends it after everything staged
          before it; the engine applies it, all or nothing, and publishes
          `annotations.updated` before it answers:
            the records mirror folds it → confirmed obj:12 is at the new place
          the change leaves the view → the view shows the confirmed record

One user action is one change: moving five annotations is one change of five ops, one request, one undo step. The rules:

  • Refused (a revoked grant, a lock set elsewhere): the change leaves the view at once, and the view shows the confirmed record, including anything another session changed meanwhile. Nothing has to be undone by hand, and onWriteFailed tells the UI why. A change that names a record only a refused change would have created is refused with it.
  • Applied: the change leaves the view once the records mirror holds the answer, which is at once when its events folded; if a fold asked to read a page again, the view keeps it until that read lands.
  • In order: the engine answers changes in the order they were staged, so a newer change always shows over an older one's answer.

The same pipeline carries every kind of change:

  • New annotations take an object number the session holds (services/object-numbers.ts), so a new record has its final ref from its first frame: it can be selected, edited, linked or deleted at once, and each of those is simply staged after its create. A replace-text caret and its strikeout are two creates in one change, the strikeout answering the caret by its number. Should the document's pool be empty for a moment, the create waits for numbers (the drawing stays on screen).
  • Deletes: the record and what goes with it (its replies, grouped parts, popups) leave the view at once, and come back if the engine refuses.
  • Typing goes into one held change per pause (write/text-editing.ts): every keystroke amends it, so the text shows at once, and it is sent after a pause, when editing ends, when anything else is staged, and before a download. A format on the words selected while typing goes into it too.
  • Changes stated in code (create, update, delete, reorder, comments, links, scripts) go through the store's second door, apply: each is checked with the engine's own functions, shown at once, and sent, answered and refused exactly like a gesture's. One call is one change.

How a record renders

A confirmed record renders from the engine's appearance raster (baked) unless this session edited or created it, in which case it renders live from its description (vector): the vector preference in the state. Another session's change hands the record back to the raster (sync/confirmed.ts). While a change of this session is pending, how the record is drawn follows from the change itself (core drawnAfter, the engine's verdict on it): a move carries the raster along, anything else visible draws live, and a change the raster already shows (a flag) leaves it as it is. Stamps and widgets have no live rendering and always use a raster: a stamp's keeps its proportions in a resized box (the engine fits it contain), and a stamp this session placed shows the preview it was placed with until the engine's picture of it exists (read/render.ts). The raster is fetched again exactly when the record's appearance version changes, which the mirror advances when the engine reports a re-baked appearance.

Where things live

FolderWhat is in it
controller.tsthe composition root: builds the services, wires every area, assembles the capability
model.tsthe plugin state and its transitions (withSession, render preferences, …)
services/what every area is built on: the store, object numbers, events, authority
sync/the records mirror (what the engine confirmed, and its prediction) and what follows
read/the view, and the reads the capability exposes (annotations, render items, chrome)
write/the verbs, and the op builders that turn core effects into the engine's ops
comments/the comment threads lens over the same records
tools/tool definitions, the registry, the tool's ghost, and the pointer handlers on the hub

Tests

test/harness.ts runs the controller on the kernel's test context with a fake engine that behaves like the real ones: it applies one change at a time, in the order they were sent, all or nothing, and publishes its events before it answers; a create gets the object number it names. test/pending.test.ts is the place to start: what the user sees while, and after, a change is on its way. test/interleavings.test.ts plays seeded random sequences of changes, refusals and remote updates, and checks the view (values and how the record renders) after every step.

版本与发布记录

@embedpdf/plugin-annotation 共发布 114 个版本,首次发布于 2025/06/13;过去 12 个月发布了 64 个版本。

版本发布日期距今
3.2.0latest2026/11/121 天前
3.1.02026/11/094 天前
3.0.02026/11/076 天前
3.0.0-next.23next2026/11/076 天前
3.0.0-next.222026/11/049 天前
3.0.0-next.212026/11/0310 天前

关于 @embedpdf/plugin-annotation 的常见问题

@embedpdf/plugin-annotation 是什么?

Annotation feature plugin: the pure annotation-core wired to the engine repository (optimistic create/patch/delete) + the interaction hub (ambient edit + draw tools). Behaviors (forms) plug in here.

如何安装 @embedpdf/plugin-annotation?

在项目目录运行 npm install @embedpdf/plugin-annotation,或使用 pnpm add / yarn add / bun add。

@embedpdf/plugin-annotation 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@embedpdf/plugin-annotation 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@embedpdf/plugin-annotation 可以商用吗?

@embedpdf/plugin-annotation 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@embedpdf/plugin-annotation 的下载量有多少?

根据 npm 官方下载统计,@embedpdf/plugin-annotation 在 2026/10/14 – 2026/11/12 这 30 天内被下载 1,804,069 次,日均约 60,136 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@embedpdf/plugin-annotation 还在维护吗?

最新版本 3.2.0 发布于 2026/11/12(1 天前);至今共发布 114 个版本,其中过去 12 个月发布了 64 个;首次发布于 2025/06/13。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。