@zumer/snapdom 2.24.15 โ†’ 3.0.0-beta.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 CHANGED
@@ -36,325 +36,300 @@
36
36
 
37
37
  # SnapDOM
38
38
 
39
- **SnapDOM** is a next-generation **DOM Capture Engine**: the fast, modern alternative to **html2canvas**, **dom-to-image**, and **html-to-image**.
40
- It converts any DOM subtree into a self-contained representation that can be exported to SVG, PNG, JPG, WebP, Canvas, Blob, or **any custom format** through plugins. Ultra-fast, modular, extensible, and dependency-free.
39
+ SnapDOM is a browser capture engine for web interfaces. It captures rendered DOM state as a reusable result, with styles, fonts and images included.
41
40
 
42
- > ๐Ÿ“– **[Documentation, guides & live demos โ†’ snapdom.dev](https://snapdom.dev)**
41
+ Export images and canvas with the core. Use plugins for self-contained HTML, page context, maps for visual agents, PDF and recordings. Captures can also feed WebGL textures, visual regression tests and UI transitions. Everything runs in the page, using standard Web APIs, with no core dependencies.
43
42
 
44
- ## Features
43
+ [Documentation and demos](https://snapdom.dev/) ยท [Technical features](FEATURES.md) ยท [Official plugins](packages/plugins/README.md) ยท [็ฎ€ไฝ“ไธญๆ–‡](README_CN.md)
45
44
 
46
- Full DOM capture with embedded styles, pseudo-elements and fonts; export to SVG, PNG, JPG, WebP, `canvas` or Blob. Ultra fast, dependency-free, and 100% based on standard Web APIs.
45
+ This checkout documents **v3**. The migration guide below compares it with 2.24.16, the last v2 release.
47
46
 
48
- ๐Ÿ‘‰ **See the complete technical feature list in [FEATURES.md](FEATURES.md).**
47
+ ## What you can build
49
48
 
50
- ## Website & Live Demos
49
+ | Use | Output | Provided by |
50
+ | --- | --- | --- |
51
+ | Share a card, chart, invoice or dashboard | SVG, PNG, JPG, WebP, canvas or Blob | Core |
52
+ | Reuse a capture in a texture, overlay or transition | Canvas plus capture geometry | Core |
53
+ | Save a page fragment for later display | HTML with captured styles and fonts | `html-export` plugin |
54
+ | Give an agent or a log a view of page content | Text/JSON context, or an image with an element map | `context-export` / `agent-map` plugins |
55
+ | Download a document or record changing content | Image-based PDF, animated GIF or browser-encoded video | `pdf-image` / `gif-export` / `video-export` plugins |
51
56
 
52
- [https://snapdom.dev](https://snapdom.dev)
57
+ Image, HTML and context exports use the captured state. GIF and video plugins record the live element over time.
53
58
 
54
-
55
- ## Quick Start
56
-
57
- **Capture any DOM element to PNG in one line:**
59
+ ## Quick start
58
60
 
59
61
  ```js
60
62
  import { snapdom } from '@zumer/snapdom';
61
63
 
62
- const img = await snapdom.toPng(document.querySelector('#card'));
63
- document.body.appendChild(img);
64
+ const card = document.querySelector('#card');
65
+ const image = await snapdom.toPng(card);
66
+ document.body.appendChild(image);
64
67
  ```
65
68
 
66
- **Reusable capture** (one clone, multiple exports):
69
+ Capture once when you need several outputs:
67
70
 
68
71
  ```js
69
- const result = await snapdom(document.querySelector('#card'));
70
- await result.toPng(); // โ†’ HTMLImageElement
71
- await result.toSvg(); // โ†’ SVG as Image
72
- await result.download({ format: 'jpg', filename: 'card.jpg' });
72
+ const result = await snapdom(card);
73
+
74
+ const image = await result.toPng();
75
+ const canvas = await result.toCanvas();
76
+ const blob = await result.toBlob({ format: 'png' });
77
+ await result.download({ format: 'jpg', filename: 'card' });
73
78
  ```
74
79
 
75
- ---
76
-
77
- ## Table of Contents
78
-
79
- - [Quick Start](#quick-start)
80
- - [Features](#features)
81
- - [Website & Live Demos](#website--live-demos)
82
- - [Installation](#installation)
83
- - [Build Outputs](#build-outputs)
84
- - [Usage](#usage)
85
- - [Documentation](#documentation): full API, Options, Plugins & Cache reference on [snapdom.dev/docs](https://snapdom.dev/docs/)
86
- - [Limitations](#limitations)
87
- - [Performance Benchmarks](#performance-benchmarks)
88
- - [Development](#development)
89
- - [Contributors](#contributors)
90
- - [Sponsors](#sponsors)
91
- - [Show your support](#show-your-support)
92
- - [License](#license)
80
+ The result keeps that capture even if the source element later changes. Call `snapdom(card)` again to capture its new state.
93
81
 
94
82
  ## Installation
95
83
 
96
- ### NPM / Yarn (stable)
84
+ Install the core and, when you need them, the official plugins; use matching major versions:
97
85
 
98
- ```bash
99
- npm i @zumer/snapdom
100
- yarn add @zumer/snapdom
86
+ ```sh
87
+ npm i @zumer/snapdom @zumer/snapdom-plugins
101
88
  ```
102
89
 
103
- ### NPM / Yarn (dev builds)
90
+ Or load it in a browser:
104
91
 
105
- For early access to new features and fixes:
106
-
107
- ```bash
108
- npm i @zumer/snapdom@dev
109
- yarn add @zumer/snapdom@dev
92
+ ```html
93
+ <script src="https://unpkg.com/@zumer/snapdom/dist/snapdom.js"></script>
94
+ <script>
95
+ snapdom.toPng(document.querySelector('#card')).then(image => {
96
+ document.body.appendChild(image);
97
+ });
98
+ </script>
110
99
  ```
111
100
 
112
- โš ๏ธ The `@dev` tag usually includes improvements before they reach production, but may be less stable.
101
+ As an ES module from a CDN:
113
102
 
103
+ ```js
104
+ import { snapdom } from 'https://esm.sh/@zumer/snapdom';
105
+ import { htmlExport } from 'https://esm.sh/@zumer/snapdom-plugins/html-export';
106
+ ```
114
107
 
115
- ### CDN (stable)
108
+ `https://unpkg.com/@zumer/snapdom/dist/snapdom.mjs` serves the same module. Pin package versions in production.
116
109
 
117
- ```html
118
- <!-- Minified build -->
119
- <script src="https://unpkg.com/@zumer/snapdom/dist/snapdom.js"></script>
110
+ To run the docs site against a local build of this checkout instead:
120
111
 
121
- <!-- Minified ES Module build -->
122
- <script type="module">
123
- import { snapdom } from "https://unpkg.com/@zumer/snapdom/dist/snapdom.mjs";
124
- </script>
112
+ ```sh
113
+ npm install
114
+ npm run compile
115
+ npm run site
125
116
  ```
126
117
 
127
- ### CDN (dev builds)
118
+ The local site runs the local build. The public site's demos load the published package.
128
119
 
129
- ```html
130
- <!-- Minified build (dev) -->
131
- <script src="https://unpkg.com/@zumer/snapdom@dev/dist/snapdom.js"></script>
120
+ ### Build outputs
132
121
 
133
- <!-- Minified ES Module build (dev) -->
134
- <script type="module">
135
- import { snapdom } from "https://unpkg.com/@zumer/snapdom@dev/dist/snapdom.mjs";
136
- </script>
137
- ```
122
+ | File | Use |
123
+ | --- | --- |
124
+ | `dist/snapdom.mjs` | ES module for imports and bundlers |
125
+ | `dist/snapdom.js` | Script tag exposing `window.snapdom` |
126
+ | `types/snapdom.d.ts` | TypeScript declarations |
138
127
 
139
- ## Build Outputs
128
+ There is no CommonJS build. `@zumer/snapdom/plugins` and the package root share the same runtime and plugin registry.
140
129
 
141
- | Variant | File | Use case |
142
- |---------|------|----------|
143
- | **ESM** (tree-shakeable) | `dist/snapdom.mjs` | Bundlers (Vite, webpack), `import` |
144
- | **IIFE** (global) | `dist/snapdom.js` | Script tag, legacy `require` |
130
+ ## Usage
145
131
 
146
- **Bundler (npm):**
147
- ```js
148
- import { snapdom } from '@zumer/snapdom'; // โ†’ dist/snapdom.mjs
149
- ```
132
+ ### Choose an output
150
133
 
151
- **Script tag (CDN):**
152
- ```html
153
- <script src="https://unpkg.com/@zumer/snapdom/dist/snapdom.js"></script>
154
- <script> snapdom.toPng(document.body).then(img => document.body.appendChild(img)); </script>
155
- ```
134
+ | Result method | Returns |
135
+ | --- | --- |
136
+ | `toPng()`, `toJpg()`, `toWebp()` | An `HTMLImageElement` |
137
+ | `toSvg()` | An SVG-backed `HTMLImageElement` |
138
+ | `toCanvas()` | An `HTMLCanvasElement` |
139
+ | `toBlob()` | An SVG `Blob` unless a format was explicitly set on the capture or export |
140
+ | `toRaw()` / `url` | The capture's SVG data URL |
141
+ | `download()` | Downloads the chosen format |
142
+ | `to(name, options?)` | Runs a core or plugin exporter by name |
156
143
 
157
- **Subpath imports** (lighter bundle if you only need one):
158
- ```js
159
- import { preCache } from '@zumer/snapdom/preCache';
160
- ```
144
+ One-step shortcuts such as `snapdom.toPng(element, options)` capture and export in one call. Results also have `toJpeg()` as an alias for `toJpg()`. `toImg()` remains available; prefer `toSvg()` for an SVG image.
145
+
146
+ ### Set size and content
161
147
 
162
- **Official plugins** live in their own package:
163
- ```bash
164
- npm install @zumer/snapdom-plugins
165
- ```
166
148
  ```js
167
- import { filter } from '@zumer/snapdom-plugins/filter';
149
+ const result = await snapdom(card, {
150
+ width: 800,
151
+ dpr: 1,
152
+ backgroundColor: '#ffffff',
153
+ exclude: '.capture-ignore',
154
+ excludeMode: 'remove'
155
+ });
168
156
  ```
169
157
 
158
+ `width` and `height` define output size. If only one is set, the aspect ratio is preserved. `scale` applies when neither is set, and `dpr` multiplies the pixel dimensions.
170
159
 
171
- ## Usage
160
+ | Common option | Default | Purpose |
161
+ | --- | --- | --- |
162
+ | `scale` / `dpr` | `1` / device pixel ratio | Output resolution |
163
+ | `width` / `height` | Unset | Output dimensions |
164
+ | `embedFonts` | `'auto'` | Embed the web fonts the capture uses |
165
+ | `backgroundColor` | Transparent; white for JPG/WebP | Output background |
166
+ | `exclude` | None | Selectors or predicates; `true` means exclude |
167
+ | `excludeMode` | `'hide'` | Keep an invisible spacer, or use `'remove'` |
168
+ | `filter` | None | Predicate; `true` keeps a node and `false` filters it out |
169
+ | `filterMode` | `'hide'` | Independent layout mode for nodes rejected by `filter` |
170
+ | `clip` | Unset | Capture the viewport or a page-coordinate rectangle |
171
+ | `captureSelection` | `false` | Include the user's text selection |
172
+ | `canvas` | Unset | Reuse an existing canvas |
173
+ | `invalidate` | `false` | Refresh after changes such as programmatic CSSOM edits |
172
174
 
173
- | Pattern | When to use |
174
- |---------|-------------|
175
- | **Reusable** `snapdom(el)` | One clone โ†’ many exports (PNG + JPG + download). |
176
- | **Shortcuts** `snapdom.toPng(el)` | Single export, less code. |
175
+ [All options](https://snapdom.dev/docs/options/) include shadows, transforms, fonts, CORS, fallbacks and layout reconciliation.
177
176
 
178
- ### Reusable capture
177
+ ### Export HTML or structured context
179
178
 
180
- Capture once, export many times (no re-clone):
179
+ Official plugins are published separately as `@zumer/snapdom-plugins` and must match the core major version; they declare a peer dependency on a v3 core. Their sources live in `packages/plugins/` in this checkout.
181
180
 
182
181
  ```js
183
- const el = document.querySelector('#target');
184
- const result = await snapdom(el);
182
+ import { htmlExport, contextExport } from '@zumer/snapdom-plugins';
183
+
184
+ const result = await snapdom(card, {
185
+ plugins: [htmlExport(), contextExport({ format: 'json' })]
186
+ });
185
187
 
186
- const img = await result.toPng();
187
- document.body.appendChild(img);
188
- await result.download({ format: 'jpg', filename: 'my-capture.jpg' });
188
+ const html = await result.toHtml();
189
+ const context = await result.toContext();
189
190
  ```
190
191
 
191
- ### One-step shortcuts
192
+ The same plugin system supports overlays, redaction and custom exporters. Local plugins override global plugins by name. See the [official plugin reference](packages/plugins/README.md) and [plugin specification](PLUGIN_SPEC.md).
192
193
 
193
- Direct export when you need a single format:
194
+ ### Capture HTML strings
194
195
 
195
196
  ```js
196
- const png = await snapdom.toPng(el);
197
- const blob = await snapdom.toBlob(el);
198
- document.body.appendChild(png);
197
+ const result = await snapdom.fromString('<article>Hello</article>');
198
+ const image = await result.toPng();
199
199
  ```
200
200
 
201
- ## CORS & External Resources
201
+ `fromString()` mounts the markup offscreen and removes it after capture. The string is parsed and activated like markup you wrote yourself: inline handlers such as `<img onerror>` run in the caller's origin, and can keep running after the mount is removed. Sanitize untrusted HTML first, with DOMPurify or equivalent.
202
202
 
203
- When capturing elements that reference **external stylesheets** (e.g., Google Fonts, Font Awesome, or any CDNโ€‘hosted CSS), you **must** ensure that the resources are served with proper CORS headers. Otherwise, the captured image may lack the expected fonts or icons, even though they render correctly in the browser.
203
+ ### Feed a WebGL texture
204
204
 
205
- ### Why is this needed?
205
+ ```js
206
+ const canvas = document.createElement('canvas');
207
+ const texture = new THREE.CanvasTexture(canvas);
208
+ texture.colorSpace = THREE.SRGBColorSpace;
209
+
210
+ async function refresh(element) {
211
+ await snapdom.toCanvas(element, { canvas, scale: 1, dpr: 1 });
212
+ texture.needsUpdate = true;
213
+ }
214
+ ```
206
215
 
207
- - Browsers block JavaScript (including SnapDOM) from reading the binary data of crossโ€‘origin fonts or images unless the server explicitly allows it via `Access-Control-Allow-Origin`.
208
- - SnapDOM relies on Canvas, which enforces strict CORS policies. The browser's rendering engine is more permissive for onโ€‘screen display.
216
+ `result.meta` contains the capture geometry needed to place an exported image over the source interface. The [labs](https://snapdom.dev/labs.html) show textures, mirrors and transitions.
209
217
 
210
- ### How to fix it
218
+ ## What's new in v3
211
219
 
212
- Add the `crossorigin="anonymous"` attribute to the `<link>` tag when loading external stylesheets:
220
+ - Eligible unchanged captures reuse the first result. Safe local changes rebuild only affected subtrees; other changes use a full capture.
221
+ - Web fonts embed automatically when used. System-font captures skip that work.
222
+ - The style pass avoids redundant reads, and per-capture state is isolated for concurrent captures.
223
+ - Safari image decoding and drawing retain their browser-specific handling.
224
+ - `snapdom.preCapture()` can prepare captures on user intent. It learns a control when a capture starts during its press/click event, then prepares that capture on later hover or focus.
213
225
 
214
- ```html
215
- <link
216
- rel="stylesheet"
217
- href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"
218
- crossorigin="anonymous"
219
- />
226
+ ```js
227
+ snapdom.preCapture();
228
+ button.onclick = () => snapdom.toPng(card);
220
229
  ```
221
230
 
222
- > **Note**: If you are hosting the fonts or assets **on the same origin** as your page (e.g., using a local server like `http://localhost`), you **do not** need to add `crossorigin` โ€“ the browser treats them as sameโ€‘origin and allows full access.
231
+ Image compression and resource caching remain automatic. They do not make rasterization or image encoding free. See [performance measurements](BENCHMARKS.md) and the [cache guide](https://snapdom.dev/docs/cache/).
223
232
 
224
- ## Documentation
233
+ SnapDOM has two rendering engines: **SVG**, the default, and **html-in-canvas**, which paints the same captured clone through the browser's native canvas API. Select the second with `engine: 'html-in-canvas'`.
225
234
 
226
- The full reference lives on **[snapdom.dev/docs](https://snapdom.dev/docs/)**, kept there so it stays in sync and searchable:
227
-
228
- - **[API reference](https://snapdom.dev/docs/api/)**: the `snapdom()` reusable object, shortcut methods, and exporter-specific options.
229
- - **[Options](https://snapdom.dev/docs/options/)**: every capture option (`scale`, `dpr`, `embedFonts`, `useProxy`, `exclude`/`filter`, `compress`, `outerTransforms`, `outerShadows`, `cache`โ€ฆ) explained with examples.
230
- - **[Plugins](https://snapdom.dev/docs/plugins/)**: build, register and ship custom plugins and export formats. Browse community plugins on the [plugins page](https://snapdom.dev/plugins.html).
231
- - **[Cache & preCache](https://snapdom.dev/docs/cache/)**: control caching between captures and preload resources.
232
-
233
- Popular guides: **[convert HTML to PNG](https://snapdom.dev/how-to/html-to-png/)** ยท **[tile a full-page capture](https://snapdom.dev/blog/huge-page-mosaic/)** ยท **[run SnapDOM from Playwright or Puppeteer](https://snapdom.dev/blog/dom-capture-boundaries/)** ยท **[how SnapDOM captures the DOM](https://snapdom.dev/blog/painting-without-canvas/)**
234
-
235
- ### API at a glance
236
-
237
- `snapdom(el, options?)` returns a reusable object (`toPng`, `toSvg`, `toCanvas`, `toBlob`, `toJpg`, `toWebp`, `download`, `url`). For single exports, use the shortcuts:
238
-
239
- | Method | Description |
240
- | ------------------------------ | --------------------------------- |
241
- | `snapdom.toSvg(el, options?)` | Returns an SVG `HTMLImageElement` |
242
- | `snapdom.toCanvas(el, options?)` | Returns a `Canvas` |
243
- | `snapdom.toBlob(el, options?)` | Returns an SVG or raster `Blob` |
244
- | `snapdom.toPng(el, options?)` | Returns a PNG image |
245
- | `snapdom.toJpg(el, options?)` | Returns a JPG image |
246
- | `snapdom.toWebp(el, options?)` | Returns a WebP image |
247
- | `snapdom.download(el, options?)` | Triggers a download |
248
-
249
- ### Options at a glance
250
-
251
- All options are optional and can be passed to `snapdom(el, options)` or any shortcut method.
252
-
253
- | Option | Type | Default | Description |
254
- | ------ | ---- | ------- | ----------- |
255
- | `scale` | `number` | `1` | Output scale multiplier |
256
- | `dpr` | `number` | `devicePixelRatio` | Pixel density of the rasterized output |
257
- | `width` / `height` | `number` | `null` | Target output size (keeps aspect ratio if only one is set) |
258
- | `backgroundColor` | `string` | `null` (`#ffffff` for JPEG/WebP) | Background fill |
259
- | `quality` | `number` | `0.92` | JPEG/WebP quality (0โ€“1) |
260
- | `format` | `'png' \| 'jpeg' \| 'webp' \| 'svg'` | `'png'` | Format for `download()` |
261
- | `type` | `string` | `'svg'` | Blob type for `toBlob()` (`'png'`, `'jpeg'`โ€ฆ) |
262
- | `filename` | `string` | `'snapDOM'` | Download filename |
263
- | `embedFonts` | `boolean` | `false` | Inline `@font-face` so text renders with your real fonts |
264
- | `iconFonts` | `string \| RegExp \| array` | `[]` | Icon font families (always embedded) |
265
- | `localFonts` | `array` | `[]` | Explicit fonts: `{ family, src, weight?, style? }` |
266
- | `excludeFonts` | `object` | none | Skip fonts by family / domain / subset |
267
- | `exclude` | `string[]` | `[]` | CSS selectors to leave out of the capture |
268
- | `filter` | `(el) => boolean` | `null` | Keep-predicate (return `false` to drop a node) |
269
- | `excludeMode` / `filterMode` | `'hide' \| 'remove'` | `'hide'` | How excluded nodes are handled |
270
- | `clip` | `'viewport' \| {x, y, width, height}` | `null` | Capture only a region; offscreen content is pruned |
271
- | `compress` | `boolean` | `true` | Downsample inlined images to their visible resolution |
272
- | `useProxy` | `string` | `''` | CORS proxy prefix for cross-origin images |
273
- | `fallbackURL` | `string \| fn` | none | Fallback image for broken `<img>` |
274
- | `cache` | `'soft' \| 'auto' \| 'full' \| 'disabled'` | `'soft'` | Cache policy between captures |
275
- | `outerTransforms` | `boolean` | `true` | Keep root translate/rotate in the output |
276
- | `outerShadows` | `boolean` | `false` | Expand bounds to include root shadows/blur/outline |
277
- | `fast` | `boolean` | `true` | Skip idle delays for faster capture |
278
- | `reconcile` | `boolean` | `false` | Measure the clone against the live DOM and pin any diverging box to its real size. Fixes rare text re-wrap/layout drift at the cost of roughly doubling capture time. snapdom warns once (`console.warn`) if it detects a capture that could benefit from it |
279
- | `burst` | `boolean` | `false` | Memoizes repeated captures of this element via a scoped MutationObserver, so an unchanged repeat skips the pipeline entirely. Without it, snapdom warns once if the same element is captured 3+ times within 2s |
280
- | `invalidate` | `boolean` | `false` | With `burst: true`, forces a fresh capture for changes automatic tracking can't see (canvas draws, programmatic CSSOM edits) |
281
- | `plugins` | `array` | none | Per-capture plugins (override globals by name) |
282
-
283
- ๐Ÿ“– **[Full API & every option, explained with examples โ†’ snapdom.dev/docs](https://snapdom.dev/docs/)**
235
+ The second engine is experimental: it still needs a compatible browser with its canvas drawing flag enabled, plus a build compiled with `SNAPDOM_CANVAS_ENGINE=1`. The default build includes SVG only. Unsupported captures fall back to SVG. A successful native capture produces a bitmap, so its URL and `toRaw()` return PNG, not serialized SVG. See [ARCHITECTURE.md](ARCHITECTURE.md) for details.
284
236
 
285
- ## Limitations
237
+ ## Migrating from v2
286
238
 
287
- * External images should be CORS-accessible (use `useProxy` option for handling CORS denied)
288
- * When WebP format is used on Safari, it will fallback to PNG rendering (verified on Safari 26.5: `canvas.toDataURL('image/webp')` returns PNG). `download()` keeps the `.webp` filename, so the saved file carries PNG bytes.
289
- * `@font-face` CSS rule is well supported. Fonts registered from JavaScript with `FontFace()` are *not* embedded automatically: list them in the `localFonts` option (`{ family, src }`), or use the workaround in [`#43`](https://github.com/zumerlab/snapdom/issues/43)
290
- * **Safari**: captures with `embedFonts` or background/mask images run slower due to [WebKit #219770](https://bugs.webkit.org/show_bug.cgi?id=219770) (font decode timing). SnapDOM waits for the fonts the element actually uses and verifies the first canvas draw, so there is nothing to configure.
291
- * **Custom scrollbar styles** (`::-webkit-scrollbar`): Applied only when the element has *not* been scrolled. When scrolled, the viewport content is captured without the scrollbar.
239
+ The main capture pattern remains `snapdom(element, options)`. This guide compares v3 with **v2.24.16**; review these changes before upgrading:
292
240
 
241
+ | In v2 | In v3 | What to change |
242
+ | --- | --- | --- |
243
+ | Web fonts were opt-in | `embedFonts: 'auto'` | Usually nothing; use `false` only if you want to omit them |
244
+ | Raster width/height could be multiplied by `scale` | Width/height win over scale | Pass the final size: `width: 400` instead of `width: 200, scale: 2` |
245
+ | `burst` opted into repeat memoization | Eligible captures memoize automatically; `burst` is no longer documented or supported | Remove `burst` (the engine still reads it for internal use, do not rely on it) and use `invalidate: true` for one fresh capture after unobservable changes such as `sheet.insertRule()` |
246
+ | `preCache` prepared resources | Removed; `preCapture()` learns capture intent | Remove `preCache`; `preCapture()` is not a drop-in rename |
247
+ | `fast` selected an optimization path | Removed | Delete the option |
248
+ | `filter` / `filterMode` and `exclude` / `excludeMode` could be used together | Both controls and their independent modes remain supported; `exclude` also accepts predicates | Keep existing rules and modes; use the additional predicate form only when useful |
249
+ | `cache: 'auto'` or `'full'` | Both map to `'soft'` | Usually omit it; `'disabled'` / `false` is for debugging |
250
+ | `compress` controlled embedded image downsampling | Image optimization is automatic; `compress` is no longer documented or supported | Remove `compress`; the engine still reads it for internal use, do not rely on it |
251
+ | `resolvePicturePlaceholders` / `pictureResolver` configured lazy-image preparation | Responsive/lazy image resolution happens on the clone; these options are no longer documented or supported | Remove the options and handle custom loading/timeouts in your app before capture; the engine still reads `resolvePicturePlaceholders` for internal use, do not rely on it |
252
+ | Some visible input values were redacted | Core masks passwords only | Add `redactInputs()` for other fields |
253
+ | `afterExport` returns became the next hook's payload, not the caller's result | Returns are ignored; hooks receive the same export payload | Stop chaining through return values; use `defineExports` to produce a different output |
254
+ | TypeScript exported `PluginExportFacade` | The named type is removed; `ctx.exports` still provides core exporters | Infer it in `defineExports`, or use `NonNullable<CaptureContext['exports']>` |
293
255
 
294
- ## Performance Benchmarks
256
+ ### Use filter and exclude together
295
257
 
296
- **Setup.** Vitest benchmarks on Chromium, repo tests. Hardware may affect results.
297
- Values are **average capture time (ms)** โ†’ lower is better.
258
+ `filter` and `exclude` are independent controls, as in v2. `filter(node)` returns true to keep a node and false to filter it out; `filterMode` controls how a filtered node affects layout. `exclude` specifies additional omissions using selectors or predicates; an exclusion predicate returns true to omit the node. `excludeMode` controls those omissions. You can keep both controls and different modes in the same capture:
298
259
 
299
- ### Simple elements
260
+ ```js
261
+ // Works in v2 and v3: hide private fields, remove the toolbar.
262
+ await snapdom(card, {
263
+ filter: node => !node.matches('[data-private]'),
264
+ filterMode: 'hide',
265
+ exclude: ['.toolbar'],
266
+ excludeMode: 'remove'
267
+ });
268
+ ```
300
269
 
301
- | Scenario | SnapDOM current | SnapDOM v1.9.9 | html2canvas | html-to-image |
302
- | ------------------------ | --------------- | -------------- | ----------- | ------------- |
303
- | Small (200ร—100) | **0.5 ms** | 0.8 ms | 67.7 ms | 3.1 ms |
304
- | Modal (400ร—300) | **0.5 ms** | 0.8 ms | 75.5 ms | 3.6 ms |
305
- | Page View (1200ร—800) | **0.5 ms** | 0.8 ms | 114.2 ms | 3.3 ms |
306
- | Large Scroll (2000ร—1500) | **0.5 ms** | 0.8 ms | 186.3 ms | 3.2 ms |
307
- | Very Large (4000ร—2000) | **0.5 ms** | 0.9 ms | 425.9 ms | 3.3 ms |
270
+ `'hide'` keeps an invisible spacer; `'remove'` drops the node and allows reflow. Both omit its content. V3 additionally lets `exclude` mix selectors and predicates, such as `exclude: ['.toolbar', node => node.dataset.export === 'omit']`; any matching rule excludes the node. This is optional and does not replace `filter` or merge the two modes. The separate CSS-effect plugin named `filter` is also available.
308
271
 
272
+ Both modes default to `'hide'`. Per node, `data-capture="exclude"` is checked first, then `exclude`, then `filter`. The first omission decides the mode and stops evaluation for that node: if it matches `exclude`, `excludeMode` wins even when `filter` would reject it with a different mode. `filter` uses the v2 truthiness rule: any falsy return filters the node out.
309
273
 
310
- ### Complex elements
274
+ ### Callbacks that read changing application state
311
275
 
312
- | Scenario | SnapDOM current | SnapDOM v1.9.9 | html2canvas | html-to-image |
313
- | ------------------------ | --------------- | -------------- | ----------- | ------------- |
314
- | Small (200ร—100) | **1.6 ms** | 3.3 ms | 68.0 ms | 14.3 ms |
315
- | Modal (400ร—300) | **2.9 ms** | 6.8 ms | 87.5 ms | 34.8 ms |
316
- | Page View (1200ร—800) | **17.5 ms** | 50.2 ms | 178.0 ms | 429.0 ms |
317
- | Large Scroll (2000ร—1500) | **54.0 ms** | 201.8 ms | 735.2 ms | 984.2 ms |
318
- | Very Large (4000ร—2000) | **171.4 ms** | 453.7 ms | 1,800.4 ms | 2,611.9 ms |
276
+ Captures with function-valued `filter`, `exclude`, `excludeStyleProps` or `fallbackURL` run fresh so applicable callbacks can read current application state on each new capture. They do not reuse an unchanged capture or an earlier callback's style/fallback decision. You do not need `invalidate` just because a callback's closure changed. That freshness has a cost: a function-valued `filter`, `exclude`, `excludeStyleProps` or `fallbackURL` turns off memoization and differential recapture for the capture, so a polling loop with a predicate pays a full capture on every tick. When the rule can be written as a selector, pass the selector and keep the reuse:
319
277
 
278
+ ```js
279
+ let privateMode = false;
280
+ const options = {
281
+ exclude: node => privateMode && node.matches('[data-private]'),
282
+ excludeMode: 'remove'
283
+ };
284
+ const before = await snapdom(card, options);
285
+ privateMode = true;
286
+ const after = await snapdom(card, options); // evaluates the current policy
287
+ ```
320
288
 
321
- ### Run the benchmarks
289
+ `before` still contains its original captured state; exporting it again does not apply a new policy. Use a new capture such as `after`. Keep exclusion and style predicates synchronous and boolean-returning. `invalidate: true` remains necessary after unobservable changes such as direct CSSOM edits.
322
290
 
323
- ```sh
324
- git clone https://github.com/zumerlab/snapdom.git
325
- cd snapdom
326
- npm install
327
- npm run test:benchmark
328
- ```
291
+ Capture-affecting plugins suspend memoization unless they declare `pure: true`. Declare it only for deterministic hooks; timestamps and callbacks reading external state must run again. See the [v3 plugin contract](PLUGIN_SPEC.md).
292
+
293
+ ## Limitations
294
+
295
+ - SnapDOM needs a browser DOM. A server-side Node.js process needs a browser environment to run it.
296
+ - Cross-origin images, fonts and stylesheets need readable resources or an appropriate proxy. `crossorigin` does not grant access unless the server also allows it. Cross-origin iframes use placeholders.
297
+ - SVG output includes HTML inside `<foreignObject>`. It is suitable for browsers; support varies in other SVG viewers and document tools.
298
+ - Output depends on browser rendering and canvas limits. Safari may fall back to PNG when WebP encoding is unavailable.
299
+ - Canvas, video and other changing surfaces are captured fresh. JavaScript CSSOM edits are not observable automatically; use `invalidate: true` after them.
300
+ - Core captures visible input values. Semantic plugins redact sensitive field values in their text/map output, but their attached image needs `redactInputs` or `exclude` if you want those pixels hidden too.
301
+
302
+ [Technical features and browser behavior](FEATURES.md) covers the details.
303
+
304
+ ## Performance benchmarks
305
+
306
+ [Recorded measurements](BENCHMARKS.md) separate first captures, repeat captures and image-heavy scenes. The [live comparison](https://snapdom.dev/compare/live/) runs in your browser and identifies the package version it loads.
329
307
 
308
+ For a useful comparison, use the same scene, output format, scale and DPR. Compare the images as well as the times.
309
+
310
+ ## Documentation
311
+
312
+ - [API](https://snapdom.dev/docs/api/) and [options](https://snapdom.dev/docs/options/)
313
+ - [Framework guides](https://snapdom.dev/guides/) and [how-to examples](https://snapdom.dev/how-to/)
314
+ - [Official plugins](packages/plugins/README.md), [plugin specification](PLUGIN_SPEC.md) and [contributing plugins](CONTRIBUTING_PLUGINS.md)
315
+ - [Architecture](ARCHITECTURE.md) and [technical features](FEATURES.md)
330
316
 
331
317
  ## Development
332
318
 
333
- **Source layout:**
334
- - `src/api/` โ€“ Public API (`snapdom`, `preCache`)
335
- - `src/core/` โ€“ Capture pipeline, clone, prepare, plugins
336
- - `src/modules/` โ€“ Images, fonts, pseudo-elements, backgrounds, SVG
337
- - `src/exporters/` โ€“ toPng, toSvg, toBlob, etc.
338
- - `dist/` โ€“ Build output (`snapdom.js`, `snapdom.mjs`, `preCache.mjs`, `plugins.mjs`)
319
+ From this checkout:
339
320
 
340
- **Build:**
341
321
  ```sh
342
- git clone https://github.com/zumerlab/snapdom.git
343
- cd snapdom
344
- git checkout dev
345
322
  npm install
323
+ npx playwright install
346
324
  npm run compile
325
+ npm run lint
326
+ npm run test:types
327
+ npm run test:bundle
328
+ BROWSER=all npx vitest run __tests__ --browser.headless
329
+ npm run test:pack
347
330
  ```
348
331
 
349
- **Test:**
350
- ```sh
351
- npx playwright install # Required for browser tests
352
- npm test
353
- npm run test:benchmark
354
- ```
355
-
356
- For detailed guidelines, see [CONTRIBUTING](https://github.com/zumerlab/snapdom/blob/main/CONTRIBUTING.md).
357
-
332
+ `npm run site` serves the docs with the local build. `npm test` checks lint without changing files; use `npm run lint:fix` to apply fixes. See [ARCHITECTURE.md](ARCHITECTURE.md) for implementation notes.
358
333
 
359
334
  ## Contributors
360
335
 
@@ -399,7 +374,7 @@ If you'd like to support this project too, you can [become a sponsor](https://gi
399
374
 
400
375
  ## Show your support
401
376
 
402
- If SnapDOM saved you time, a โญ on GitHub helps other developers find it. That's the whole ask.
377
+ If SnapDOM saved you time, a star on GitHub helps other developers find it.
403
378
 
404
379
  Shipping something built with SnapDOM? Add the badge to your README:
405
380
 
@@ -411,22 +386,22 @@ Shipping something built with SnapDOM? Add the badge to your README:
411
386
 
412
387
  ### Projects using SnapDOM
413
388
 
414
- SnapDOM runs in production across 250+ public repositories ([GitHub dependents graph](https://github.com/zumerlab/snapdom/network/dependents)). A few notable ones, each verified from its own `package.json`:
415
-
416
- - [LobeHub](https://github.com/lobehub/lobehub): platform for operating AI agents
417
- - [Trilium Notes](https://github.com/TriliumNext/Trilium): hierarchical personal knowledge base
418
- - [Sealos](https://github.com/labring/sealos): AI-native cloud operating system
419
- - [Tencent tmagic-editor](https://github.com/Tencent/tmagic-editor): low-code page editor
420
- - [Playroom](https://github.com/seek-oss/playroom): JSX design tool by SEEK
421
- - [GPT-Vis](https://github.com/antvis/GPT-Vis): AI-friendly data viz by Ant Group's AntV
422
- - [Rabby Wallet](https://github.com/RabbyHub/Rabby): browser wallet for EVM chains
423
- - [uMap](https://github.com/umap-project/umap): OpenStreetMap map builder
424
- - [ListenBrainz](https://github.com/metabrainz/listenbrainz-server): music tracker by MetaBrainz
425
- - [Mind Elixir](https://github.com/SSShooter/mind-elixir-core): mind-map core; recommends SnapDOM for image export
426
- - [Kong UI Components](https://github.com/Kong/public-ui-components): Kong's dashboard renderer exports PDFs with SnapDOM
427
- - [SnapDIFF](https://zumerlab.com/snapdiff/): in-browser visual regression testing *(by Zumerlab)*
428
-
429
- See the full gallery at **[snapdom.dev/made-with](https://snapdom.dev/made-with/)**. Shipping SnapDOM? [Open a PR](https://github.com/zumerlab/snapdom/pulls) to add your project. Real, verifiable projects only.
389
+ Projects using SnapDOM include:
390
+
391
+ - [LobeHub](https://github.com/lobehub/lobehub) โ€” platform for operating AI agents
392
+ - [Hugging Face Chat UI](https://github.com/huggingface/chat-ui) โ€” HuggingChat interface with artifact preview captures
393
+ - [Sealos](https://github.com/labring/sealos) โ€” AI-native cloud operating system
394
+ - [Tencent tmagic-editor](https://github.com/Tencent/tmagic-editor) โ€” low-code page editor
395
+ - [Playroom](https://github.com/seek-oss/playroom) โ€” JSX design tool by SEEK
396
+ - [GPT-Vis](https://github.com/antvis/GPT-Vis) โ€” AI-friendly data viz by Ant Group's AntV
397
+ - [Rabby Wallet](https://github.com/RabbyHub/Rabby) โ€” browser wallet for EVM chains
398
+ - [uMap](https://github.com/umap-project/umap) โ€” OpenStreetMap map builder
399
+ - [ListenBrainz](https://github.com/metabrainz/listenbrainz-server) โ€” music tracker by MetaBrainz
400
+ - [Mind Elixir](https://github.com/SSShooter/mind-elixir-core) โ€” mind-map core; recommends SnapDOM for image export
401
+ - [Kong UI Components](https://github.com/Kong/public-ui-components) โ€” Kong's dashboard renderer exports PDFs with SnapDOM
402
+ - [SnapDIFF](https://zumerlab.com/snapdiff/) โ€” in-browser visual regression testing *(by Zumerlab)*
403
+
404
+ See the full gallery at **[snapdom.dev/made-with](https://snapdom.dev/made-with/)**. Shipping SnapDOM? [Open a PR](https://github.com/zumerlab/snapdom/pulls) to add your project โ€” real, verifiable projects only.
430
405
 
431
406
  ## License
432
407