@inkbar/core 0.3.0 → 0.5.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 +57 -7
- package/dist/dom.d.mts +848 -43
- package/dist/dom.d.mts.map +1 -1
- package/dist/dom.mjs +5520 -769
- package/dist/dom.mjs.map +1 -1
- package/dist/index.d.mts +99 -17
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/dist/signatures-BwaXi7C8.d.mts +2284 -0
- package/dist/signatures-BwaXi7C8.d.mts.map +1 -0
- package/dist/signatures-CMapTKrC.mjs +4712 -0
- package/dist/signatures-CMapTKrC.mjs.map +1 -0
- package/package.json +5 -6
- package/dist/document-BftlPZKU.d.mts +0 -1455
- package/dist/document-BftlPZKU.d.mts.map +0 -1
- package/dist/threads-2USvyGpX.mjs +0 -1901
- package/dist/threads-2USvyGpX.mjs.map +0 -1
- package/schema.json +0 -650
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
-
`@inkbar/core` opens PDFs, renders pages with PDF.js and keeps annotations as typed data. The Vue, React and Svelte packages build on it. You can also use it on its own: `createViewer` puts a complete viewer, with
|
|
23
|
+
`@inkbar/core` opens PDFs, renders pages with PDF.js and keeps annotations as typed data. The Vue, React and Svelte packages build on it. You can also use it on its own: `createViewer` puts a complete viewer, with comment threads, redaction review and controls to build your toolbars from, into any element in a plain TypeScript app.
|
|
24
24
|
|
|
25
25
|
Every coordinate is stored in PDF user space (points, origin bottom-left, before `/Rotate`), so annotations stay put whatever the zoom, rotation or screen.
|
|
26
26
|
|
|
@@ -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
|
|
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 {
|
|
@@ -74,7 +76,7 @@ const viewer = createViewer(el, { source: file, assets, zoom: 'fit-width' })
|
|
|
74
76
|
|
|
75
77
|
const tools = createToolbar()
|
|
76
78
|
tools.append(
|
|
77
|
-
createToolButton(viewer, '
|
|
79
|
+
createToolButton(viewer, 'select-text'),
|
|
78
80
|
createToolButton(viewer, 'select'),
|
|
79
81
|
createToolButton(viewer, 'highlight')
|
|
80
82
|
)
|
|
@@ -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 `
|
|
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` and `createNextPage`. 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. 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
|
|
|
@@ -128,11 +130,16 @@ Options:
|
|
|
128
130
|
- `container` and `document`, both required.
|
|
129
131
|
- `zoom` (a number, `'fit-width'` or `'fit-page'`; default `'fit-width'`), `page`, `mode`, `rotation` and `tool` set the starting view. Change them later through `engine.viewport` and `engine.tools`.
|
|
130
132
|
- `gap`, `buffer` and `customAnnotations` are fixed for a build. Change them with `engine.rebuild`.
|
|
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.
|
|
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.
|
|
131
136
|
- `onActivate(id)` fires when the user activates an annotation.
|
|
132
137
|
- `onBuild(viewport, tools)` fires after every build, the first one included, before `mountViewer` returns. Subscribe to the new viewport and tool controller here.
|
|
133
138
|
- `onPageMount(page, mount, host)` and `onPageUnmount(page)` fire as pages enter and leave the mounted range.
|
|
134
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`.
|
|
135
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
|
+
|
|
136
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`.
|
|
137
144
|
|
|
138
145
|
## Annotation changes
|
|
@@ -148,15 +155,42 @@ The viewport mounts pages synchronously, so a build's page callbacks fire before
|
|
|
148
155
|
|
|
149
156
|
`doc.history.batch(fn)` records everything `fn` changes as one undo step, so a host that adds many annotations at once can undo them together. Change events still fire once per annotation. `fn` must be synchronous, and nested calls join the outer batch. In controlled mode `doc.history` records nothing; the framework packages' `useHistory().batch` works in both modes.
|
|
150
157
|
|
|
158
|
+
## PDF names in annotations
|
|
159
|
+
|
|
160
|
+
Values taken from PDF names use PDF's spelling: `OpenArrow` and `ROpenArrow` for line endings, `Comment` and `NewParagraph` for note icons, `FreeTextCallout` and `LineArrow` for intents. Other valid PDF names (no spaces, none of `( ) < > [ ] { } / %`) are kept and written back unchanged, so `icon: 'Check'` survives an import and export. `LINE_ENDINGS`, `NOTE_ICONS` and `INTENTS` list the names Inkbar knows, and `isKnown(list, name)` tests one. A line ending Inkbar does not know draws as `None`, and every note icon draws the same icon.
|
|
161
|
+
|
|
162
|
+
## Callouts and speech bubbles
|
|
163
|
+
|
|
164
|
+
A free-text callout is stored as PDF and XFDF store it: `intent: 'FreeTextCallout'`, a `rect` that holds the text box and the leader line together, a `fringe` (`{ left, top, right, bottom }`, PDF `/RD`) that insets `rect` to the text box, and a `callout` of two or three points running from the tip, through an optional bend, to where the line meets the box. `head` is the ending at the tip.
|
|
165
|
+
|
|
166
|
+
A speech bubble is a standard callout plus Inkbar's `cornerRadius`. Inkbar draws it as one rounded outline with a wedge to the tip. Other viewers ignore `cornerRadius` and draw a box with a leader line.
|
|
167
|
+
|
|
168
|
+
A callout's corner handles resize its text box: the tip and bend stay, and the point on the box moves with the edge it sits on. Its tip handle moves the tip alone. Each edit refits `rect` and `fringe` in the same update, so it is one undo step.
|
|
169
|
+
|
|
170
|
+
## Limits of PDF import
|
|
171
|
+
|
|
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:
|
|
177
|
+
|
|
178
|
+
- Keep line endings Inkbar does not know on lines and polylines. PDF.js turns them into `None`. Annotations loaded from JSON keep them.
|
|
179
|
+
|
|
151
180
|
## Tools and keyboard
|
|
152
181
|
|
|
153
|
-
`createToolController` from `@inkbar/core/dom` routes pointer and key events on the viewport to the active tool: `
|
|
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.
|
|
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.
|
|
154
187
|
|
|
155
188
|
| Where focus is | Key | Effect |
|
|
156
189
|
| ---------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
157
190
|
| Viewport, `highlight` tool | `Enter` | Turns the text selection into a highlight |
|
|
158
191
|
| Viewport, `redact-area` tool | `Enter` | Turns the text selection into text redactions |
|
|
159
192
|
| Viewport, `note` tool | `Enter` | Places a note at the centre of the current page and focuses it |
|
|
193
|
+
| Viewport, `signature` tool | `Enter` | Places the chosen signature at the centre of the current page |
|
|
160
194
|
| Viewport | `+` or `=`, `-` | Zoom in, zoom out |
|
|
161
195
|
| Viewport | `Home`, `End`, `PageUp`, `PageDown` | First, last, previous, next page |
|
|
162
196
|
| Annotation | `Enter` or `Space` | Selects and activates it |
|
|
@@ -182,9 +216,25 @@ On a focused annotation, `Enter` or `Space` selects it and calls `onActivate`; w
|
|
|
182
216
|
|
|
183
217
|
Locked annotations, such as those embedded in the file, ignore move, resize and delete. Without a text selection, `Enter` in the `highlight` and `redact-area` tools does nothing.
|
|
184
218
|
|
|
185
|
-
`redact-area` draws an area mark when you drag. To redact text instead, select the text with the `
|
|
219
|
+
`redact-area` draws an area mark when you drag, even from a highlight. To redact text instead, select the text with the `select-text` tool, switch to `redact-area`, then click inside the selection or press `Enter`. The tool keeps the press from starting a new text selection, so it cannot select text itself.
|
|
220
|
+
|
|
221
|
+
`sketch` draws freehand ink, one annotation per stroke. A stroke that looks like a line, an arrow, a rectangle, an ellipse, a polygon or a star becomes that shape, in the shape style. A click, or a stroke under 4 CSS px across, stays ink. One undo turns the shape back into the stroke, and a second removes the stroke. Draw each shape in one stroke: for an arrow, draw the shaft, then the head, without lifting. The recogniser is exported from `@inkbar/core` for your own input: `recogniseStroke(points)` takes user-space points and returns the shape with a score from 0 to 1, or `undefined` when the points fit no shape. A stroke counts as a shape at `RECOGNISE_THRESHOLD` (0.8) or above.
|
|
222
|
+
|
|
223
|
+
The `shape` tool's `loupe` kind adds a loupe: a circle that shows the page under its centre at `magnification` times the size (2 by default), with a rim in the shape style's border colour and width. A drag sizes it, always as a circle, and a click places one 72 pt across. It needs a pointer: `Enter` places no loupe. It magnifies the page itself, not the annotations under it, and redraws a moment after it moves, resizes or the zoom changes. Pass the overlay a `document` (page mounts and the viewer do) or a loupe shows only its rim.
|
|
224
|
+
|
|
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.
|
|
226
|
+
|
|
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.
|
|
228
|
+
|
|
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.
|
|
230
|
+
|
|
231
|
+
## Signatures
|
|
232
|
+
|
|
233
|
+
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.
|
|
234
|
+
|
|
235
|
+
Your app owns the templates. Pass a `SignatureStore` (`list`, `save`, `remove`, each returning a promise) as `signatures`. `createMemorySignatureStore()` is the default, and forgets everything on reload. `createLocalSignatureStore(key)` keeps them in `localStorage` and falls back to memory where storage is blocked. When storage is full, `save` rejects and the dialog says the signature was not saved. An uploaded image is scaled to at most 1000 px on its long side before it is stored. Typed signatures use the fonts in `signatureFonts`. Load each one with `@font-face` before offering it.
|
|
186
236
|
|
|
187
|
-
|
|
237
|
+
`createSignatureMenu(viewer)` lists the saved signatures, removes them, and opens `createSignatureDialog`, which draws, types or uploads a new one and selects it.
|
|
188
238
|
|
|
189
239
|
## Serving PDF.js assets
|
|
190
240
|
|