@inkbar/core 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -7
- package/dist/dom.d.mts +610 -58
- package/dist/dom.d.mts.map +1 -1
- package/dist/dom.mjs +2387 -929
- package/dist/dom.mjs.map +1 -1
- package/dist/index.d.mts +17 -37
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +181 -25
- package/dist/index.mjs.map +1 -1
- package/dist/{signatures-BHYKTwYE.d.mts → signatures-By4B40DP.d.mts} +706 -620
- package/dist/signatures-By4B40DP.d.mts.map +1 -0
- package/dist/{signatures-DF2VmNVa.mjs → signatures-D68fuLlt.mjs} +1333 -325
- package/dist/signatures-D68fuLlt.mjs.map +1 -0
- package/package.json +4 -7
- package/dist/signatures-BHYKTwYE.d.mts.map +0 -1
- package/dist/signatures-DF2VmNVa.mjs.map +0 -1
- package/schema.json +0 -1356
package/README.md
CHANGED
|
@@ -49,13 +49,15 @@ const json = doc.exportAnnotations('json')
|
|
|
49
49
|
|
|
50
50
|
`file` can be a `File`, `Blob`, `Uint8Array`, a URL string, `{ url, key }` when the URL is signed and changes between loads, or `{ data, key }` for bytes you want to identify. `quads` is a list of quads in PDF user space.
|
|
51
51
|
|
|
52
|
+
`exportAnnotations('json')` returns a [Gloss](https://gloss-format.org) document, an open JSON format for PDF annotations, and `importAnnotations` reads one. Gloss has no loupe, mask, pen pressure or free-text border colour, so Inkbar writes them under `custom.inkbar`. It writes a loupe as a circle and a mask as a circle or square, so other Gloss readers can still draw them. Inkbar leaves your own keys under `custom` as they are. In the store and the API, loupes, masks, `pressure` and `borderColor` keep their plain names.
|
|
53
|
+
|
|
52
54
|
Writing annotations back into the PDF needs `@inkbar/engine`, which is not released yet. Until then `save()` rejects with `EngineUnavailable`. Persist `exportAnnotations('json')` instead and re-import it on the next open. `doc.key` stays the same between opens only when you open with a key: a URL string, `{ url, key }` or `{ data, key }`. A `File`, `Blob` or raw bytes get a new random key each time.
|
|
53
55
|
|
|
54
56
|
The DOM layer (`@inkbar/core/dom`) provides the viewport, canvas renderer, text layer, overlay and tools for hosts without a framework package. Vue hosts should use [`@inkbar/vue`](https://github.com/inkbar/inkbar/tree/main/packages/vue), React hosts [`@inkbar/react`](https://github.com/inkbar/inkbar/tree/main/packages/react), Svelte hosts [`@inkbar/svelte`](https://github.com/inkbar/inkbar/tree/main/packages/svelte).
|
|
55
57
|
|
|
56
58
|
### Without a framework
|
|
57
59
|
|
|
58
|
-
`createViewer` opens a document and renders it into an element, with a comment thread popover for notes and highlights
|
|
60
|
+
`createViewer` opens a document and renders it into an element, with a comment thread popover for notes and highlights. The toolbars are yours: build them from the controls below, which use the same markup as the framework packages, so `@inkbar/theme` styles them.
|
|
59
61
|
|
|
60
62
|
```ts
|
|
61
63
|
import {
|
|
@@ -101,7 +103,7 @@ viewer.open(lockedFile, { password }) // this password applies to this load only
|
|
|
101
103
|
viewer.destroy()
|
|
102
104
|
```
|
|
103
105
|
|
|
104
|
-
It takes the [`mountViewer` options](#mounting-a-viewer) except `container` and `document`, plus `source` and `assets` (both required), `password`, `defaults`, `history`, `onReady` and `onError`. Set `threads`
|
|
106
|
+
It takes the [`mountViewer` options](#mounting-a-viewer) except `container` and `document`, plus `source` and `assets` (both required), `password`, `defaults`, `history`, `onReady` and `onError`. Set `threads` to `false` to leave out the thread popover. `viewer.status`, `viewer.document`, `viewer.engine`, `viewer.viewport` and `viewer.tools` are readables; the last two change when the engine rebuilds. `viewer.activated` is a readable too: the id of the annotation the user last clicked or activated with Enter, or `null`. `viewer.setActivated(id)` activates one from your code without calling `onActivate`; it ignores an id the document doesn't have, and `setActivated(null)` clears it. A password you pass to `open` replaces the `createViewer` one for that load. The controls are `createToolButton`, `createUndo`, `createRedo`, `createZoomIn`, `createZoomOut`, `createZoomSelect`, `createPageInput`, `createPageCount`, `createPrevPage`, `createNextPage` and `createCommandButton`. Each one disables itself while no document is ready and lives as long as its viewer. Buttons show an icon from [`@inkbar/icons`](https://github.com/inkbar/inkbar/tree/main/packages/icons). Replace their children with your own icon and the accessible name stays; `createIcon` draws any `@inkbar/icons` icon for your own buttons. `createToolbar` gives one Tab stop to all its buttons, with arrow keys between them, and `createToolbarSeparator` divides groups of controls in it. `createCommandButton(viewer, id)` places a plugin command: a `div.ink-command` holding a button named by the command's label (disabled with the command; while it runs, it keeps focus with `aria-busy` and `aria-disabled`, and a press does nothing) and a `span.ink-command-status[role="status"]` with its status text; for an id no plugin registered it logs an `InkError('UnknownCommand')` to the console, naming the id and the ones that are registered, and returns the same markup disabled and hidden (`aria-hidden`, no label). Pass `plugins: [...]` to `createViewer` to register plugins; their tools join the viewer's, and `createToolButton` takes a plugin tool's own label and icon. `viewer.commands.get(id)` returns a command. An input, select or textarea you add keeps its arrow keys and its own `tabindex`: it is a Tab stop unless you give it `tabindex="-1"`.
|
|
105
107
|
|
|
106
108
|
`createViewer` reads the theme's colours from the element you pass, so that element needs to be in the page when you call it.
|
|
107
109
|
|
|
@@ -130,11 +132,14 @@ Options:
|
|
|
130
132
|
- `gap`, `buffer` and `customAnnotations` are fixed for a build. Change them with `engine.rebuild`.
|
|
131
133
|
- `toolOptions` holds the tools' options, such as the shape kind. Pass one from `createToolOptions()` and keep it to carry the options over to the next document; `createViewer` keeps one for its whole life. The tool style (colours, line style, text style) belongs to the document: each new document starts from the theme's defaults.
|
|
132
134
|
- `signatures` is where saved signatures live (see [Signatures](#signatures)); `signatureFonts` lists the handwriting fonts a typed signature offers.
|
|
135
|
+
- `decorateAnnotation(annotation)` returns your own `data`, `classes` and `description` for an annotation's element, or `undefined`. `{ data: { 'my-flag': 'on' } }` writes `data-my-flag="on"`, and `description` becomes the element's `aria-description`. Inkbar re-applies them whenever the annotations change. Call `engine.redecorate()` when they depend on anything else. Core's own attributes and `ink-` classes always win.
|
|
133
136
|
- `onActivate(id)` fires when the user activates an annotation.
|
|
134
137
|
- `onBuild(viewport, tools)` fires after every build, the first one included, before `mountViewer` returns. Subscribe to the new viewport and tool controller here.
|
|
135
138
|
- `onPageMount(page, mount, host)` and `onPageUnmount(page)` fire as pages enter and leave the mounted range.
|
|
136
139
|
- `onPageError(page, error)` reports a page whose canvas or text layer failed, and `onPageError(page, null)` once every failed layer renders again. Unmounting a failed page emits no `null`, so clear any per-page error in `onPageUnmount`.
|
|
137
140
|
|
|
141
|
+
To open your own card next to an annotation, use `createPopover`: it anchors a panel to the annotation's element, follows it as pages mount and unmount, and calls your `onDismiss` on Escape or a press outside. `placePanel` positions a panel against any other rectangle.
|
|
142
|
+
|
|
138
143
|
The viewport mounts pages synchronously, so a build's page callbacks fire before its `onBuild`. During a rebuild, `engine.viewport` and `engine.tools` still point at the destroyed objects while those callbacks run. Read the arguments instead, or wait for `onBuild`.
|
|
139
144
|
|
|
140
145
|
## Annotation changes
|
|
@@ -164,16 +169,22 @@ A callout's corner handles resize its text box: the tip and bend stay, and the p
|
|
|
164
169
|
|
|
165
170
|
## Limits of PDF import
|
|
166
171
|
|
|
167
|
-
Inkbar reads the annotations embedded in a PDF through PDF.js, which passes on only part of each one. Import keeps `intent` (`/IT`) for every type.
|
|
172
|
+
Inkbar reads the annotations embedded in a PDF through PDF.js, which passes on only part of each one. Import keeps `intent` (`/IT`) for every type. Once the document is ready, Inkbar reads from the file itself the fields PDF.js leaves out: a FreeText's `callout`, `fringe`, `head` and font, and a note's icon. They arrive as one `replace` change with `reason: 'embedded'`. That change adds no undo step and keeps the undo history.
|
|
173
|
+
|
|
174
|
+
Inkbar does not read callout, icon or font fields from encrypted PDFs, from annotations stored as direct objects, or from a file with a broken cross-reference table.
|
|
175
|
+
|
|
176
|
+
One gap remains:
|
|
168
177
|
|
|
169
|
-
-
|
|
170
|
-
- Read a note's `/Name` when the note has an appearance stream. PDF.js reports such notes as `NoIcon`, so they import with the default icon, `Note`.
|
|
171
|
-
- Keep line endings Inkbar does not know. PDF.js turns them into `None`. Annotations loaded from JSON keep them.
|
|
178
|
+
- Keep line endings Inkbar does not know on lines and polylines. PDF.js turns them into `None`. Annotations loaded from JSON keep them.
|
|
172
179
|
|
|
173
180
|
## Tools and keyboard
|
|
174
181
|
|
|
175
182
|
`createToolController` from `@inkbar/core/dom` routes pointer and key events on the viewport to the active tool: `select-text`, `select`, `highlight`, `redact-area`, `sketch`, `draw`, `shape`, `freetext`, `signature` or `note`. Highlights, text redactions, notes and signatures can be made from the keyboard. Area redactions, sketches and drawings need a pointer.
|
|
176
183
|
|
|
184
|
+
Most creation tools are one-shot: `shape`, `freetext`, `note`, `redact-area` and `signature` each make one thing, select it, then hand back to `select-text`. After a one-shot tool has made something, a press on empty page or `Escape` clears the selection and switches to `select-text` without making anything more. The press that closes a new text box, or a new note's popover, counts as that press too. Until then, what it made keeps its edit handles: drag it to move it, drag a handle to resize it, and the tool stays on. A press on another annotation switches to `select-text` and selects it, and a drag moves it. Removing what the tool made (`Delete`, undo, or a controlled host refusing the add) leaves the tool on for another go, and the selection moving to something else switches to `select-text`. Picking a tool again starts it afresh. While the tool waits for that press, the viewport carries `data-ink-made`. `draw`, `sketch` and `highlight` are sticky: they stay on until you pick another tool.
|
|
185
|
+
|
|
186
|
+
A host tool says which it is with `kind: 'one-shot' | 'sticky'` (no `kind` is sticky). A one-shot tool adds and selects what it makes, then calls `ctx.made(ids)`; `ctx.done()` clears the selection and switches to `select-text` itself. Under a sticky tool, `ctx.made` does nothing. The controller's `active` readable reports every switch, including one a one-shot tool makes.
|
|
187
|
+
|
|
177
188
|
| Where focus is | Key | Effect |
|
|
178
189
|
| ---------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
179
190
|
| Viewport, `highlight` tool | `Enter` | Turns the text selection into a highlight |
|
|
@@ -213,10 +224,82 @@ The `shape` tool's `loupe` kind adds a loupe: a circle that shows the page under
|
|
|
213
224
|
|
|
214
225
|
The `shape` tool's `mask` and `ovalMask` kinds add a mask: the page is dimmed everywhere outside a rectangle or an ellipse (`shape: 'rect' | 'ellipse'`), by `dim` (0 to 1, 0.5 by default) in black. A drag sizes it, with `Shift` for a square or circle, and a click places one 72 pt across. Like the loupe it needs a pointer: `Enter` places no mask. A page has one mask: adding a second replaces the first in the same undo step, and in a controlled store in the same `onChange`. The dimming is an SVG (`svg[data-ink-mask-layer]`) between the page and the annotation overlay that never takes the pointer; the mask annotation's own element is the shape, and only a thin band along its edge takes presses. Inside the shape the page works as usual: select text, highlight it, add shapes or notes. The style controls have nothing to change on a mask.
|
|
215
226
|
|
|
216
|
-
With either select tool (`select-text` or `select`), a selected area redaction, text box, shape, loupe, mask, polyline, polygon or drawing shows four corner handles (`.ink-handle[data-handle="nw|ne|sw|se"]`), and a selected line shows one on each end (`data-handle="start|end"`). Dragging a corner resizes the annotation, scaling its points with it; `Shift` keeps its aspect ratio. Dragging a line end moves that end only. The store records a single update on release. Drag the annotation itself to move it, or any member of a multi-selection to move them all in one undo step. Text markup in the selection stays on its text unless it is the one you drag.
|
|
227
|
+
With either select tool (`select-text` or `select`), or on what a one-shot tool just made, a selected area redaction, text box, shape, loupe, mask, polyline, polygon or drawing shows four corner handles (`.ink-handle[data-handle="nw|ne|sw|se"]`), and a selected line shows one on each end (`data-handle="start|end"`). Dragging a corner resizes the annotation, scaling its points with it; `Shift` keeps its aspect ratio. Dragging a line end moves that end only. The store records a single update on release. Drag the annotation itself to move it, or any member of a multi-selection to move them all in one undo step. Text markup in the selection stays on its text unless it is the one you drag.
|
|
217
228
|
|
|
218
229
|
A press on text markup (a highlight, underline, strikeout or squiggly) does nothing at first. A click selects it; a drag selects the text under it, as a drag on the bare text would. Presses on every other annotation select it straight away.
|
|
219
230
|
|
|
231
|
+
## Plugins
|
|
232
|
+
|
|
233
|
+
A plugin adds commands, tools, annotation decorations, page layers and its own records to a viewer. Write it once with `definePlugin` from `@inkbar/core/dom` and it runs in every framework package and in `createViewer`.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
import type { RecordBase, Rect } from '@inkbar/core'
|
|
237
|
+
import { definePlugin } from '@inkbar/core/dom'
|
|
238
|
+
|
|
239
|
+
interface Suggestion extends RecordBase {
|
|
240
|
+
page: number // 0-based
|
|
241
|
+
rect: Rect // in PDF user space
|
|
242
|
+
reason: string
|
|
243
|
+
status: 'suggested' | 'dismissed'
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// The id is lower-case kebab; everything the plugin adds is named under it.
|
|
247
|
+
export const review = definePlugin('review', (plugin) => {
|
|
248
|
+
// The open document's `review.suggestions` records; empty while none is open.
|
|
249
|
+
const suggestions = plugin.records<Suggestion>('suggestions')
|
|
250
|
+
|
|
251
|
+
// Placed as 'review.suggest'. The returned string becomes the command's status.
|
|
252
|
+
plugin.command('suggest', 'Suggest redactions', async ({ doc, signal }) => {
|
|
253
|
+
const found = await findSuggestions(doc, { signal }) // your own code
|
|
254
|
+
return `${suggestions.in(doc).addMissing(found).length} suggestions`
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// A button over each open suggestion, opening a card styled by `@inkbar/theme`.
|
|
258
|
+
plugin.marks(suggestions, {
|
|
259
|
+
where: (s) => s.status === 'suggested',
|
|
260
|
+
rect: (s) => s.rect,
|
|
261
|
+
label: (s) => `Suggested redaction: ${s.reason}`,
|
|
262
|
+
card: (s) => ({
|
|
263
|
+
title: s.reason,
|
|
264
|
+
actions: { Dismiss: () => suggestions.update(s.id, { status: 'dismissed' }) },
|
|
265
|
+
}),
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`definePlugin(id, setup)` checks the id and returns a factory. Pass `plugins: [review()]` to the viewer. Nothing a plugin registers shows by itself: place a command with `InkCommand` (`createCommandButton(viewer, 'review.suggest')` without a framework), and a plugin tool with a tool button, as you place the built-in ones.
|
|
271
|
+
|
|
272
|
+
Inkbar keeps no plugin data. The host loads and saves a plugin's records through the factory's `records` option, which `plugin.records(name)` picks up by name: `review({ records: { suggestions: { initial, onChange } } })`. `initial(doc)` returns the records a document opens with; `onChange(list, change)` runs after every change. Options passed to `plugin.records(name, { initial, onChange })` win over the host's.
|
|
273
|
+
|
|
274
|
+
Three rules cover undo and stale results:
|
|
275
|
+
|
|
276
|
+
- Every records call is one undo step, and `plugin.batch` makes several one step.
|
|
277
|
+
- Inside a command, write through `records.in(doc)`. A write through the plain handle after an `await` goes to whatever document is open by then; `in(doc)` writes to the run's document, and nothing once that document closes.
|
|
278
|
+
- Each card action is one undo step.
|
|
279
|
+
|
|
280
|
+
A command's `run` gets `doc`, the document open when it started, and a `signal` that aborts when that document closes or another opens. Returning `undefined` clears the status. A failed run is logged and sets the status to `errorStatus` (default "Something went wrong."); the status clears when the document changes.
|
|
281
|
+
|
|
282
|
+
Marks redraw when their records change, undo and redo included, and keep their elements, so focus and an open card survive a zoom. Chain `.scrollToFirst()` onto `plugin.marks(…)` to scroll the viewer to new marks: when records start to get a mark, it scrolls to the first in reading order, unless that one is in view already. It moves no focus, and the records a document opens with, undo and redo never scroll. Each carries `data-record` and `data-plugin`; `style` (custom properties included), `className` and `attrs` add your own look. A card has a `title`, optional `label`, `meta`, `swatch` and `body`, and `actions`. Actions must be synchronous: a write after an `await` falls outside the action's undo step. The first action is the primary one and takes focus. After an action that removes the mark, focus moves to the next of this `marks()` call's marks, else the previous one, else the viewport. A mark with a card has `aria-haspopup="dialog"`, and `aria-expanded` is true while its card is open. Escape or a press outside closes the card and returns focus to the mark. `@inkbar/theme` styles `.ink-card`, `.ink-card-title`, `.ink-card-meta`, `.ink-card-swatch`, `.ink-card-body` and `.ink-card-actions`, and the card's `className` adds your own class.
|
|
283
|
+
|
|
284
|
+
`suggestions.annotates('redact', { from, onRemoved })` links records to the annotations made from them. Its `add(record)` adds an annotation from `from(record)`, stores the link at `custom.<plugin>.<records name>` (here `custom.review.suggestions`) and fills the type's default colour when `from` gives none. `of(record)` returns the linked annotations. `onRemoved(record, annotation)` and `onChanged(record, annotation)` run inside the user's undo step, never on undo or redo. Call it in `setup`. The link travels with the annotation through a Gloss export and import.
|
|
285
|
+
|
|
286
|
+
`quadsFromRect(rect)` and `rectFromQuads(quads)` from `@inkbar/core` convert between a box and the quads that text markup takes, and `doc.pageText(page)` resolves to a page's text joined with single spaces.
|
|
287
|
+
|
|
288
|
+
### Going further
|
|
289
|
+
|
|
290
|
+
The `plugin` that `setup` receives is also the plugin's context, so everything below works in the new form. The object form, `definePlugin({ id, setup(ctx) { … } })`, still works and returns the plugin itself: pass `plugins: [plugin]`.
|
|
291
|
+
|
|
292
|
+
A command added with `ctx.commands.add` is disabled until a document is ready, unless it passes its own `enabled` readable, and it is busy, and disabled, until the promise `run` returns settles. A failed run is logged, never thrown. `ctx.store(name)` gives each document its own record store, keyed `<plugin>.<name>`, and its changes undo with annotation edits; `ctx.onAnnotationEdit(fn)` runs inside each user edit's undo step. `ctx.history.batch(fn)` makes everything `fn` changes, annotations and records alike, one undo step. Load records with `initial(doc)` and save them from `onChange`. `ctx.scrollTo(page, rect)` scrolls the viewer to a rect in user space and moves no focus.
|
|
293
|
+
|
|
294
|
+
#### Plugin authors
|
|
295
|
+
|
|
296
|
+
- A layer (`ctx.layer`) lets presses through to the page, and the elements you put straight into it take them back (`pointer-events: auto`), with or without `@inkbar/theme`. That default has no specificity, so a rule of yours such as `pointer-events: none` on a mark wins.
|
|
297
|
+
- A tool you add with `ctx.tools.add` is placed under its namespaced id (`review.stamp`), but its methods run on your own object, so inside them `this.id` is the id you gave it (`stamp`). A class instance keeps its `#private` fields.
|
|
298
|
+
- Presses on a mark you draw in your own layer bubble up to the viewer's root. Stop propagation if listeners there shouldn't see them. `plugin.marks` does this for you.
|
|
299
|
+
- A layer's `destroy` can run after the document's stores have closed, so check `store.get()` for `null` there.
|
|
300
|
+
- Decorations from `ctx.decorate` re-apply when the annotations change. Call `ctx.redecorate()` when one reads state of your own that changed. Marks from `plugin.marks` take `refresh()` for the same job.
|
|
301
|
+
- The framework packages run `setup` once the viewer has mounted in the browser, never while rendering on the server. Until then the plugin's commands are disabled, and anything `setup` creates, such as a store, doesn't exist yet.
|
|
302
|
+
|
|
220
303
|
## Signatures
|
|
221
304
|
|
|
222
305
|
A signature here is a mark on the page, not a cryptographic signature of the file. Draw one, type your name in a handwriting font, or upload a scan, and it is saved as a template. The `signature` tool places the chosen template: a click puts it about 150 pt wide, centred on the press, and `Enter` puts it at the centre of the current page. It is turned against the page's `/Rotate`, so it reads upright on the page; a view rotation turns it with the page. A drawn signature becomes `ink` and the others a `stamp` (an image), both with `subject: 'Signature'`. Both keep their aspect ratio when resized.
|