@ingcreators/annot-core 0.1.0 → 0.2.1
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/CHANGELOG.md +227 -0
- package/README.md +35 -11
- package/dist/encode/options.d.ts +28 -5
- package/dist/encode/quantize-median-cut.d.ts +53 -0
- package/dist/index.js +6 -8
- package/dist/xmp/xmp-browser.d.ts +13 -15
- package/dist/xmp/xmp-bytes.d.ts +118 -0
- package/dist/xmp-bytes.js +265 -0
- package/package.json +8 -4
- package/styles/editor.css +24 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,232 @@
|
|
|
1
1
|
# @ingcreators/annot-core
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f485646: **Expose `./xmp-bytes` subpath on the published tarball.**
|
|
8
|
+
The workspace `package.json#exports` declared
|
|
9
|
+
`"./xmp-bytes": "./src/xmp/xmp-bytes.ts"` from day one, but
|
|
10
|
+
`publishConfig.exports` only declared `.` + `./styles/*` —
|
|
11
|
+
so every workspace subpath (`./headless`, `./editor/*`,
|
|
12
|
+
`./xmp-bytes`, etc.) was stripped from the published
|
|
13
|
+
tarball.
|
|
14
|
+
|
|
15
|
+
This patch unblocks the `./xmp-bytes` subpath specifically.
|
|
16
|
+
`@ingcreators/annot-product-docs-astro@0.2.1`'s playwright
|
|
17
|
+
fixture imports `@ingcreators/annot-core/xmp-bytes` (via
|
|
18
|
+
the bundled `writePngWithTagsOnly` helper); without this
|
|
19
|
+
patch, any consumer doing
|
|
20
|
+
`import { test } from "@ingcreators/annot-product-docs-astro/playwright"`
|
|
21
|
+
hits `Package subpath './xmp-bytes' is not defined`.
|
|
22
|
+
|
|
23
|
+
## Fix
|
|
24
|
+
|
|
25
|
+
`vite.config.ts` switches to multi-entry library mode:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
lib: {
|
|
29
|
+
entry: {
|
|
30
|
+
index: resolve(__dirname, "src/index.ts"),
|
|
31
|
+
"xmp-bytes": resolve(__dirname, "src/xmp/xmp-bytes.ts"),
|
|
32
|
+
},
|
|
33
|
+
formats: ["es"],
|
|
34
|
+
},
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`package.json#publishConfig.exports` gains the matching
|
|
38
|
+
`./xmp-bytes` entry:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
"./xmp-bytes": {
|
|
42
|
+
"types": "./dist/xmp/xmp-bytes.d.ts",
|
|
43
|
+
"default": "./dist/xmp-bytes.js"
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Bundle landing in the tarball:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
dist/xmp-bytes.js 7.55 kB │ gzip: 2.67 kB
|
|
51
|
+
dist/xmp/xmp-bytes.d.ts declarations colocated by the dts plugin
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Other subpaths still missing
|
|
55
|
+
|
|
56
|
+
The workspace `exports` map declares 17+ subpaths (`./headless`,
|
|
57
|
+
`./editor`, `./editor/*`, `./icons`, `./xmp`, `./zip`,
|
|
58
|
+
`./utils`, `./desktop-bridge`, `./storage`, `./encode/*`,
|
|
59
|
+
`./auto-capture-options`, …). Most are Tier C (browser-only)
|
|
60
|
+
and shouldn't ship as standalone published bundles — Tier C
|
|
61
|
+
code lives in `@ingcreators/annot-editor` / `-render` for
|
|
62
|
+
npm consumers. The few Tier A subpaths that DO make sense
|
|
63
|
+
on npm (`./headless`, `./storage`, `./utils`, `./zip`,
|
|
64
|
+
`./zip-bytes`) can ship in a separate follow-up patch
|
|
65
|
+
when a concrete downstream consumer needs them.
|
|
66
|
+
|
|
67
|
+
## After this PR republishes
|
|
68
|
+
|
|
69
|
+
`@ingcreators/annot-product-docs-astro` needs a `0.2.2`
|
|
70
|
+
republish that bumps its `@ingcreators/annot-core` dep
|
|
71
|
+
from `0.2.0` to `^0.2.1` so the playwright fixture can
|
|
72
|
+
finally resolve `./xmp-bytes` at consumer install time.
|
|
73
|
+
Once both land, the `examples/workflow-app/` tour can
|
|
74
|
+
migrate from the hybrid `captureScreen + page.screenshot`
|
|
75
|
+
pair to the unified `page.screenshot({ annot: { mdx } })`
|
|
76
|
+
single call.
|
|
77
|
+
|
|
78
|
+
## 0.2.0
|
|
79
|
+
|
|
80
|
+
### Minor Changes
|
|
81
|
+
|
|
82
|
+
- 2e8d397: Switch the client-capture PNG-8 default quantizer to the pure-TS
|
|
83
|
+
Median Cut at `@ingcreators/annot-core/encode/quantize-median-cut`.
|
|
84
|
+
`DEFAULT_ENCODE_OPTIONS.quantizer` is now `"median-cut"`. The
|
|
85
|
+
`@ingcreators/annot-imagequant` WASM import and the `ensureWasm()`
|
|
86
|
+
initialisation path are removed from `@ingcreators/annot-core/encode`.
|
|
87
|
+
The `quantizer: "wasm"` value on `EncodeOptions` is retained for
|
|
88
|
+
back-compat (now resolves to the same Median Cut implementation)
|
|
89
|
+
so consumers that persisted `quantizer: "wasm"` to localStorage
|
|
90
|
+
keep working unchanged.
|
|
91
|
+
|
|
92
|
+
Phase 2 of `docs/plans/replace-libimagequant-with-median-cut.md`.
|
|
93
|
+
Phase 3 retires the annotator / MCP dynamic-import path; Phase 4
|
|
94
|
+
deletes the workspace package and deprecates the published
|
|
95
|
+
`@ingcreators/annot-imagequant@0.1.0` on npm.
|
|
96
|
+
|
|
97
|
+
- df1a429: **`@ingcreators/annot-annotator` — new `Annotator.toEditablePng()`
|
|
98
|
+
method** that returns a re-editable PNG. The bytes carry the same
|
|
99
|
+
visible pixels as `toPng()` plus the original un-annotated capture +
|
|
100
|
+
the annotations SVG embedded in the PNG's XMP / custom `svGo` chunk.
|
|
101
|
+
Re-opening the file in the Annot editor (or `annot.work/app/`)
|
|
102
|
+
restores the annotations as selectable / movable / restylable
|
|
103
|
+
objects rather than a flat bitmap.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const annotator = createAnnotator();
|
|
107
|
+
const editablePng = annotator.toEditablePng({
|
|
108
|
+
originalDataUrl,
|
|
109
|
+
annotationsSvg,
|
|
110
|
+
width,
|
|
111
|
+
height,
|
|
112
|
+
tags: {
|
|
113
|
+
source: "playwright-fixture",
|
|
114
|
+
capturedAt: new Date().toISOString(),
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
await writeFile("shot.png", editablePng);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Image viewers that don't know about the custom chunks display the
|
|
121
|
+
rasterised pixels verbatim — no compatibility loss vs `toPng()`.
|
|
122
|
+
|
|
123
|
+
The existing `toPng()` / `toSvg()` / `toEncoded()` methods are
|
|
124
|
+
unchanged — `toEditablePng()` is purely additive.
|
|
125
|
+
|
|
126
|
+
**`@ingcreators/annot-core` — new `/xmp-bytes` Tier-A subpath**
|
|
127
|
+
exposing the pure-bytes XMP encode / decode primitives that used to
|
|
128
|
+
live (Blob-wrapped) inside `/xmp`:
|
|
129
|
+
- `createEditablePngBytes(opts) -> Uint8Array` — write a re-editable
|
|
130
|
+
PNG. Takes raw PNG bytes for both the rasterised image and the
|
|
131
|
+
original capture; no `Blob` / `FileReader` dependency. The
|
|
132
|
+
function the new `Annotator.toEditablePng()` is built on.
|
|
133
|
+
- `readEditablePngBytes(data) -> AnnotMetadata | null` — PNG-only
|
|
134
|
+
reader.
|
|
135
|
+
- `readEditableImage(data) -> AnnotMetadata | null` — dual PNG /
|
|
136
|
+
JPEG reader (moved here from `/xmp`, also re-exported from `/xmp`
|
|
137
|
+
for source-compat).
|
|
138
|
+
- `WELL_KNOWN_TAG_KEYS` — soft-convention key names for the
|
|
139
|
+
optional `tags` field (`source` / `screen` / `capturedAt` /
|
|
140
|
+
`commit`).
|
|
141
|
+
|
|
142
|
+
Existing `@ingcreators/annot-core/xmp` consumers stay working
|
|
143
|
+
without source changes — `xmp-browser.ts` re-exports the Tier-A
|
|
144
|
+
surface alongside its Blob-wrapped `createEditableImage`.
|
|
145
|
+
|
|
146
|
+
- 5e74421: Add `quantizeMedianCut` — a pure-TS Median Cut + Floyd–Steinberg
|
|
147
|
+
dither quantizer at `@ingcreators/annot-core/encode/quantize-median-cut`
|
|
148
|
+
— and an opt-in `quantizer?: "wasm" | "median-cut"` field on
|
|
149
|
+
`EncodeOptions`. Default stays `"wasm"` (libimagequant via the
|
|
150
|
+
existing GPL-3.0 WASM dependency) so production behaviour is
|
|
151
|
+
unchanged. Phase 1 of
|
|
152
|
+
`docs/plans/replace-libimagequant-with-median-cut.md`; Phase 2
|
|
153
|
+
will flip the default to `"median-cut"` and Phase 4 will retire
|
|
154
|
+
the `"wasm"` branch entirely.
|
|
155
|
+
- 4768855: **`@ingcreators/annot-product-docs-astro` — new `/playwright`
|
|
156
|
+
subpath** that re-exports an extended Playwright `test` fixture
|
|
157
|
+
whose `page.screenshot()` accepts a compositional `annot: { … }`
|
|
158
|
+
option:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { test } from "@ingcreators/annot-product-docs-astro/playwright";
|
|
162
|
+
|
|
163
|
+
test("login flow", async ({ page }) => {
|
|
164
|
+
await page.goto("/login");
|
|
165
|
+
await page.screenshot({
|
|
166
|
+
path: "public/login.png",
|
|
167
|
+
annot: {
|
|
168
|
+
mdx: { id: "login", path: "src/content/docs/login.mdx" },
|
|
169
|
+
tags: { source: "docs-tour", capturedAt: new Date().toISOString() },
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The `annot` option is compositional — each field is an
|
|
176
|
+
independent contribution to the embedded XMP record:
|
|
177
|
+
- `mdx: { id, path }` — refresh the MDX's `annot:snapshot` block
|
|
178
|
+
against the current page, then resolve `<Overlay match>` blocks
|
|
179
|
+
for the named `<Screen>`.
|
|
180
|
+
- `overlays: BboxAnnotation[]` — caller-supplied annotations
|
|
181
|
+
(same DSL `@ingcreators/annot-annotator` accepts). Merged with
|
|
182
|
+
MDX-derived overlays when both are present.
|
|
183
|
+
- `tags: Record<string, string>` — provenance metadata written
|
|
184
|
+
verbatim into the XMP. No auto-fill — callers write the
|
|
185
|
+
`WELL_KNOWN_TAG_KEYS` they want.
|
|
186
|
+
- `editable: boolean` (default `true`) — toggle between
|
|
187
|
+
"annotations preserved as SVG layer + embedded original"
|
|
188
|
+
(re-editable in Annot Cloud) and "annotations baked into
|
|
189
|
+
visible pixels" (flat PNG, no XMP layer).
|
|
190
|
+
|
|
191
|
+
`page.screenshot()` calls WITHOUT `annot` fall through to
|
|
192
|
+
vanilla Playwright byte-for-byte — codegen / DevTools Recorder
|
|
193
|
+
output keeps working unedited.
|
|
194
|
+
|
|
195
|
+
Phase 1 of
|
|
196
|
+
`docs/plans/playwright-screenshot-annot-fixture.md`. Phase 2 will
|
|
197
|
+
add the same interception on `locator.screenshot()` with
|
|
198
|
+
coordinate rebasing for sub-region overlays.
|
|
199
|
+
|
|
200
|
+
Two helpers also exported from the main `@ingcreators/annot-product-docs-astro`
|
|
201
|
+
entry for callers who want to compose annotations themselves:
|
|
202
|
+
- `resolveMdxAnnotations({ mdxPath, screenId, dims })` — extract
|
|
203
|
+
the MDX's `<Overlay>` blocks into a typed `BboxNumberedBadgeAnnotation[]`
|
|
204
|
+
(the underlying step the fixture uses internally).
|
|
205
|
+
- `svgFromBboxAnnotations(annotations)` — wrap a
|
|
206
|
+
`BboxAnnotation[]` into a single-root `<svg>` ready for
|
|
207
|
+
`Annotator.toEditablePng()` / `toPng()`.
|
|
208
|
+
|
|
209
|
+
**`@ingcreators/annot-core` — new `writePngWithTagsOnly`
|
|
210
|
+
helper** exported from `/xmp-bytes` (and re-exported from
|
|
211
|
+
`/xmp`). Writes `tags` into a PNG's XMP iTXt chunk without
|
|
212
|
+
embedding an original capture or annotations layer — for the
|
|
213
|
+
"PNG with provenance metadata sidecar" path (CI failure
|
|
214
|
+
screenshots, VRT references, etc.). The resulting bytes are
|
|
215
|
+
still a valid PNG and the Annot editor treats them as a normal
|
|
216
|
+
PNG (no `<annot:annotations>` element → not editable round-trip;
|
|
217
|
+
opens as fresh canvas).
|
|
218
|
+
|
|
219
|
+
### Patch Changes
|
|
220
|
+
|
|
221
|
+
- 780985d: Remove the `@ingcreators/annot-imagequant` workspace dependency
|
|
222
|
+
graph entirely. The package's GPL-3.0 WASM was replaced by the
|
|
223
|
+
pure-TS Median Cut quantizer in Phases 1–3 of
|
|
224
|
+
`docs/plans/_done/replace-libimagequant-with-median-cut.md`;
|
|
225
|
+
Phase 4 deletes the workspace package itself + the `verify-wasm`
|
|
226
|
+
CI job + the Cargo dependabot watch + the issue-template option +
|
|
227
|
+
the published 0.1.0 on npm (via the `npm deprecate` operator
|
|
228
|
+
action documented in the Phase 4 PR description).
|
|
229
|
+
|
|
3
230
|
## 0.1.0
|
|
4
231
|
|
|
5
232
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# @ingcreators/annot-core
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@ingcreators/annot-core)
|
|
4
|
+
[](https://github.com/ingcreators/annot/blob/main/LICENSE)
|
|
5
|
+
|
|
3
6
|
The shared core of [Annot](../../README.md). Two tiers live here:
|
|
4
7
|
|
|
5
8
|
- **Tier A** — pure Node, DOM-free. Storage types, SVG format
|
|
@@ -16,19 +19,40 @@ PropertyPanel, tools) live in [`@ingcreators/annot-editor`](../editor).
|
|
|
16
19
|
Data-driven canvas rendering (gallery thumbnails, the shared OOXML
|
|
17
20
|
DrawingML builder) lives in [`@ingcreators/annot-render`](../render).
|
|
18
21
|
|
|
22
|
+
## Install (npm)
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install @ingcreators/annot-core
|
|
26
|
+
# or
|
|
27
|
+
pnpm add @ingcreators/annot-core
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Most consumers will install the higher-level
|
|
31
|
+
[`@ingcreators/annot-annotator`](https://www.npmjs.com/package/@ingcreators/annot-annotator)
|
|
32
|
+
or [`@ingcreators/annot-playwright`](https://www.npmjs.com/package/@ingcreators/annot-playwright)
|
|
33
|
+
instead — those packages inline the parts of `annot-core` they
|
|
34
|
+
need. Install `annot-core` directly only when building a custom
|
|
35
|
+
headless tool against the SDK.
|
|
36
|
+
|
|
19
37
|
## Public entry points
|
|
20
38
|
|
|
21
|
-
| Subpath | Tier | Surface |
|
|
22
|
-
|
|
23
|
-
| `@ingcreators/annot-core` | A | Re-exports everything from `/headless` — DOM-free root entry |
|
|
24
|
-
| `@ingcreators/annot-core/headless` | A | Same as root, kept as an alias for callers that want to be explicit |
|
|
25
|
-
| `@ingcreators/annot-core/storage` | A | `ImageRecord`, `FolderRecord`, `PageElement`, `PageMetadata`, `StorageProvider` |
|
|
26
|
-
| `@ingcreators/annot-core/utils` | A | `assertNonNull`, `computeDasharray`, `newIdB58`, defaults |
|
|
27
|
-
| `@ingcreators/annot-core/zip` | A | ZIP builder used by PPTX export |
|
|
28
|
-
| `@ingcreators/annot-core/encode` | A | Image encode helpers |
|
|
29
|
-
| `@ingcreators/annot-core/editor` | B | jsdom-friendly element helpers + `TOOL_REGISTRY` / `PROPERTY_CONTROLS` |
|
|
30
|
-
| `@ingcreators/annot-core/xmp` | browser | `createEditableImage` / `readEditableImage` round-trip |
|
|
31
|
-
| `@ingcreators/annot-core/desktop-bridge` | browser | Electron desktop-host IPC + `isDesktop` detection |
|
|
39
|
+
| Subpath | Tier | Surface | Available on npm? |
|
|
40
|
+
|---------|------|---------|---|
|
|
41
|
+
| `@ingcreators/annot-core` | A | Re-exports everything from `/headless` — DOM-free root entry | ✅ |
|
|
42
|
+
| `@ingcreators/annot-core/headless` | A | Same as root, kept as an alias for callers that want to be explicit | workspace only (v0.1.0) |
|
|
43
|
+
| `@ingcreators/annot-core/storage` | A | `ImageRecord`, `FolderRecord`, `PageElement`, `PageMetadata`, `StorageProvider` | workspace only (v0.1.0) |
|
|
44
|
+
| `@ingcreators/annot-core/utils` | A | `assertNonNull`, `computeDasharray`, `newIdB58`, defaults | workspace only (v0.1.0) |
|
|
45
|
+
| `@ingcreators/annot-core/zip` | A | ZIP builder used by PPTX export | workspace only (v0.1.0) |
|
|
46
|
+
| `@ingcreators/annot-core/encode` | A | Image encode helpers | workspace only (v0.1.0) |
|
|
47
|
+
| `@ingcreators/annot-core/editor` | B | jsdom-friendly element helpers + `TOOL_REGISTRY` / `PROPERTY_CONTROLS` | workspace only (v0.1.0) |
|
|
48
|
+
| `@ingcreators/annot-core/xmp` | browser | `createEditableImage` / `readEditableImage` round-trip | workspace only (v0.1.0) |
|
|
49
|
+
| `@ingcreators/annot-core/desktop-bridge` | browser | Electron desktop-host IPC + `isDesktop` detection | workspace only (v0.1.0) |
|
|
50
|
+
|
|
51
|
+
Subpath imports for npm consumers land in a later minor — the
|
|
52
|
+
v0.1.0 published tarball bundles everything into the root
|
|
53
|
+
`./dist/index.js` entry. Workspace consumers (within this
|
|
54
|
+
monorepo) keep using the full subpath map via the source-side
|
|
55
|
+
exports.
|
|
32
56
|
|
|
33
57
|
The Tier A surface is CI-enforced by
|
|
34
58
|
[`src/headless.test.ts`](./src/headless.test.ts): it imports every
|
package/dist/encode/options.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Encode option types + defaults — split out of `./index.ts` so
|
|
3
|
-
* that only need the data shape (e.g. the web app's
|
|
4
|
-
* loader) can import this leaf without dragging in
|
|
5
|
-
*
|
|
6
|
-
* encoder lazy-loadable as a separate chunk.
|
|
2
|
+
* Encode option types + defaults — split out of `./index.ts` so
|
|
3
|
+
* callers that only need the data shape (e.g. the web app's
|
|
4
|
+
* preferences loader) can import this leaf without dragging in
|
|
5
|
+
* the heavier encoder + quantizer module that `encodeCapture`
|
|
6
|
+
* requires. Keeps the encoder lazy-loadable as a separate chunk.
|
|
7
7
|
*/
|
|
8
8
|
export type EncodeFormat = "smart" | "png" | "jpeg";
|
|
9
9
|
/**
|
|
@@ -23,6 +23,23 @@ export declare const SAVE_SIZE_MAX_WIDTH: Record<SaveSizePreset, number | null>;
|
|
|
23
23
|
* + the (future) extension settings UI. Shared so both surfaces
|
|
24
24
|
* speak the same language. */
|
|
25
25
|
export declare const SAVE_SIZE_LABEL: Record<SaveSizePreset, string>;
|
|
26
|
+
/**
|
|
27
|
+
* Quantizer backend for PNG-8 smart-mode encoding.
|
|
28
|
+
*
|
|
29
|
+
* Both values currently resolve to the in-tree pure-TS Median Cut
|
|
30
|
+
* + Floyd–Steinberg dither at
|
|
31
|
+
* [`quantize-median-cut.ts`](./quantize-median-cut.ts). The
|
|
32
|
+
* `"wasm"` value is retained for back-compat with the Phase 1
|
|
33
|
+
* feature flag so consumers that persisted `quantizer: "wasm"` to
|
|
34
|
+
* localStorage keep working unchanged.
|
|
35
|
+
*
|
|
36
|
+
* Phase 2 of
|
|
37
|
+
* [`docs/plans/_done/replace-libimagequant-with-median-cut.md`](../../../../docs/plans/_done/replace-libimagequant-with-median-cut.md)
|
|
38
|
+
* flipped the default to Median Cut and removed the WASM init
|
|
39
|
+
* path. Phase 4 deletes the `@ingcreators/annot-imagequant`
|
|
40
|
+
* workspace package and removes this field entirely.
|
|
41
|
+
*/
|
|
42
|
+
export type Quantizer = "wasm" | "median-cut";
|
|
26
43
|
export interface EncodeOptions {
|
|
27
44
|
/** "smart" | "png" | "jpeg" */
|
|
28
45
|
format: EncodeFormat;
|
|
@@ -53,6 +70,12 @@ export interface EncodeOptions {
|
|
|
53
70
|
* `Settings.quality.saveSizePreset` field here.
|
|
54
71
|
*/
|
|
55
72
|
saveSizePreset?: SaveSizePreset;
|
|
73
|
+
/**
|
|
74
|
+
* Quantizer backend used by smart-mode PNG-8 output. Optional;
|
|
75
|
+
* both values resolve to the same Median Cut implementation
|
|
76
|
+
* post-Phase 2. See {@link Quantizer}.
|
|
77
|
+
*/
|
|
78
|
+
quantizer?: Quantizer;
|
|
56
79
|
}
|
|
57
80
|
export declare const DEFAULT_ENCODE_OPTIONS: EncodeOptions;
|
|
58
81
|
/** Compute the resize target for a given source size + preset.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-TS palette quantizer (Median Cut + Floyd–Steinberg dither).
|
|
3
|
+
*
|
|
4
|
+
* Drop-in replacement for the WASM `quantize_image` from the
|
|
5
|
+
* (GPL-3.0) `@ingcreators/annot-imagequant` package. Lives at
|
|
6
|
+
* Tier A — no DOM, no canvas, no WASM. Importable from Node and
|
|
7
|
+
* the browser identically.
|
|
8
|
+
*
|
|
9
|
+
* The API mirrors the existing internal contract so callers can
|
|
10
|
+
* swap one for the other without touching anything else:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* const { palette, indices } = quantizeMedianCut(rgba, w, h, 256);
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* - `palette`: flat RGBA8 bytes, length `numColors * 4`.
|
|
17
|
+
* - `indices`: one byte per pixel, length `w * h`.
|
|
18
|
+
*
|
|
19
|
+
* Deterministic: same input always produces the same output.
|
|
20
|
+
* Priority-queue ties are broken by insertion order; splits use
|
|
21
|
+
* a stable sort. Useful for snapshot tests.
|
|
22
|
+
*
|
|
23
|
+
* Algorithm summary:
|
|
24
|
+
*
|
|
25
|
+
* 1. Build a histogram (Map<rgba32, count>) over the input.
|
|
26
|
+
* 2. Split samples below an alpha threshold into a dedicated
|
|
27
|
+
* "transparent" palette entry (index 0).
|
|
28
|
+
* 3. Place the remaining samples into a single bounding box and
|
|
29
|
+
* repeatedly split the highest-priority box along its longest
|
|
30
|
+
* RGB edge at the population-weighted median, until the
|
|
31
|
+
* target palette size is reached.
|
|
32
|
+
* 4. Each box contributes one palette entry — the population-
|
|
33
|
+
* weighted mean of its samples.
|
|
34
|
+
* 5. Remap the source image to palette indices with
|
|
35
|
+
* Floyd–Steinberg error diffusion in scanline order.
|
|
36
|
+
*
|
|
37
|
+
* @see `quantize-median-cut.test.ts` for the property tests.
|
|
38
|
+
*/
|
|
39
|
+
export interface QuantizeResult {
|
|
40
|
+
/** Flat RGBA8 palette bytes, length = numColors * 4 (1 ≤ N ≤ 256). */
|
|
41
|
+
palette: Uint8Array;
|
|
42
|
+
/** One byte per pixel, indexing into the palette. */
|
|
43
|
+
indices: Uint8Array;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Quantize an RGBA image to ≤ `maxColors` palette entries.
|
|
47
|
+
*
|
|
48
|
+
* @param rgba Packed RGBA8 input, length must equal `width*height*4`.
|
|
49
|
+
* @param width Image width in pixels (≥ 1).
|
|
50
|
+
* @param height Image height in pixels (≥ 1).
|
|
51
|
+
* @param maxColors Target palette size, clamped to [1, 256].
|
|
52
|
+
*/
|
|
53
|
+
export declare function quantizeMedianCut(rgba: Uint8Array | Uint8ClampedArray, width: number, height: number, maxColors: number): QuantizeResult;
|
package/dist/index.js
CHANGED
|
@@ -502,14 +502,12 @@ function de(e, t, n, r, i, a, o) {
|
|
|
502
502
|
};
|
|
503
503
|
n.x += -s * 2, n.y += -c * 2, e.moveTo(d.x - l - u / t, d.y - u + l / t), e.lineTo(d.x, d.y), e.lineTo(d.x + u / t - l, d.y - u - l / t);
|
|
504
504
|
}
|
|
505
|
-
function fe(e, t, n, r, i, a
|
|
506
|
-
let
|
|
507
|
-
x:
|
|
508
|
-
y:
|
|
505
|
+
function fe(e, t, n, r, i, a) {
|
|
506
|
+
let o = n * a * Math.SQRT1_2, s = r * a * Math.SQRT1_2, c = n * (i + a), l = r * (i + a), u = {
|
|
507
|
+
x: t.x - o,
|
|
508
|
+
y: t.y - s
|
|
509
509
|
};
|
|
510
|
-
|
|
511
|
-
let p = t ? 3.4 : 2;
|
|
512
|
-
e.moveTo(f.x, f.y), e.lineTo(f.x - u / 2 - d / p, f.y + u / p - d / 2), e.lineTo(f.x - u, f.y - d), e.lineTo(f.x - u / 2 + d / p, f.y - d / 2 - u / p), e.close();
|
|
510
|
+
t.x += -c - o, t.y += -l - s, e.moveTo(u.x, u.y), e.lineTo(u.x - c / 2 - l / 2, u.y + c / 2 - l / 2), e.lineTo(u.x - c, u.y - l), e.lineTo(u.x - c / 2 + l / 2, u.y - l / 2 - c / 2), e.close();
|
|
513
511
|
}
|
|
514
512
|
function pe(e, t, n, r, i) {
|
|
515
513
|
let a = i / 2, o = {
|
|
@@ -542,7 +540,7 @@ function ve(e, t, n, r, i, a) {
|
|
|
542
540
|
case "triangle": return ue(e, s, !1, n, r, i, o, a), { filled: !0 };
|
|
543
541
|
case "stealth": return ue(e, s, !0, n, r, i, o, a), { filled: !0 };
|
|
544
542
|
case "arrow": return de(e, s, n, r, i, o, a), { filled: !1 };
|
|
545
|
-
case "diamond": return fe(e,
|
|
543
|
+
case "diamond": return fe(e, n, r, i, o, a), { filled: !0 };
|
|
546
544
|
case "oval": return pe(e, n, r, i, o), { filled: !0 };
|
|
547
545
|
}
|
|
548
546
|
return { filled: !1 };
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Browser-side XMP metadata embedding for re-editable images.
|
|
3
|
-
* Mirrors the Rust implementation in src-tauri/src/commands/xmp.rs
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Wraps the Tier-A `xmp-bytes` primitives with Blob input / Blob output
|
|
5
|
+
* and adds the JPEG output path, which needs an Image + <canvas> pipeline
|
|
6
|
+
* to transcode the rendered PNG to JPEG.
|
|
7
|
+
*
|
|
8
|
+
* For Node / non-browser callers, import the Tier-A primitives
|
|
9
|
+
* directly:
|
|
10
|
+
*
|
|
11
|
+
* import { createEditablePngBytes, readEditableImage }
|
|
12
|
+
* from "@ingcreators/annot-core/xmp-bytes";
|
|
13
|
+
*
|
|
14
|
+
* Re-exports of Tier-A symbols below keep existing
|
|
15
|
+
* `@ingcreators/annot-core/xmp` consumers working unchanged.
|
|
7
16
|
*/
|
|
17
|
+
export { type AnnotMetadata, type CreateEditablePngBytesOptions, createEditablePngBytes, readEditableImage, readEditablePngBytes, WELL_KNOWN_TAG_KEYS, writePngWithTagsOnly, } from './xmp-bytes.js';
|
|
8
18
|
export interface EditableImageOptions {
|
|
9
19
|
/** Rendered image (screenshot + annotations) as Blob */
|
|
10
20
|
renderedBlob: Blob;
|
|
@@ -25,15 +35,3 @@ export interface EditableImageOptions {
|
|
|
25
35
|
* Returns a Blob ready for download.
|
|
26
36
|
*/
|
|
27
37
|
export declare function createEditableImage(opts: EditableImageOptions): Promise<Blob>;
|
|
28
|
-
export interface AnnotMetadata {
|
|
29
|
-
originalImageDataUrl: string;
|
|
30
|
-
annotationsSvg: string;
|
|
31
|
-
width: number;
|
|
32
|
-
height: number;
|
|
33
|
-
tags: Record<string, string>;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Read XMP metadata from a re-editable image file (JPEG or PNG).
|
|
37
|
-
* Pass the file as an ArrayBuffer or Uint8Array.
|
|
38
|
-
*/
|
|
39
|
-
export declare function readEditableImage(data: Uint8Array): AnnotMetadata | null;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier-A XMP metadata embedding for re-editable images — pure-bytes
|
|
3
|
+
* primitives that run in Node and the browser alike.
|
|
4
|
+
*
|
|
5
|
+
* This file holds the byte-level XMP build / read / write logic that
|
|
6
|
+
* used to live inside `xmp-browser.ts`. The browser-side
|
|
7
|
+
* `createEditableImage` (Blob in → Blob out) is now a thin wrapper
|
|
8
|
+
* around `createEditablePngBytes` for the PNG path.
|
|
9
|
+
*
|
|
10
|
+
* JPEG WRITE remains browser-only because the rendered PNG → JPEG
|
|
11
|
+
* conversion needs an Image + <canvas> pipeline. The JPEG READ path
|
|
12
|
+
* is pure bytes and lives here.
|
|
13
|
+
*
|
|
14
|
+
* PNG: XMP in iTXt chunk, original image in a custom "svGo" chunk.
|
|
15
|
+
* JPEG: XMP in APP1 segment, original image in one or more APP2
|
|
16
|
+
* segments (each prefixed with `annot:OriginalImage\0`).
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildXmp(annotationsSvg: string, width: number, height: number, tags?: Record<string, string>): string;
|
|
19
|
+
export declare function dataUrlToUint8Array(dataUrl: string): Uint8Array;
|
|
20
|
+
export declare function writeJpegWithMetadata(jpegData: Uint8Array, xmpBytes: Uint8Array, originalData: Uint8Array): Uint8Array;
|
|
21
|
+
export declare function writePngWithMetadata(pngData: Uint8Array, xmpBytes: Uint8Array, originalData: Uint8Array): Uint8Array;
|
|
22
|
+
/**
|
|
23
|
+
* Build a tags-only XMP packet — `<annot:tags>` element present,
|
|
24
|
+
* `<annot:annotations>` / `<annot:width>` / `<annot:height>` absent.
|
|
25
|
+
*
|
|
26
|
+
* Used for the "PNG with provenance metadata sidecar" path: image viewers
|
|
27
|
+
* see a normal PNG, the Annot editor reads no `<annot:annotations>` and
|
|
28
|
+
* treats it as an ordinary file, but XMP-aware tools (or this library's
|
|
29
|
+
* own reader) can extract the `tags` for downstream consumption.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildXmpTagsOnly(tags: Record<string, string>): string;
|
|
32
|
+
/**
|
|
33
|
+
* Write `tags` into a PNG's XMP iTXt chunk without embedding an original
|
|
34
|
+
* capture or annotations layer. The resulting bytes are still a valid PNG
|
|
35
|
+
* (image viewers display the rasterised pixels) and the Annot editor
|
|
36
|
+
* treats it as a normal PNG (no `<annot:annotations>` → not editable
|
|
37
|
+
* round-trip).
|
|
38
|
+
*
|
|
39
|
+
* Use this for "I want my CI failure screenshot to carry test-id /
|
|
40
|
+
* commit metadata" style provenance sidecars.
|
|
41
|
+
*
|
|
42
|
+
* Returns the input bytes unchanged if `tags` is empty.
|
|
43
|
+
*/
|
|
44
|
+
export declare function writePngWithTagsOnly(pngData: Uint8Array, tags: Record<string, string>): Uint8Array;
|
|
45
|
+
/**
|
|
46
|
+
* Common tag keys written by built-in Annot producers. **Not validated**
|
|
47
|
+
* at write time — `createEditablePngBytes` accepts any string/string
|
|
48
|
+
* pair. Documented as a soft convention so that downstream readers can
|
|
49
|
+
* key off the same names when present.
|
|
50
|
+
*
|
|
51
|
+
* - `source` — what produced the PNG (e.g. `"docs-tour"`,
|
|
52
|
+
* `"playwright-fixture"`, `"annot-mcp"`).
|
|
53
|
+
* - `screen` — for living-product-docs, the `<Screen id>` value.
|
|
54
|
+
* - `capturedAt` — ISO timestamp.
|
|
55
|
+
* - `commit` — git SHA when applicable.
|
|
56
|
+
*/
|
|
57
|
+
export declare const WELL_KNOWN_TAG_KEYS: readonly ["source", "screen", "capturedAt", "commit"];
|
|
58
|
+
export interface CreateEditablePngBytesOptions {
|
|
59
|
+
/** Rasterised PNG bytes — the visible image, with annotations already
|
|
60
|
+
* baked into the pixels. */
|
|
61
|
+
renderedPng: Uint8Array;
|
|
62
|
+
/** Original un-annotated capture. Pass either the raw PNG/JPEG bytes
|
|
63
|
+
* or a `data:` URL string. */
|
|
64
|
+
originalImage: Uint8Array | string;
|
|
65
|
+
/** Annotations-only SVG fragment (no `<svg>` wrapper required — this
|
|
66
|
+
* is what the editor's `readEditableImage` will reconstruct from). */
|
|
67
|
+
annotationsSvg: string;
|
|
68
|
+
/** Image width in pixels. Written into the XMP `<annot:width>` field. */
|
|
69
|
+
width: number;
|
|
70
|
+
/** Image height in pixels. Written into the XMP `<annot:height>` field. */
|
|
71
|
+
height: number;
|
|
72
|
+
/** Optional opaque kv tags. See {@link WELL_KNOWN_TAG_KEYS} for the
|
|
73
|
+
* soft convention. */
|
|
74
|
+
tags?: Record<string, string>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Write a re-editable PNG: takes a rasterised PNG (visible image) and
|
|
78
|
+
* embeds the original capture + annotations SVG in the XMP / custom
|
|
79
|
+
* chunks. The Annot editor reads the metadata back via
|
|
80
|
+
* {@link readEditablePngBytes} / {@link readEditableImage} and
|
|
81
|
+
* reconstructs an editable document.
|
|
82
|
+
*
|
|
83
|
+
* The returned bytes are still a valid PNG — image viewers that don't
|
|
84
|
+
* know about the custom chunks display the rasterised pixels verbatim.
|
|
85
|
+
*/
|
|
86
|
+
export declare function createEditablePngBytes(opts: CreateEditablePngBytesOptions): Uint8Array;
|
|
87
|
+
/** Parsed XMP metadata extracted from a re-editable image. */
|
|
88
|
+
export interface AnnotMetadata {
|
|
89
|
+
/** Original un-annotated capture re-emitted as a `data:` URL. Empty
|
|
90
|
+
* string when no original was embedded. MIME is inferred from the
|
|
91
|
+
* embedded bytes' magic header (PNG vs JPEG). */
|
|
92
|
+
originalImageDataUrl: string;
|
|
93
|
+
/** Annotations-only SVG fragment recovered from the XMP. */
|
|
94
|
+
annotationsSvg: string;
|
|
95
|
+
/** Image width in pixels. */
|
|
96
|
+
width: number;
|
|
97
|
+
/** Image height in pixels. */
|
|
98
|
+
height: number;
|
|
99
|
+
/** Opaque kv tags. Empty object when the XMP carried no `<annot:tags>`
|
|
100
|
+
* element or the embedded JSON was malformed. */
|
|
101
|
+
tags: Record<string, string>;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Read XMP metadata from a re-editable PNG.
|
|
105
|
+
*
|
|
106
|
+
* Returns `null` when the bytes aren't a PNG, when the PNG carries no
|
|
107
|
+
* Annot iTXt chunk, or when the XMP is missing the required
|
|
108
|
+
* `<annot:annotations>` field. For format-agnostic reading (PNG OR
|
|
109
|
+
* JPEG), use {@link readEditableImage}.
|
|
110
|
+
*/
|
|
111
|
+
export declare function readEditablePngBytes(data: Uint8Array): AnnotMetadata | null;
|
|
112
|
+
/**
|
|
113
|
+
* Read XMP metadata from a re-editable image — PNG or JPEG. Returns
|
|
114
|
+
* `null` for any other format, for files without the Annot custom
|
|
115
|
+
* metadata, and for files whose XMP is missing the required
|
|
116
|
+
* `<annot:annotations>` field.
|
|
117
|
+
*/
|
|
118
|
+
export declare function readEditableImage(data: Uint8Array): AnnotMetadata | null;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
//#region src/xmp/xmp-bytes.ts
|
|
2
|
+
var e = "annot", t = "https://ingcreators.com/annot/ns/1.0/", n = new TextEncoder().encode("http://ns.adobe.com/xap/1.0/\0"), r = new TextEncoder().encode("annot:OriginalImage\0"), i = new TextEncoder().encode("XML:com.adobe.xmp");
|
|
3
|
+
function a(n, r, i, a) {
|
|
4
|
+
let o = a && Object.keys(a).length > 0 ? JSON.stringify(a) : "";
|
|
5
|
+
return `<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
|
6
|
+
<x:xmpmeta xmlns:x="adobe:ns:meta/">
|
|
7
|
+
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
|
8
|
+
<rdf:Description rdf:about=""
|
|
9
|
+
xmlns:${e}="${t}">
|
|
10
|
+
<${e}:annotations><![CDATA[${n}]]></${e}:annotations>
|
|
11
|
+
<${e}:width>${r}</${e}:width>
|
|
12
|
+
<${e}:height>${i}</${e}:height>
|
|
13
|
+
<${e}:version>1.0</${e}:version>${o ? `\n <${e}:tags>${o}</${e}:tags>` : ""}
|
|
14
|
+
</rdf:Description>
|
|
15
|
+
</rdf:RDF>
|
|
16
|
+
</x:xmpmeta>
|
|
17
|
+
<?xpacket end="w"?>`;
|
|
18
|
+
}
|
|
19
|
+
function o(e) {
|
|
20
|
+
return new Uint8Array([e >> 8 & 255, e & 255]);
|
|
21
|
+
}
|
|
22
|
+
function s(e) {
|
|
23
|
+
return new Uint8Array([
|
|
24
|
+
e >> 24 & 255,
|
|
25
|
+
e >> 16 & 255,
|
|
26
|
+
e >> 8 & 255,
|
|
27
|
+
e & 255
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
function c(e, t) {
|
|
31
|
+
return e[t] << 8 | e[t + 1];
|
|
32
|
+
}
|
|
33
|
+
function l(e, t) {
|
|
34
|
+
return (e[t] << 24 | e[t + 1] << 16 | e[t + 2] << 8 | e[t + 3]) >>> 0;
|
|
35
|
+
}
|
|
36
|
+
function u(...e) {
|
|
37
|
+
let t = 0;
|
|
38
|
+
for (let n of e) t += n.length;
|
|
39
|
+
let n = new Uint8Array(t), r = 0;
|
|
40
|
+
for (let t of e) n.set(t, r), r += t.length;
|
|
41
|
+
return n;
|
|
42
|
+
}
|
|
43
|
+
function d(e, t, n) {
|
|
44
|
+
if (t + n.length > e.length) return !1;
|
|
45
|
+
for (let r = 0; r < n.length; r++) if (e[t + r] !== n[r]) return !1;
|
|
46
|
+
return !0;
|
|
47
|
+
}
|
|
48
|
+
function f(e) {
|
|
49
|
+
let t = e.split(",")[1] || "", n = atob(t), r = new Uint8Array(n.length);
|
|
50
|
+
for (let e = 0; e < n.length; e++) r[e] = n.charCodeAt(e);
|
|
51
|
+
return r;
|
|
52
|
+
}
|
|
53
|
+
function p(e, t) {
|
|
54
|
+
let n = t.length + 2;
|
|
55
|
+
return u(new Uint8Array([255, e]), o(n), t);
|
|
56
|
+
}
|
|
57
|
+
function m(e) {
|
|
58
|
+
let t = 65533 - r.length - 4, n = Math.ceil(e.length / t), i = [];
|
|
59
|
+
for (let a = 0; a < n; a++) {
|
|
60
|
+
let s = a * t, c = Math.min(s + t, e.length), l = e.slice(s, c), d = u(r, o(a), o(n), l);
|
|
61
|
+
i.push(p(226, d));
|
|
62
|
+
}
|
|
63
|
+
return u(...i);
|
|
64
|
+
}
|
|
65
|
+
function h(e) {
|
|
66
|
+
let t = [e.slice(0, 2)], i = 2;
|
|
67
|
+
for (; i + 4 <= e.length && e[i] === 255;) {
|
|
68
|
+
let a = e[i + 1];
|
|
69
|
+
if (a === 217 || a === 218) return t.push(e.slice(i)), u(...t);
|
|
70
|
+
let o = c(e, i + 2), s = i + 2 + o;
|
|
71
|
+
if (s > e.length) break;
|
|
72
|
+
let l = a === 225 && d(e, i + 4, n), f = a === 226 && d(e, i + 4, r);
|
|
73
|
+
!l && !f && t.push(e.slice(i, s)), i = s;
|
|
74
|
+
}
|
|
75
|
+
return i < e.length && t.push(e.slice(i)), u(...t);
|
|
76
|
+
}
|
|
77
|
+
function g(e, t, r) {
|
|
78
|
+
let i = p(225, u(n, t)), a = m(r), o = h(e);
|
|
79
|
+
return u(o.slice(0, 2), i, a, o.slice(2));
|
|
80
|
+
}
|
|
81
|
+
var _ = (() => {
|
|
82
|
+
let e = new Uint32Array(256);
|
|
83
|
+
for (let t = 0; t < 256; t++) {
|
|
84
|
+
let n = t;
|
|
85
|
+
for (let e = 0; e < 8; e++) n & 1 ? n = 3988292384 ^ n >>> 1 : n >>>= 1;
|
|
86
|
+
e[t] = n;
|
|
87
|
+
}
|
|
88
|
+
return e;
|
|
89
|
+
})();
|
|
90
|
+
function v(e) {
|
|
91
|
+
let t = 4294967295;
|
|
92
|
+
for (let n = 0; n < e.length; n++) t = _[(t ^ e[n]) & 255] ^ t >>> 8;
|
|
93
|
+
return (t ^ 4294967295) >>> 0;
|
|
94
|
+
}
|
|
95
|
+
function y(e, t) {
|
|
96
|
+
let n = u(e, t), r = v(n);
|
|
97
|
+
return u(s(t.length), n, s(r));
|
|
98
|
+
}
|
|
99
|
+
function b(e) {
|
|
100
|
+
let t = u(i, new Uint8Array([
|
|
101
|
+
0,
|
|
102
|
+
0,
|
|
103
|
+
0,
|
|
104
|
+
0,
|
|
105
|
+
0
|
|
106
|
+
]), e);
|
|
107
|
+
return y(new TextEncoder().encode("iTXt"), t);
|
|
108
|
+
}
|
|
109
|
+
function x(e) {
|
|
110
|
+
let t = [e.slice(0, 8)], n = 8;
|
|
111
|
+
for (; n + 12 <= e.length;) {
|
|
112
|
+
let r = l(e, n), a = e.slice(n + 4, n + 8), o = n + 8, s = o + r + 4;
|
|
113
|
+
if (s > e.length) break;
|
|
114
|
+
let c = String.fromCharCode(...a);
|
|
115
|
+
!(c === "iTXt" && d(e, o, i)) && c !== "svGo" && t.push(e.slice(n, s)), n = s;
|
|
116
|
+
}
|
|
117
|
+
return u(...t);
|
|
118
|
+
}
|
|
119
|
+
function S(e, t, n) {
|
|
120
|
+
let r = b(t), i = y(new TextEncoder().encode("svGo"), n), a = x(e), o = a.length - 12;
|
|
121
|
+
return u(a.slice(0, o), r, i, a.slice(o));
|
|
122
|
+
}
|
|
123
|
+
function C(n) {
|
|
124
|
+
return `<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
|
125
|
+
<x:xmpmeta xmlns:x="adobe:ns:meta/">
|
|
126
|
+
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
|
127
|
+
<rdf:Description rdf:about=""
|
|
128
|
+
xmlns:${e}="${t}">
|
|
129
|
+
<${e}:tags>${JSON.stringify(n)}</${e}:tags>
|
|
130
|
+
</rdf:Description>
|
|
131
|
+
</rdf:RDF>
|
|
132
|
+
</x:xmpmeta>
|
|
133
|
+
<?xpacket end="w"?>`;
|
|
134
|
+
}
|
|
135
|
+
function w(e, t) {
|
|
136
|
+
if (!t || Object.keys(t).length === 0) return e;
|
|
137
|
+
let n = b(new TextEncoder().encode(C(t))), r = x(e), i = r.length - 12;
|
|
138
|
+
return u(r.slice(0, i), n, r.slice(i));
|
|
139
|
+
}
|
|
140
|
+
var T = [
|
|
141
|
+
"source",
|
|
142
|
+
"screen",
|
|
143
|
+
"capturedAt",
|
|
144
|
+
"commit"
|
|
145
|
+
];
|
|
146
|
+
function E(e) {
|
|
147
|
+
let t = a(e.annotationsSvg, e.width, e.height, e.tags), n = new TextEncoder().encode(t), r = typeof e.originalImage == "string" ? f(e.originalImage) : e.originalImage;
|
|
148
|
+
return S(e.renderedPng, n, r);
|
|
149
|
+
}
|
|
150
|
+
function D(e) {
|
|
151
|
+
return A(e) ? M(e) : null;
|
|
152
|
+
}
|
|
153
|
+
function O(e) {
|
|
154
|
+
return k(e) ? j(e) : A(e) ? M(e) : null;
|
|
155
|
+
}
|
|
156
|
+
function k(e) {
|
|
157
|
+
return e[0] === 255 && e[1] === 216;
|
|
158
|
+
}
|
|
159
|
+
function A(e) {
|
|
160
|
+
return e[0] === 137 && e[1] === 80 && e[2] === 78 && e[3] === 71;
|
|
161
|
+
}
|
|
162
|
+
function j(e) {
|
|
163
|
+
let t = F(e), n = I(e);
|
|
164
|
+
return t ? N(t, n) : null;
|
|
165
|
+
}
|
|
166
|
+
function M(e) {
|
|
167
|
+
let t = L(e), n = R(e);
|
|
168
|
+
return t ? N(t, n) : null;
|
|
169
|
+
}
|
|
170
|
+
function N(e, t) {
|
|
171
|
+
let n = P(e, "annotations");
|
|
172
|
+
if (!n) return null;
|
|
173
|
+
let r = n.replace(/^<!\[CDATA\[/, "").replace(/\]\]>$/, ""), i = Number.parseInt(P(e, "width") || "0", 10), a = Number.parseInt(P(e, "height") || "0", 10), o = "";
|
|
174
|
+
t && t.length > 0 && (o = `data:${t[0] === 137 && t[1] === 80 ? "image/png" : "image/jpeg"};base64,${z(t)}`);
|
|
175
|
+
let s = {}, c = P(e, "tags");
|
|
176
|
+
if (c) try {
|
|
177
|
+
s = JSON.parse(c);
|
|
178
|
+
} catch {}
|
|
179
|
+
return {
|
|
180
|
+
originalImageDataUrl: o,
|
|
181
|
+
annotationsSvg: r,
|
|
182
|
+
width: i,
|
|
183
|
+
height: a,
|
|
184
|
+
tags: s
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function P(t, n) {
|
|
188
|
+
let r = `<${e}:${n}>`, i = `</${e}:${n}>`, a = t.indexOf(r);
|
|
189
|
+
if (a < 0) return null;
|
|
190
|
+
let o = t.indexOf(i, a);
|
|
191
|
+
return o < 0 ? null : t.substring(a + r.length, o);
|
|
192
|
+
}
|
|
193
|
+
function F(e) {
|
|
194
|
+
let t = 2;
|
|
195
|
+
for (; t + 4 <= e.length && e[t] === 255;) {
|
|
196
|
+
let r = e[t + 1];
|
|
197
|
+
if (r === 217 || r === 218) break;
|
|
198
|
+
let i = c(e, t + 2), a = t + 2 + i;
|
|
199
|
+
if (a > e.length) break;
|
|
200
|
+
if (r === 225 && d(e, t + 4, n)) {
|
|
201
|
+
let r = t + 4 + n.length;
|
|
202
|
+
return new TextDecoder().decode(e.slice(r, a));
|
|
203
|
+
}
|
|
204
|
+
t = a;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
function I(e) {
|
|
209
|
+
let t = r.length, n = [], i = 2;
|
|
210
|
+
for (; i + 4 <= e.length && e[i] === 255;) {
|
|
211
|
+
let a = e[i + 1];
|
|
212
|
+
if (a === 217 || a === 218) break;
|
|
213
|
+
let o = c(e, i + 2), s = i + 2 + o;
|
|
214
|
+
if (s > e.length) break;
|
|
215
|
+
if (a === 226 && d(e, i + 4, r)) {
|
|
216
|
+
let r = i + 4 + t;
|
|
217
|
+
if (r + 4 <= s) {
|
|
218
|
+
let t = c(e, r), i = e.slice(r + 4, s);
|
|
219
|
+
n.push({
|
|
220
|
+
seq: t,
|
|
221
|
+
data: i
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
i = s;
|
|
226
|
+
}
|
|
227
|
+
return n.length === 0 ? null : (n.sort((e, t) => e.seq - t.seq), u(...n.map((e) => e.data)));
|
|
228
|
+
}
|
|
229
|
+
function L(e) {
|
|
230
|
+
let t = 8;
|
|
231
|
+
for (; t + 12 <= e.length;) {
|
|
232
|
+
let n = l(e, t), r = String.fromCharCode(e[t + 4], e[t + 5], e[t + 6], e[t + 7]), a = t + 8, o = a + n + 4;
|
|
233
|
+
if (o > e.length) break;
|
|
234
|
+
if (r === "iTXt" && d(e, a, i)) {
|
|
235
|
+
let t = a + i.length, r = 0, o = t;
|
|
236
|
+
for (let i = t; i < a + n; i++) if (e[i] === 0 && r++, r >= 4) {
|
|
237
|
+
o = i + 1;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
return new TextDecoder().decode(e.slice(o, a + n));
|
|
241
|
+
}
|
|
242
|
+
t = o;
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
function R(e) {
|
|
247
|
+
let t = 8;
|
|
248
|
+
for (; t + 12 <= e.length;) {
|
|
249
|
+
let n = l(e, t), r = String.fromCharCode(e[t + 4], e[t + 5], e[t + 6], e[t + 7]), i = t + 8, a = i + n + 4;
|
|
250
|
+
if (a > e.length) break;
|
|
251
|
+
if (r === "svGo") return e.slice(i, i + n);
|
|
252
|
+
t = a;
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
function z(e) {
|
|
257
|
+
let t = 32768, n = "";
|
|
258
|
+
for (let r = 0; r < e.length; r += t) {
|
|
259
|
+
let i = e.subarray(r, Math.min(r + t, e.length));
|
|
260
|
+
n += String.fromCharCode.apply(null, i);
|
|
261
|
+
}
|
|
262
|
+
return btoa(n);
|
|
263
|
+
}
|
|
264
|
+
//#endregion
|
|
265
|
+
export { T as WELL_KNOWN_TAG_KEYS, a as buildXmp, C as buildXmpTagsOnly, E as createEditablePngBytes, f as dataUrlToUint8Array, O as readEditableImage, D as readEditablePngBytes, g as writeJpegWithMetadata, S as writePngWithMetadata, w as writePngWithTagsOnly };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ingcreators/annot-core",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Annot SDK — SVG-first screenshot annotation format, storage abstractions, and editor primitives. The foundation shared by the PWA, Chrome extension, Electron desktop, VSCode extension, and the headless annotator.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "ingcreators",
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
"types": "./dist/index.d.ts",
|
|
31
31
|
"default": "./dist/index.js"
|
|
32
32
|
},
|
|
33
|
+
"./xmp-bytes": {
|
|
34
|
+
"types": "./dist/xmp/xmp-bytes.d.ts",
|
|
35
|
+
"default": "./dist/xmp-bytes.js"
|
|
36
|
+
},
|
|
33
37
|
"./styles/*": "./styles/*"
|
|
34
38
|
},
|
|
35
39
|
"files": [
|
|
@@ -45,14 +49,14 @@
|
|
|
45
49
|
"@types/pako": "^2.0.4",
|
|
46
50
|
"typescript": "^6.0.3",
|
|
47
51
|
"vite": "^8.0.13",
|
|
48
|
-
"vite-plugin-dts": "^5.0.
|
|
49
|
-
"@ingcreators/annot-imagequant": "0.1.0"
|
|
52
|
+
"vite-plugin-dts": "^5.0.1"
|
|
50
53
|
},
|
|
51
54
|
"dependencies": {
|
|
52
55
|
"pako": "^2.1.0"
|
|
53
56
|
},
|
|
54
57
|
"scripts": {
|
|
55
58
|
"build": "vite build",
|
|
56
|
-
"typecheck": "tsc --noEmit"
|
|
59
|
+
"typecheck": "tsc --noEmit",
|
|
60
|
+
"bench:quantize": "node --experimental-strip-types --no-warnings scripts/bench-quantize.ts"
|
|
57
61
|
}
|
|
58
62
|
}
|
package/styles/editor.css
CHANGED
|
@@ -140,6 +140,14 @@ body {
|
|
|
140
140
|
0 10px,
|
|
141
141
|
10px -10px,
|
|
142
142
|
-10px 0;
|
|
143
|
+
/* Hidden by default — only the image editor mounts here. Gallery,
|
|
144
|
+
* doc, split-editor, and capture workspaces each own their own
|
|
145
|
+
* surface. Without this default, the checkered canvas backdrop
|
|
146
|
+
* paints on every boot before JS decides the route, producing a
|
|
147
|
+
* brief "empty editor" flash on gallery loads (e.g. `/app`, `/`,
|
|
148
|
+
* `/folder/...`). `body.editor-mode` below restores display when
|
|
149
|
+
* `EditorSession.setupEditor` mounts an image. */
|
|
150
|
+
display: none;
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
/* Editor SVG root.
|
|
@@ -171,9 +179,12 @@ body {
|
|
|
171
179
|
|
|
172
180
|
/* The contentEditable overlay lives inside a `<foreignObject>` —
|
|
173
181
|
bring text selection back so the user can mark / replace runs
|
|
174
|
-
while editing.
|
|
175
|
-
|
|
176
|
-
[data-annot-shell-root]
|
|
182
|
+
while editing. Selectors ordered ascending-by-specificity so
|
|
183
|
+
Biome's `noDescendingSpecificity` rule is satisfied:
|
|
184
|
+
`[data-annot-shell-root]` is (0,2,1); `#svg-root` is (1,1,1).
|
|
185
|
+
Same applied rules either way. */
|
|
186
|
+
[data-annot-shell-root] foreignObject [contenteditable="true"],
|
|
187
|
+
#svg-root foreignObject [contenteditable="true"] {
|
|
177
188
|
user-select: text;
|
|
178
189
|
-webkit-user-select: text;
|
|
179
190
|
}
|
|
@@ -186,13 +197,21 @@ body {
|
|
|
186
197
|
height: 28px;
|
|
187
198
|
background: var(--annot-bg-panel, #1e1e1e);
|
|
188
199
|
border-top: 1px solid var(--annot-border-color, #333);
|
|
189
|
-
|
|
200
|
+
/* Hidden by default for the same reason as `#canvas-container`
|
|
201
|
+
* above — only the image editor mode shows the statusbar, and
|
|
202
|
+
* leaving it visible by default paints the dark bottom strip
|
|
203
|
+
* before JS resolves the route, contributing to the boot flash
|
|
204
|
+
* on gallery loads. `body.editor-mode` restores `display: flex`. */
|
|
205
|
+
display: none;
|
|
190
206
|
align-items: center;
|
|
191
207
|
padding: 0 12px;
|
|
192
208
|
gap: 16px;
|
|
193
209
|
font-size: 12px;
|
|
194
210
|
color: #888;
|
|
195
211
|
}
|
|
212
|
+
body.editor-mode #statusbar {
|
|
213
|
+
display: flex;
|
|
214
|
+
}
|
|
196
215
|
|
|
197
216
|
/* Save toast notification */
|
|
198
217
|
.save-toast {
|
|
@@ -970,6 +989,7 @@ body.editor-mode #editor-sidebar {
|
|
|
970
989
|
overflow: visible;
|
|
971
990
|
}
|
|
972
991
|
body.editor-mode #canvas-container {
|
|
992
|
+
display: block;
|
|
973
993
|
top: 48px; /* below editor-header only */
|
|
974
994
|
left: 48px; /* right of the sidebar */
|
|
975
995
|
}
|