@onerjs/snapdom 2.7.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/LICENSE +21 -0
- package/README.md +969 -0
- package/dist/plugins.mjs +7 -0
- package/dist/preCache.mjs +10 -0
- package/dist/snapdom.js +14 -0
- package/dist/snapdom.mjs +14 -0
- package/package.json +86 -0
- package/types/snapdom.d.ts +375 -0
package/README.md
ADDED
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="http://zumerlab.github.io/snapdom">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/zumerlab/snapdom/main/docs/assets/newhero.png" width="80%">
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@zumer/snapdom">
|
|
9
|
+
<img alt="NPM version" src="https://img.shields.io/npm/v/@zumer/snapdom?style=flat-square&label=Version">
|
|
10
|
+
</a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/@zumer/snapdom">
|
|
12
|
+
<img alt="NPM weekly downloads" src="https://img.shields.io/npm/dw/@zumer/snapdom?style=flat-square&label=Downloads">
|
|
13
|
+
</a>
|
|
14
|
+
<a href="https://github.com/zumerlab/snapdom/graphs/contributors">
|
|
15
|
+
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/zumerlab/snapdom?style=flat-square&label=Contributors">
|
|
16
|
+
</a>
|
|
17
|
+
<a href="https://github.com/zumerlab/snapdom/stargazers">
|
|
18
|
+
<img alt="GitHub stars" src="https://img.shields.io/github/stars/zumerlab/snapdom?style=flat-square&label=Stars">
|
|
19
|
+
</a>
|
|
20
|
+
<a href="https://github.com/zumerlab/snapdom/network/members">
|
|
21
|
+
<img alt="GitHub forks" src="https://img.shields.io/github/forks/zumerlab/snapdom?style=flat-square&label=Forks">
|
|
22
|
+
</a>
|
|
23
|
+
<a href="https://github.com/sponsors/tinchox5">
|
|
24
|
+
<img alt="Sponsor tinchox5" src="https://img.shields.io/github/sponsors/tinchox5?style=flat-square&label=Sponsor">
|
|
25
|
+
</a>
|
|
26
|
+
|
|
27
|
+
<a href="https://github.com/zumerlab/snapdom/blob/main/LICENSE">
|
|
28
|
+
<img alt="License" src="https://img.shields.io/github/license/zumerlab/snapdom?style=flat-square">
|
|
29
|
+
</a>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
<p align="center">English | <a href="README_CN.md">简体中文</a></p>
|
|
33
|
+
|
|
34
|
+
# SnapDOM
|
|
35
|
+
|
|
36
|
+
**SnapDOM** is a next-generation **DOM Capture Engine** — ultra-fast, modular, and extensible.
|
|
37
|
+
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.
|
|
38
|
+
|
|
39
|
+
* Full DOM capture
|
|
40
|
+
* Embedded styles, pseudo-elements, and fonts
|
|
41
|
+
* Export to SVG, PNG, JPG, WebP, `canvas`, or Blob
|
|
42
|
+
* ⚡ Ultra fast, no dependencies
|
|
43
|
+
* 100% based on standard Web APIs
|
|
44
|
+
* Support same-origin `ìframe`
|
|
45
|
+
* Support CSS counter() and CSS counters()
|
|
46
|
+
* Support `...` line-clamp
|
|
47
|
+
|
|
48
|
+
## Demo
|
|
49
|
+
|
|
50
|
+
[https://snapdom.dev](https://snapdom.dev)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
**Capture any DOM element to PNG in one line:**
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
import { snapdom } from '@zumer/snapdom';
|
|
59
|
+
|
|
60
|
+
const img = await snapdom.toPng(document.querySelector('#card'));
|
|
61
|
+
document.body.appendChild(img);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Reusable capture** (one clone, multiple exports):
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
const result = await snapdom(document.querySelector('#card'));
|
|
68
|
+
await result.toPng(); // → HTMLImageElement
|
|
69
|
+
await result.toSvg(); // → SVG as Image
|
|
70
|
+
await result.download({ format: 'jpg', filename: 'card.jpg' });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Capture Flow
|
|
76
|
+
|
|
77
|
+
SnapDOM transforms your DOM element through these stages:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
DOM Element
|
|
81
|
+
↓
|
|
82
|
+
Clone
|
|
83
|
+
↓
|
|
84
|
+
Styles & Pseudo
|
|
85
|
+
↓
|
|
86
|
+
Images & Backgrounds
|
|
87
|
+
↓
|
|
88
|
+
Fonts
|
|
89
|
+
↓
|
|
90
|
+
SVG foreignObject
|
|
91
|
+
↓
|
|
92
|
+
data:image/svg+xml
|
|
93
|
+
↓
|
|
94
|
+
toPng / toSvg / toBlob / download
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
| Stage | What happens |
|
|
98
|
+
|-------|--------------|
|
|
99
|
+
| **Clone** | Deep clone with styles, Shadow DOM, iframes. Exclude/filter nodes. |
|
|
100
|
+
| **Styles & Pseudo** | Inline `::before`/`::after` as elements, resolve `counter()`/`counters()`. |
|
|
101
|
+
| **Images & Backgrounds** | Fetch and inline external images/backgrounds as data URLs. |
|
|
102
|
+
| **Fonts** | Embed `@font-face` (optional) and icon fonts. |
|
|
103
|
+
| **SVG** | Wrap clone in `<foreignObject>`, serialize to `data:image/svg+xml`. |
|
|
104
|
+
| **Export** | Convert SVG to PNG/JPG/WebP/Blob or trigger download. |
|
|
105
|
+
|
|
106
|
+
Plugin hooks: `beforeSnap` → `beforeClone` → `afterClone` → `beforeRender` → `afterRender` → `beforeExport` → `afterExport`.
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
## Table of Contents
|
|
110
|
+
|
|
111
|
+
- [Quick Start](#quick-start)
|
|
112
|
+
- [Capture Flow](#capture-flow)
|
|
113
|
+
- [Installation](#installation)
|
|
114
|
+
- [NPM / Yarn (stable)](#npm--yarn-stable)
|
|
115
|
+
- [NPM / Yarn (dev builds)](#npm--yarn-dev-builds)
|
|
116
|
+
- [CDN (stable)](#cdn-stable)
|
|
117
|
+
- [CDN (dev builds)](#cdn-dev-builds)
|
|
118
|
+
- [Build Outputs](#build-outputs--tree-shaking)
|
|
119
|
+
- [Usage](#usage)
|
|
120
|
+
- [Reusable capture](#reusable-capture)
|
|
121
|
+
- [One-step shortcuts](#one-step-shortcuts)
|
|
122
|
+
- [API](#api)
|
|
123
|
+
- [snapdom(el, options?)](#snapdomel-options)
|
|
124
|
+
- [Shortcut methods](#shortcut-methods)
|
|
125
|
+
- [Options](#options)
|
|
126
|
+
- [debug](#debug)
|
|
127
|
+
- [Fallback image on `<img>` load failure](#fallback-image-on-img-load-failure)
|
|
128
|
+
- [Dimensions (`scale`, `width`, `height`)](#dimensions-scale-width-height)
|
|
129
|
+
- [Cross-Origin Images & Fonts (`useProxy`)](#cross-origin-images--fonts-useproxy)
|
|
130
|
+
- [Fonts](#fonts)
|
|
131
|
+
- [embedFonts](#embedfonts)
|
|
132
|
+
- [localFonts](#localfonts)
|
|
133
|
+
- [iconFonts](#iconfonts)
|
|
134
|
+
- [excludeFonts](#excludefonts)
|
|
135
|
+
- [Filtering nodes: `exclude` vs `filter`](#filtering-nodes-exclude-vs-filter)
|
|
136
|
+
- [outerTransforms](#outerTransforms)
|
|
137
|
+
- [outerShadows](#no-shadows)
|
|
138
|
+
- [Cache control](#cache-control)
|
|
139
|
+
- [preCache](#precache--optional-helper)
|
|
140
|
+
- [Plugins (BETA)](#plugins-beta)
|
|
141
|
+
- [Official Plugins](#official-plugins)
|
|
142
|
+
- [Community Plugins](#community-plugins)
|
|
143
|
+
- [Build a Plugin in 5 Minutes](#build-a-plugin-in-5-minutes)
|
|
144
|
+
- [Registering Plugins](#registering-plugins)
|
|
145
|
+
- [Plugin Lifecycle Hooks](#plugin-lifecycle-hooks)
|
|
146
|
+
- [Context Object](#context-object)
|
|
147
|
+
- [Custom Exports via Plugins](#custom-exports-via-plugins)
|
|
148
|
+
- [Example: Overlay Filter Plugin](#example-overlay-filter-plugin)
|
|
149
|
+
- [Full Plugin Template](#full-plugin-template)
|
|
150
|
+
- [Limitations](#limitations)
|
|
151
|
+
- [⚡ Performance Benchmarks (Chromium)](#performance-benchmarks)
|
|
152
|
+
- [Simple elements](#simple-elements)
|
|
153
|
+
- [Complex elements](#complex-elements)
|
|
154
|
+
- [Run the benchmarks](#run-the-benchmarks)
|
|
155
|
+
- [Roadmap](#roadmap)
|
|
156
|
+
- [Development](#development)
|
|
157
|
+
- [Contributors 🙌](#contributors)
|
|
158
|
+
- [💖 Sponsors](#sponsors)
|
|
159
|
+
- [Star History](#star-history)
|
|
160
|
+
- [License](#license)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
## Installation
|
|
165
|
+
|
|
166
|
+
### NPM / Yarn (stable)
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npm i @zumer/snapdom
|
|
170
|
+
yarn add @zumer/snapdom
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### NPM / Yarn (dev builds)
|
|
174
|
+
|
|
175
|
+
For early access to new features and fixes:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
npm i @zumer/snapdom@dev
|
|
179
|
+
yarn add @zumer/snapdom@dev
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
⚠️ The `@dev` tag usually includes improvements before they reach production, but may be less stable.
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
### CDN (stable)
|
|
186
|
+
|
|
187
|
+
```html
|
|
188
|
+
<!-- Minified build -->
|
|
189
|
+
<script src="https://unpkg.com/@zumer/snapdom/dist/snapdom.js"></script>
|
|
190
|
+
|
|
191
|
+
<!-- Minified ES Module build -->
|
|
192
|
+
<script type="module">
|
|
193
|
+
import { snapdom } from "https://unpkg.com/@zumer/snapdom/dist/snapdom.mjs";
|
|
194
|
+
</script>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### CDN (dev builds)
|
|
198
|
+
|
|
199
|
+
```html
|
|
200
|
+
<!-- Minified build (dev) -->
|
|
201
|
+
<script src="https://unpkg.com/@zumer/snapdom@dev/dist/snapdom.js"></script>
|
|
202
|
+
|
|
203
|
+
<!-- Minified ES Module build (dev) -->
|
|
204
|
+
<script type="module">
|
|
205
|
+
import { snapdom } from "https://unpkg.com/@zumer/snapdom@dev/dist/snapdom.mjs";
|
|
206
|
+
</script>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Build Outputs
|
|
210
|
+
|
|
211
|
+
| Variant | File | Use case |
|
|
212
|
+
|---------|------|----------|
|
|
213
|
+
| **ESM** (tree-shakeable) | `dist/snapdom.mjs` | Bundlers (Vite, webpack), `import` |
|
|
214
|
+
| **IIFE** (global) | `dist/snapdom.js` | Script tag, legacy `require` |
|
|
215
|
+
|
|
216
|
+
**Bundler (npm):**
|
|
217
|
+
```js
|
|
218
|
+
import { snapdom } from '@zumer/snapdom'; // → dist/snapdom.mjs
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Script tag (CDN):**
|
|
222
|
+
```html
|
|
223
|
+
<script src="https://unpkg.com/@zumer/snapdom/dist/snapdom.js"></script>
|
|
224
|
+
<script> snapdom.toPng(document.body).then(img => document.body.appendChild(img)); </script>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Subpath imports** (lighter bundle if you only need one):
|
|
228
|
+
```js
|
|
229
|
+
import { preCache } from '@zumer/snapdom/preCache';
|
|
230
|
+
import { plugins } from '@zumer/snapdom/plugins';
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
## Usage
|
|
235
|
+
|
|
236
|
+
| Pattern | When to use |
|
|
237
|
+
|---------|-------------|
|
|
238
|
+
| **Reusable** `snapdom(el)` | One clone → many exports (PNG + JPG + download). |
|
|
239
|
+
| **Shortcuts** `snapdom.toPng(el)` | Single export, less code. |
|
|
240
|
+
|
|
241
|
+
### Reusable capture
|
|
242
|
+
|
|
243
|
+
Capture once, export many times (no re-clone):
|
|
244
|
+
|
|
245
|
+
```js
|
|
246
|
+
const el = document.querySelector('#target');
|
|
247
|
+
const result = await snapdom(el);
|
|
248
|
+
|
|
249
|
+
const img = await result.toPng();
|
|
250
|
+
document.body.appendChild(img);
|
|
251
|
+
await result.download({ format: 'jpg', filename: 'my-capture.jpg' });
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### One-step shortcuts
|
|
255
|
+
|
|
256
|
+
Direct export when you need a single format:
|
|
257
|
+
|
|
258
|
+
```js
|
|
259
|
+
const png = await snapdom.toPng(el);
|
|
260
|
+
const blob = await snapdom.toBlob(el);
|
|
261
|
+
document.body.appendChild(png);
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## API
|
|
265
|
+
|
|
266
|
+
### `snapdom(el, options?)`
|
|
267
|
+
|
|
268
|
+
Returns an object with reusable export methods:
|
|
269
|
+
|
|
270
|
+
```js
|
|
271
|
+
{
|
|
272
|
+
url: string;
|
|
273
|
+
toRaw(): string;
|
|
274
|
+
toImg(): Promise<HTMLImageElement>; // deprecated
|
|
275
|
+
toSvg(): Promise<HTMLImageElement>;
|
|
276
|
+
toCanvas(): Promise<HTMLCanvasElement>;
|
|
277
|
+
toBlob(options?): Promise<Blob>;
|
|
278
|
+
toPng(options?): Promise<HTMLImageElement>;
|
|
279
|
+
toJpg(options?): Promise<HTMLImageElement>;
|
|
280
|
+
toWebp(options?): Promise<HTMLImageElement>;
|
|
281
|
+
download(options?): Promise<void>;
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Shortcut methods
|
|
286
|
+
|
|
287
|
+
| Method | Description |
|
|
288
|
+
| ------------------------------ | --------------------------------- |
|
|
289
|
+
| `snapdom.toImg(el, options?)` | Returns an SVG `HTMLImageElement` (deprecated) |
|
|
290
|
+
| `snapdom.toSvg(el, options?)` | Returns an SVG `HTMLImageElement` |
|
|
291
|
+
| `snapdom.toCanvas(el, options?)` | Returns a `Canvas` |
|
|
292
|
+
| `snapdom.toBlob(el, options?)` | Returns an SVG or raster `Blob` |
|
|
293
|
+
| `snapdom.toPng(el, options?)` | Returns a PNG image |
|
|
294
|
+
| `snapdom.toJpg(el, options?)` | Returns a JPG image |
|
|
295
|
+
| `snapdom.toWebp(el, options?)` | Returns a WebP image |
|
|
296
|
+
| `snapdom.download(el, options?)` | Triggers a download |
|
|
297
|
+
|
|
298
|
+
### Exporter-specific options
|
|
299
|
+
|
|
300
|
+
Some exporters accept a small set of **export-only options** in addition to the global capture options.
|
|
301
|
+
|
|
302
|
+
#### `download()`
|
|
303
|
+
|
|
304
|
+
| Option | Type | Default | Description |
|
|
305
|
+
| --- | --- | --- | --- |
|
|
306
|
+
| `filename` | `string` | `snapdom` | Download name. |
|
|
307
|
+
| `format` | `"png" \| "jpeg" \| "jpg" \| "webp" \| "svg"` | `"png"` | Output format for the downloaded file. |
|
|
308
|
+
|
|
309
|
+
**Example:**
|
|
310
|
+
|
|
311
|
+
```js
|
|
312
|
+
await result.download({
|
|
313
|
+
format: 'jpg',
|
|
314
|
+
quality: 0.92,
|
|
315
|
+
filename: 'my-capture'
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### `toBlob()`
|
|
320
|
+
|
|
321
|
+
| Option | Type | Default | Description |
|
|
322
|
+
| --- | --- | --- | --- |
|
|
323
|
+
| `type` | `"svg" \| "png" \| "jpeg" \| "jpg" \| "webp"` | `"svg"` | Blob type to generate. |
|
|
324
|
+
|
|
325
|
+
**Example:**
|
|
326
|
+
|
|
327
|
+
```js
|
|
328
|
+
const blob = await result.toBlob({ type: 'jpeg', quality: 0.92 });
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Options
|
|
332
|
+
|
|
333
|
+
All capture methods accept an `options` object:
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
| Option | Type | Default | Description |
|
|
337
|
+
| ----------------- | -------- | -------- | ----------------------------------------------- |
|
|
338
|
+
| `debug` | boolean | `false` | When `true`, logs suppressed errors to `console.warn` for troubleshooting |
|
|
339
|
+
| `fast` | boolean | `true` | Skips small idle delays for faster results |
|
|
340
|
+
| `embedFonts` | boolean | `false` | Inlines non-icon fonts (icon fonts always on) |
|
|
341
|
+
| `localFonts` | array | `[]` | Local fonts `{ family, src, weight?, style? }` |
|
|
342
|
+
| `iconFonts` | string\|RegExp\|Array | `[]` | Extra icon font matchers |
|
|
343
|
+
| `excludeFonts` | object | `{}` | Exclude families/domains/subsets during embedding |
|
|
344
|
+
| `scale` | number | `1` | Output scale multiplier |
|
|
345
|
+
| `dpr` | number | `devicePixelRatio` | Device pixel ratio |
|
|
346
|
+
| `width` | number | - | Output width |
|
|
347
|
+
| `height` | number | - | Output height |
|
|
348
|
+
| `backgroundColor` | string | `"#fff"` | Fallback color for JPG/WebP |
|
|
349
|
+
| `quality` | number | `1` | Quality for JPG/WebP (0 to 1) |
|
|
350
|
+
| `useProxy` | string | `''` | Proxy base for CORS fallbacks |
|
|
351
|
+
| `exclude` | string[] | - | CSS selectors to exclude |
|
|
352
|
+
| `excludeMode` | `"hide"`\|`"remove"` | `"hide"` | How `exclude` is applied |
|
|
353
|
+
| `filter` | function | - | Custom predicate `(el) => boolean` |
|
|
354
|
+
| `filterMode` | `"hide"`\|`"remove"` | `"hide"` | How `filter` is applied |
|
|
355
|
+
| `cache` | string | `"soft"` | `disabled` \| `soft` \| `auto` \| `full` |
|
|
356
|
+
| `placeholders` | boolean | `true` | Show placeholders for images/CORS iframes |
|
|
357
|
+
| `fallbackURL` | string \| function | - | Fallback image for `<img>` load failure |
|
|
358
|
+
| `outerTransforms` | boolean | `true` | When `false` removes `translate/rotate` but preserves `scale/skew`, producing a flat, reusable capture |
|
|
359
|
+
| `outerShadows` | boolean | `false` | Do not expand the root’s bounding box for shadows/blur/outline, and strip those visual effects from the cloned root |
|
|
360
|
+
|
|
361
|
+
| `safariWarmupAttempts` | number | `3` | Safari only: iterations to prime font/decode (WebKit #219770). Use `1` if 3 causes lag |
|
|
362
|
+
|
|
363
|
+
### debug
|
|
364
|
+
|
|
365
|
+
When `debug: true`, SnapDOM logs normally suppressed errors to `console.warn` (with the `[snapdom]` prefix). Useful for troubleshooting capture issues (canvas failures, blob resolution, style stripping, etc.) without noisy output in production.
|
|
366
|
+
|
|
367
|
+
```js
|
|
368
|
+
await snapdom.toPng(el, { debug: true });
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Fallback image on `<img>` load failure
|
|
372
|
+
|
|
373
|
+
Provide a default image for failed `<img>` loads. You can pass a fixed URL or a callback that receives measured dimensions and returns a URL (handy to generate dynamic placeholders).
|
|
374
|
+
|
|
375
|
+
```js
|
|
376
|
+
// 1) Fixed URL fallback
|
|
377
|
+
await snapdom.toSvg(element, {
|
|
378
|
+
fallbackURL: '/images/fallback.png'
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// 2) Dynamic placeholder via callback
|
|
382
|
+
await snapdom.toSvg(element, {
|
|
383
|
+
fallbackURL: ({ width: 300, height: 150 }) =>
|
|
384
|
+
`https://placehold.co/${width}x${height}`
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// 3) With proxy (if your fallback host has no CORS)
|
|
388
|
+
await snapdom.toSvg(element, {
|
|
389
|
+
fallbackURL: ({ width = 300, height = 150 }) =>
|
|
390
|
+
`https://dummyimage.com/${width}x${height}/cccccc/666.png&text=img`,
|
|
391
|
+
useProxy: 'https://proxy.corsfix.com/?'
|
|
392
|
+
});
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Notes:
|
|
396
|
+
- If the fallback image also fails to load, snapDOM replaces the `<img>` with a placeholder block preserving width/height.
|
|
397
|
+
- Width/height used by the callback are gathered from the original element (dataset, style/attrs, etc.) when available.
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
### Dimensions (`scale`, `width`, `height`)
|
|
401
|
+
|
|
402
|
+
* If `scale` is provided, it **takes precedence** over `width`/`height`.
|
|
403
|
+
* If only `width` is provided, height scales proportionally (and vice versa).
|
|
404
|
+
* Providing both `width` and `height` forces an exact size (may distort).
|
|
405
|
+
|
|
406
|
+
### Cross-Origin Images & Fonts (`useProxy`)
|
|
407
|
+
|
|
408
|
+
By default snapDOM tries `crossOrigin="anonymous"` (or `use-credentials` for same-origin). If an asset is CORS-blocked, you can set `useProxy` to a prefix URL that forwards the actual `src`:
|
|
409
|
+
|
|
410
|
+
```js
|
|
411
|
+
await snapdom.toPng(el, {
|
|
412
|
+
useProxy: 'https://proxy.corsfix.com/?' // Note: Any cors proxy could be used 'https://proxy.corsfix.com/?'
|
|
413
|
+
});
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
* The proxy is only used as a **fallback**; same-origin and CORS-enabled assets skip it.
|
|
418
|
+
|
|
419
|
+
### Fonts
|
|
420
|
+
|
|
421
|
+
#### `embedFonts`
|
|
422
|
+
When `true`, snapDOM embeds **non-icon** `@font-face` rules detected as used within the captured subtree. Icon fonts (Font Awesome, Material Icons, etc.) are embedded **always**.
|
|
423
|
+
|
|
424
|
+
#### `localFonts`
|
|
425
|
+
If you serve fonts yourself or have data URLs, you can declare them here to avoid extra CSS discovery:
|
|
426
|
+
|
|
427
|
+
```js
|
|
428
|
+
await snapdom.toPng(el, {
|
|
429
|
+
embedFonts: true,
|
|
430
|
+
localFonts: [
|
|
431
|
+
{ family: 'Inter', src: '/fonts/Inter-Variable.woff2', weight: 400, style: 'normal' },
|
|
432
|
+
{ family: 'Inter', src: '/fonts/Inter-Italic.woff2', style: 'italic' }
|
|
433
|
+
]
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
#### `iconFonts`
|
|
438
|
+
Add custom icon families (names or regex matchers). Useful for private icon sets:
|
|
439
|
+
|
|
440
|
+
```js
|
|
441
|
+
await snapdom.toPng(el, {
|
|
442
|
+
iconFonts: ['MyIcons', /^(Remix|Feather) Icons?$/i]
|
|
443
|
+
});
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
#### `excludeFonts`
|
|
447
|
+
Skip specific non-icon fonts to speed up capture or avoid unnecessary downloads.
|
|
448
|
+
|
|
449
|
+
```js
|
|
450
|
+
await snapdom.toPng(el, {
|
|
451
|
+
embedFonts: true,
|
|
452
|
+
excludeFonts: {
|
|
453
|
+
families: ['Noto Serif', 'SomeHeavyFont'], // skip by family name
|
|
454
|
+
domains: ['fonts.gstatic.com', 'cdn.example'], // skip by source host
|
|
455
|
+
subsets: ['cyrillic-ext'] // skip by unicode-range subset tag
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
```
|
|
459
|
+
*Notes*
|
|
460
|
+
- `excludeFonts` only applies to **non-icon** fonts. Icon fonts are always embedded.
|
|
461
|
+
- Matching is case-insensitive for `families`. Hosts are matched by substring against the resolved URL.
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
#### Filtering nodes: `exclude` vs `filter`
|
|
465
|
+
|
|
466
|
+
* `exclude`: remove by **selector**.
|
|
467
|
+
* `excludeMode`: `hide` applies `visibility:hidden` CSS rule on excluded nodes and the layout remains as the original. `remove` do not clone excluded nodes at all.
|
|
468
|
+
* `filter`: advanced predicate per element (return `false` to drop).
|
|
469
|
+
* `filterMode`: `hide` applies `visibility:hidden` CSS rule on filtered nodes and the layout remains as the original. `remove` do not clone filtered nodes at all.
|
|
470
|
+
|
|
471
|
+
**Example: filter out elements with `display:none`:**
|
|
472
|
+
```js
|
|
473
|
+
/**
|
|
474
|
+
* Example filter: skip elements with display:none
|
|
475
|
+
* @param {Element} el
|
|
476
|
+
* @returns {boolean} true = keep, false = exclude
|
|
477
|
+
*/
|
|
478
|
+
function filterHidden(el) {
|
|
479
|
+
const cs = window.getComputedStyle(el);
|
|
480
|
+
if (cs.display === 'none') return false;
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
await snapdom.toPng(document.body, { filter: filterHidden });
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Example with `exclude`:** remove banners or tooltips by selector
|
|
488
|
+
```js
|
|
489
|
+
await snapdom.toPng(el, {
|
|
490
|
+
exclude: ['.cookie-banner', '.tooltip', '[data-test="debug"]']
|
|
491
|
+
});
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### outerTransforms
|
|
495
|
+
|
|
496
|
+
When capturing rotated or translated elements, you may want use **outerTransforms: false** option if you want to eliminate those external transforms. So, the output is **flat, upright, and ready** to use elsewhere.
|
|
497
|
+
|
|
498
|
+
- **`outerTransforms: true (default)`**
|
|
499
|
+
**Keeps the original `transforms` and `rotate`**.
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
### outerShadows
|
|
503
|
+
- **`outerShadows: false (default)`**
|
|
504
|
+
Prevents expanding the bounding box for shadows, blur, or outline on the root, and also strips `box-shadow`, `text-shadow`, `filter: blur()/drop-shadow()`, and `outline` from the cloned root.
|
|
505
|
+
|
|
506
|
+
> 💡 **Tip:** Using both (`outerTransforms: false` + `outerShadows: false`) produces a strict, minimal bounding box with no visual bleed.
|
|
507
|
+
|
|
508
|
+
**Example**
|
|
509
|
+
|
|
510
|
+
```js
|
|
511
|
+
// outerTransforms and remove shadow bleed
|
|
512
|
+
await snapdom.toSvg(el, { outerTransforms: true, outerShadows: true });
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## Cache control
|
|
516
|
+
|
|
517
|
+
SnapDOM maintains internal caches for images, backgrounds, resources, styles, and fonts.
|
|
518
|
+
You can control how they are cleared between captures using the `cache` option:
|
|
519
|
+
|
|
520
|
+
| Mode | Description |
|
|
521
|
+
| ----------- | --------------------------------------------------------------------------- |
|
|
522
|
+
| `"disabled"`| No cache |
|
|
523
|
+
| `"soft"` | Clears session caches (`styleMap`, `nodeMap`, `styleCache`) _(default)_ |
|
|
524
|
+
| `"auto"` | Minimal cleanup: only clears transient maps |
|
|
525
|
+
| `"full"` | Keeps all caches (nothing is cleared, maximum performance) |
|
|
526
|
+
|
|
527
|
+
**Examples:**
|
|
528
|
+
|
|
529
|
+
```js
|
|
530
|
+
// Use minimal but fast cache
|
|
531
|
+
await snapdom.toPng(el, { cache: 'auto' });
|
|
532
|
+
|
|
533
|
+
// Keep everything in memory between captures
|
|
534
|
+
await snapdom.toPng(el, { cache: 'full' });
|
|
535
|
+
|
|
536
|
+
// Force a full cleanup on every capture
|
|
537
|
+
await snapdom.toPng(el, { cache: 'disabled' });
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
## `preCache()` – Optional helper
|
|
541
|
+
|
|
542
|
+
Preloads external resources to avoid first-capture stalls (helpful for big/complex trees).
|
|
543
|
+
|
|
544
|
+
```js
|
|
545
|
+
import { preCache } from '@zumer/snapdom';
|
|
546
|
+
|
|
547
|
+
await preCache({
|
|
548
|
+
root: document.body,
|
|
549
|
+
embedFonts: true,
|
|
550
|
+
localFonts: [{ family: 'Inter', src: '/fonts/Inter.woff2', weight: 400 }],
|
|
551
|
+
useProxy: 'https://proxy.corsfix.com/?'
|
|
552
|
+
});
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
## Plugins (BETA)
|
|
556
|
+
|
|
557
|
+
SnapDOM includes a lightweight **plugin system** that allows you to extend or override behavior at any stage of the capture and export process — without touching the core library.
|
|
558
|
+
|
|
559
|
+
A plugin is a simple object with a unique `name` and one or more lifecycle **hooks**.
|
|
560
|
+
Hooks can be synchronous or `async`, and they receive a shared **`context`** object.
|
|
561
|
+
|
|
562
|
+
### Official Plugins
|
|
563
|
+
|
|
564
|
+
Install the official plugin package:
|
|
565
|
+
|
|
566
|
+
```bash
|
|
567
|
+
npm install @zumer/snapdom-plugins
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
```js
|
|
571
|
+
import { filter } from '@zumer/snapdom-plugins/filter';
|
|
572
|
+
import { timestampOverlay } from '@zumer/snapdom-plugins/timestamp-overlay';
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
| Plugin | Category | Description |
|
|
576
|
+
|--------|----------|-------------|
|
|
577
|
+
| `picture-resolver` | Capture | Resolves lazy-loaded `<picture>` placeholders. Detects base64 stubs and fetches the real image before capture. |
|
|
578
|
+
| `timestamp-overlay` | Transform | Adds a configurable timestamp label on the captured clone. Supports multiple date formats and positions. |
|
|
579
|
+
| `filter` | Transform | Applies CSS filter effects to captures. Ships with presets: `grayscale`, `sepia`, `blur`, `vintage`, `dramatic`. |
|
|
580
|
+
| `replace-text` | Transform | Find-and-replace text in the captured clone. Supports strings and regex patterns. |
|
|
581
|
+
| `color-tint` | Transform | Tints the entire capture to a specified color using an overlay with `mix-blend-mode`. |
|
|
582
|
+
| `ascii-export` | Export | Adds a `toAscii()` method that converts captures to ASCII art. Configurable width, charset, and luminance. |
|
|
583
|
+
| `pdf-image` | Export | Exports the capture as a PNG embedded in a downloadable PDF. Supports portrait and landscape orientations. |
|
|
584
|
+
| `html-in-canvas` | Export | Uses the experimental WICG `drawElementImage` API for direct DOM-to-canvas rendering where supported. |
|
|
585
|
+
| `prompt-export` | Export | LLM-friendly capture: adds a `toPrompt()` method that returns an annotated screenshot, structured element map with bounding boxes, and a pre-formatted text prompt. |
|
|
586
|
+
|
|
587
|
+
### Community Plugins
|
|
588
|
+
|
|
589
|
+
Community plugins are listed on the [Plugins page](https://zumerlab.github.io/snapdom/plugins.html). To submit your plugin, open a PR adding one line to `community-plugins.md`. See [CONTRIBUTING_PLUGINS.md](CONTRIBUTING_PLUGINS.md).
|
|
590
|
+
|
|
591
|
+
### Build a Plugin in 5 Minutes
|
|
592
|
+
|
|
593
|
+
SnapDOM's hook system gives you full control over every stage of the capture pipeline:
|
|
594
|
+
|
|
595
|
+
1. **Clone the template** — `npx degit zumerlab/snapdom/packages/plugin-template my-plugin`
|
|
596
|
+
2. **Write your hook logic** — `export function myPlugin() {}`
|
|
597
|
+
3. **Get listed** — open a PR adding one line to `community-plugins.md`
|
|
598
|
+
|
|
599
|
+
See [PLUGIN_SPEC.md](PLUGIN_SPEC.md) for the full specification and [CONTRIBUTING_PLUGINS.md](CONTRIBUTING_PLUGINS.md) for submission guidelines.
|
|
600
|
+
|
|
601
|
+
### Registering Plugins
|
|
602
|
+
|
|
603
|
+
**Global registration** (applies to all captures):
|
|
604
|
+
|
|
605
|
+
```js
|
|
606
|
+
import { snapdom } from '@zumer/snapdom';
|
|
607
|
+
|
|
608
|
+
// You can register instances, factories, or [factory, options]
|
|
609
|
+
snapdom.plugins(
|
|
610
|
+
myPluginInstance,
|
|
611
|
+
[myPluginFactory, { optionA: true }],
|
|
612
|
+
{ plugin: anotherFactory, options: { level: 2 } }
|
|
613
|
+
);
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
**Per-capture registration** (only for that specific call):
|
|
617
|
+
|
|
618
|
+
```js
|
|
619
|
+
const out = await snapdom(element, {
|
|
620
|
+
plugins: [
|
|
621
|
+
[overlayFilterPlugin, { color: 'rgba(0,0,0,0.25)' }],
|
|
622
|
+
[myFullPlugin, { providePdf: true }]
|
|
623
|
+
]
|
|
624
|
+
});
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
* **Execution order = registration order** (first registered, first executed).
|
|
628
|
+
* **Per-capture plugins** run **before** global ones.
|
|
629
|
+
* Duplicates are automatically skipped by `name`; a per-capture plugin with the same `name` overrides its global version.
|
|
630
|
+
|
|
631
|
+
### Plugin Lifecycle Hooks
|
|
632
|
+
|
|
633
|
+
Hooks run in capture order (see [Capture Flow](#capture-flow)):
|
|
634
|
+
|
|
635
|
+
| Hook | Stage | Purpose |
|
|
636
|
+
|------|-------|---------|
|
|
637
|
+
| `beforeSnap` | Start | Adjust options before any work. |
|
|
638
|
+
| `beforeClone` | Pre-clone | Before DOM clone (modify live DOM carefully). |
|
|
639
|
+
| `afterClone` | Post-clone | Modify cloned tree safely (e.g. inject overlay). |
|
|
640
|
+
| `beforeRender` | Pre-serialize | Right before SVG → data URL. |
|
|
641
|
+
| `afterRender` | Post-serialize | Inspect `context.svgString` / `context.dataURL`. |
|
|
642
|
+
| `beforeExport` | Per export | Before each `toPng`, `toSvg`, etc. |
|
|
643
|
+
| `afterExport` | Per export | Transform returned result. |
|
|
644
|
+
| `afterSnap` | Once | After first export; cleanup. |
|
|
645
|
+
| `defineExports` | Setup | Add custom exporters (e.g. `toPdf`). |
|
|
646
|
+
|
|
647
|
+
> Returned values from `afterExport` are chained to the next plugin (transform pipeline).
|
|
648
|
+
|
|
649
|
+
### Context Object
|
|
650
|
+
|
|
651
|
+
Every hook receives a single `context` object that contains normalized capture state:
|
|
652
|
+
|
|
653
|
+
* **Input & options:**
|
|
654
|
+
`element`, `debug`, `fast`, `scale`, `dpr`, `width`, `height`, `backgroundColor`, `quality`, `useProxy`, `cache`, `outerTransforms`, `outerShadows`, `safariWarmupAttempts`, `embedFonts`, `localFonts`, `iconFonts`, `excludeFonts`, `exclude`, `excludeMode`, `filter`, `filterMode`, `fallbackURL`.
|
|
655
|
+
|
|
656
|
+
* **Intermediate values (depending on stage):**
|
|
657
|
+
`clone`, `classCSS`, `styleCache`, `fontsCSS`, `baseCSS`, `svgString`, `dataURL`.
|
|
658
|
+
|
|
659
|
+
* **During export:**
|
|
660
|
+
`context.export = { type, options, url }`
|
|
661
|
+
where `type` is the exporter name (`"png"`, `"jpeg"`, `"svg"`, `"blob"`, etc.), and `url` is the serialized SVG base.
|
|
662
|
+
|
|
663
|
+
> You may safely modify `context` (e.g., override `backgroundColor` or `quality`) — but do so early (`beforeSnap`) for global effects or in `beforeExport` for single-export changes.
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
## Custom Exports via Plugins
|
|
667
|
+
|
|
668
|
+
Plugins can add new exports using `defineExports(context)`.
|
|
669
|
+
For each export key you return (e.g., `"pdf"`), SnapDOM automatically exposes a helper method named **`toPdf()`** on the capture result.
|
|
670
|
+
|
|
671
|
+
**Register the plugin (global or per capture):**
|
|
672
|
+
|
|
673
|
+
```js
|
|
674
|
+
import { snapdom } from '@zumer/snapdom';
|
|
675
|
+
|
|
676
|
+
// global
|
|
677
|
+
snapdom.plugins(pdfExportPlugin());
|
|
678
|
+
|
|
679
|
+
// or per capture
|
|
680
|
+
const out = await snapdom(element, { plugins: [pdfExportPlugin()] });
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
**Call the custom export:**
|
|
684
|
+
|
|
685
|
+
```js
|
|
686
|
+
const out = await snapdom(document.querySelector('#report'));
|
|
687
|
+
|
|
688
|
+
// because the plugin returns { pdf: async (ctx, opts) => ... }
|
|
689
|
+
const pdfBlob = await out.toPdf({
|
|
690
|
+
// exporter-specific options (width, height, quality, filename, etc.)
|
|
691
|
+
});
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
### Example: Overlay Filter Plugin
|
|
695
|
+
|
|
696
|
+
Adds a translucent overlay or color filter **only** to the captured clone (not your live DOM).
|
|
697
|
+
Useful for highlighting or dimming sections before export.
|
|
698
|
+
|
|
699
|
+
```js
|
|
700
|
+
/**
|
|
701
|
+
* Ultra-simple overlay filter for SnapDOM (HTML-only).
|
|
702
|
+
* Inserts a full-size <div> overlay on the cloned root.
|
|
703
|
+
*
|
|
704
|
+
* @param {{ color?: string; blur?: number }} [options]
|
|
705
|
+
* color: overlay color (rgba/hex/hsl). Default: 'rgba(0,0,0,0.25)'
|
|
706
|
+
* blur: optional blur in px (default: 0)
|
|
707
|
+
*/
|
|
708
|
+
export function overlayFilterPlugin(options = {}) {
|
|
709
|
+
const color = options.color ?? 'rgba(0,0,0,0.25)';
|
|
710
|
+
const blur = Math.max(0, options.blur ?? 0);
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
name: 'overlay-filter',
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Add a full-coverage overlay to the cloned HTML root.
|
|
717
|
+
* @param {any} context
|
|
718
|
+
*/
|
|
719
|
+
async afterClone(context) {
|
|
720
|
+
const root = context.clone;
|
|
721
|
+
if (!(root instanceof HTMLElement)) return; // HTML-only
|
|
722
|
+
|
|
723
|
+
// Ensure containing block so absolute overlay anchors to the root
|
|
724
|
+
if (getComputedStyle(root).position === 'static') {
|
|
725
|
+
root.style.position = 'relative';
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const overlay = document.createElement('div');
|
|
729
|
+
overlay.style.position = 'absolute';
|
|
730
|
+
overlay.style.left = '0';
|
|
731
|
+
overlay.style.top = '0';
|
|
732
|
+
overlay.style.right = '0';
|
|
733
|
+
overlay.style.bottom = '0';
|
|
734
|
+
overlay.style.background = color;
|
|
735
|
+
overlay.style.pointerEvents = 'none';
|
|
736
|
+
if (blur) overlay.style.filter = `blur(${blur}px)`;
|
|
737
|
+
|
|
738
|
+
root.appendChild(overlay);
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
**Usage:**
|
|
746
|
+
|
|
747
|
+
```js
|
|
748
|
+
import { snapdom } from '@zumer/snapdom';
|
|
749
|
+
|
|
750
|
+
// Global registration
|
|
751
|
+
snapdom.plugins([overlayFilterPlugin, { color: 'rgba(0,0,0,0.3)', blur: 2 }]);
|
|
752
|
+
|
|
753
|
+
// Per-capture
|
|
754
|
+
const out = await snapdom(document.querySelector('#card'), {
|
|
755
|
+
plugins: [[overlayFilterPlugin, { color: 'rgba(255,200,0,0.15)' }]]
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
const png = await out.toPng();
|
|
759
|
+
document.body.appendChild(png);
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
> The overlay is injected **only in the cloned tree**, never in your live DOM, ensuring perfect fidelity and zero flicker.
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
### Full Plugin Template
|
|
766
|
+
|
|
767
|
+
Use this as a starting point for custom logic or exporters.
|
|
768
|
+
|
|
769
|
+
```js
|
|
770
|
+
export function myPlugin(options = {}) {
|
|
771
|
+
return {
|
|
772
|
+
/** Unique name used for de-duplication/overrides */
|
|
773
|
+
name: 'my-plugin',
|
|
774
|
+
|
|
775
|
+
/** Early adjustments before any clone/style work. */
|
|
776
|
+
async beforeSnap(context) {},
|
|
777
|
+
|
|
778
|
+
/** Before subtree cloning (use sparingly if touching the live DOM). */
|
|
779
|
+
async beforeClone(context) {},
|
|
780
|
+
|
|
781
|
+
/** After subtree cloning (safe to modify the cloned tree). */
|
|
782
|
+
async afterClone(context) {},
|
|
783
|
+
|
|
784
|
+
/** Right before serialization (SVG/dataURL). */
|
|
785
|
+
async beforeRender(context) {},
|
|
786
|
+
|
|
787
|
+
/** After serialization; inspect context.svgString/context.dataURL if needed. */
|
|
788
|
+
async afterRender(context) {},
|
|
789
|
+
|
|
790
|
+
/** Before EACH export call (toPng/toSvg/toBlob/...). */
|
|
791
|
+
async beforeExport(context) {},
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* After EACH export call.
|
|
795
|
+
* If you return a value, it becomes the result for the next plugin (chaining).
|
|
796
|
+
*/
|
|
797
|
+
async afterExport(context, result) { return result; },
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Define custom exporters (auto-added as helpers like out.toPdf()).
|
|
801
|
+
* Return a map { [key: string]: (ctx:any, opts:any) => Promise<any> }.
|
|
802
|
+
*/
|
|
803
|
+
async defineExports(context) { return {}; },
|
|
804
|
+
|
|
805
|
+
/** Runs ONCE after the FIRST export finishes (cleanup). */
|
|
806
|
+
async afterSnap(context) {}
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
**Quick recap:**
|
|
812
|
+
|
|
813
|
+
* Plugins can modify capture behavior (`beforeSnap`, `afterClone`, etc.).
|
|
814
|
+
* You can inject visuals or transformations safely into the cloned tree.
|
|
815
|
+
* New exporters defined in `defineExports()` automatically become helpers like `out.toPdf()`.
|
|
816
|
+
* All hooks can be asynchronous, run in order, and share the same `context`.
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
## Limitations
|
|
820
|
+
|
|
821
|
+
* External images should be CORS-accessible (use `useProxy` option for handling CORS denied)
|
|
822
|
+
* When WebP format is used on Safari, it will fallback to PNG rendering.
|
|
823
|
+
* `@font-face` CSS rule is well supported, but if need to use JS `FontFace()`, see this workaround [`#43`](https://github.com/zumerlab/snapdom/issues/43)
|
|
824
|
+
* **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 does pre-captures + `drawImage` to prime the pipeline; configurable via `safariWarmupAttempts` (default 3).
|
|
825
|
+
* **Custom scrollbar styles** (`::-webkit-scrollbar`): Applied only when the element has *not* been scrolled. When scrolled, the viewport content is captured without the scrollbar.
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
## Performance Benchmarks
|
|
829
|
+
|
|
830
|
+
**Setup.** Vitest benchmarks on Chromium, repo tests. Hardware may affect results.
|
|
831
|
+
Values are **average capture time (ms)** → lower is better.
|
|
832
|
+
|
|
833
|
+
### Simple elements
|
|
834
|
+
|
|
835
|
+
| Scenario | SnapDOM current | SnapDOM v1.9.9 | html2canvas | html-to-image |
|
|
836
|
+
| ------------------------ | --------------- | -------------- | ----------- | ------------- |
|
|
837
|
+
| Small (200×100) | **0.5 ms** | 0.8 ms | 67.7 ms | 3.1 ms |
|
|
838
|
+
| Modal (400×300) | **0.5 ms** | 0.8 ms | 75.5 ms | 3.6 ms |
|
|
839
|
+
| Page View (1200×800) | **0.5 ms** | 0.8 ms | 114.2 ms | 3.3 ms |
|
|
840
|
+
| Large Scroll (2000×1500) | **0.5 ms** | 0.8 ms | 186.3 ms | 3.2 ms |
|
|
841
|
+
| Very Large (4000×2000) | **0.5 ms** | 0.9 ms | 425.9 ms | 3.3 ms |
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
### Complex elements
|
|
845
|
+
|
|
846
|
+
| Scenario | SnapDOM current | SnapDOM v1.9.9 | html2canvas | html-to-image |
|
|
847
|
+
| ------------------------ | --------------- | -------------- | ----------- | ------------- |
|
|
848
|
+
| Small (200×100) | **1.6 ms** | 3.3 ms | 68.0 ms | 14.3 ms |
|
|
849
|
+
| Modal (400×300) | **2.9 ms** | 6.8 ms | 87.5 ms | 34.8 ms |
|
|
850
|
+
| Page View (1200×800) | **17.5 ms** | 50.2 ms | 178.0 ms | 429.0 ms |
|
|
851
|
+
| Large Scroll (2000×1500) | **54.0 ms** | 201.8 ms | 735.2 ms | 984.2 ms |
|
|
852
|
+
| Very Large (4000×2000) | **171.4 ms** | 453.7 ms | 1,800.4 ms | 2,611.9 ms |
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
### Run the benchmarks
|
|
856
|
+
|
|
857
|
+
```sh
|
|
858
|
+
git clone https://github.com/zumerlab/snapdom.git
|
|
859
|
+
cd snapdom
|
|
860
|
+
npm install
|
|
861
|
+
npm run test:benchmark
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
## Roadmap
|
|
866
|
+
|
|
867
|
+
Planned improvements for future versions of SnapDOM:
|
|
868
|
+
|
|
869
|
+
* [X] **Implement plugin system**
|
|
870
|
+
SnapDOM will support external plugins to extend or override internal behavior (e.g. custom node transformers, exporters, or filters).
|
|
871
|
+
|
|
872
|
+
* [ ] **Refactor to modular architecture**
|
|
873
|
+
Internal logic will be split into smaller, focused modules to improve maintainability and code reuse.
|
|
874
|
+
|
|
875
|
+
* [X] **Decouple internal logic from global options**
|
|
876
|
+
Functions will be redesigned to avoid relying directly on `options`. A centralized capture context will improve clarity, autonomy, and testability. See [`next` branch](https://github.com/zumerlab/snapdom/tree/main)
|
|
877
|
+
|
|
878
|
+
* [X] **Expose cache control**
|
|
879
|
+
Users will be able to manually clear image and font caches or configure their own caching strategies.
|
|
880
|
+
|
|
881
|
+
* [X] **Auto font preloading**
|
|
882
|
+
Required fonts will be automatically detected and preloaded before capture, reducing the need for manual `preCache()` calls.
|
|
883
|
+
|
|
884
|
+
* [X] **Document plugin development**
|
|
885
|
+
A full guide will be provided for creating and registering custom SnapDOM plugins.
|
|
886
|
+
|
|
887
|
+
* [ ] **Make export utilities tree-shakeable**
|
|
888
|
+
Export functions like `toPng`, `toJpg`, `toBlob`, etc. will be restructured into independent modules to support tree shaking and minimal builds.
|
|
889
|
+
|
|
890
|
+
Have ideas or feature requests?
|
|
891
|
+
Feel free to share suggestions or feedback in [GitHub Discussions](https://github.com/zumerlab/snapdom/discussions).
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
## Development
|
|
895
|
+
|
|
896
|
+
**Source layout:**
|
|
897
|
+
- `src/api/` – Public API (`snapdom`, `preCache`)
|
|
898
|
+
- `src/core/` – Capture pipeline, clone, prepare, plugins
|
|
899
|
+
- `src/modules/` – Images, fonts, pseudo-elements, backgrounds, SVG
|
|
900
|
+
- `src/exporters/` – toPng, toSvg, toBlob, etc.
|
|
901
|
+
- `dist/` – Build output (`snapdom.js`, `snapdom.mjs`, `preCache.mjs`, `plugins.mjs`)
|
|
902
|
+
|
|
903
|
+
**Build:**
|
|
904
|
+
```sh
|
|
905
|
+
git clone https://github.com/zumerlab/snapdom.git
|
|
906
|
+
cd snapdom
|
|
907
|
+
git checkout dev
|
|
908
|
+
npm install
|
|
909
|
+
npm run compile
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
**Test:**
|
|
913
|
+
```sh
|
|
914
|
+
npx playwright install # Required for browser tests
|
|
915
|
+
npm test
|
|
916
|
+
npm run test:benchmark
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
For detailed guidelines, see [CONTRIBUTING](https://github.com/zumerlab/snapdom/blob/main/CONTRIBUTING.md).
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
## Contributors
|
|
923
|
+
|
|
924
|
+
<!-- CONTRIBUTORS:START -->
|
|
925
|
+
<p>
|
|
926
|
+
<a href="https://github.com/tinchox5" title="tinchox5"><img src="https://avatars.githubusercontent.com/u/11557901?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="tinchox5"/></a>
|
|
927
|
+
<a href="https://github.com/Jarvis2018" title="Jarvis2018"><img src="https://avatars.githubusercontent.com/u/36788851?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="Jarvis2018"/></a>
|
|
928
|
+
<a href="https://github.com/tarwin" title="tarwin"><img src="https://avatars.githubusercontent.com/u/646149?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="tarwin"/></a>
|
|
929
|
+
<a href="https://github.com/Amyuan23" title="Amyuan23"><img src="https://avatars.githubusercontent.com/u/25892910?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="Amyuan23"/></a>
|
|
930
|
+
<a href="https://github.com/airamhr9" title="airamhr9"><img src="https://avatars.githubusercontent.com/u/57371081?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="airamhr9"/></a>
|
|
931
|
+
<a href="https://github.com/FlavioLimaMindera" title="FlavioLimaMindera"><img src="https://avatars.githubusercontent.com/u/96424442?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="FlavioLimaMindera"/></a>
|
|
932
|
+
<a href="https://github.com/jswhisperer" title="jswhisperer"><img src="https://avatars.githubusercontent.com/u/1177690?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="jswhisperer"/></a>
|
|
933
|
+
<a href="https://github.com/K1ender" title="K1ender"><img src="https://avatars.githubusercontent.com/u/146767945?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="K1ender"/></a>
|
|
934
|
+
<a href="https://github.com/kohaiy" title="kohaiy"><img src="https://avatars.githubusercontent.com/u/15622127?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="kohaiy"/></a>
|
|
935
|
+
<a href="https://github.com/17biubiu" title="17biubiu"><img src="https://avatars.githubusercontent.com/u/13295895?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="17biubiu"/></a>
|
|
936
|
+
<a href="https://github.com/av01d" title="av01d"><img src="https://avatars.githubusercontent.com/u/6247646?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="av01d"/></a>
|
|
937
|
+
<a href="https://github.com/CHOYSEN" title="CHOYSEN"><img src="https://avatars.githubusercontent.com/u/25995358?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="CHOYSEN"/></a>
|
|
938
|
+
<a href="https://github.com/pedrocateexte" title="pedrocateexte"><img src="https://avatars.githubusercontent.com/u/207524750?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="pedrocateexte"/></a>
|
|
939
|
+
<a href="https://github.com/domialex" title="domialex"><img src="https://avatars.githubusercontent.com/u/4694217?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="domialex"/></a>
|
|
940
|
+
<a href="https://github.com/elliots" title="elliots"><img src="https://avatars.githubusercontent.com/u/622455?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="elliots"/></a>
|
|
941
|
+
<a href="https://github.com/stypr" title="stypr"><img src="https://avatars.githubusercontent.com/u/6625978?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="stypr"/></a>
|
|
942
|
+
<a href="https://github.com/mon-jai" title="mon-jai"><img src="https://avatars.githubusercontent.com/u/91261297?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="mon-jai"/></a>
|
|
943
|
+
<a href="https://github.com/sharuzzaman" title="sharuzzaman"><img src="https://avatars.githubusercontent.com/u/7421941?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="sharuzzaman"/></a>
|
|
944
|
+
<a href="https://github.com/simon1uo" title="simon1uo"><img src="https://avatars.githubusercontent.com/u/60037549?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="simon1uo"/></a>
|
|
945
|
+
<a href="https://github.com/titoBouzout" title="titoBouzout"><img src="https://avatars.githubusercontent.com/u/64156?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="titoBouzout"/></a>
|
|
946
|
+
<a href="https://github.com/ZiuChen" title="ZiuChen"><img src="https://avatars.githubusercontent.com/u/64892985?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="ZiuChen"/></a>
|
|
947
|
+
<a href="https://github.com/harshasiddartha" title="harshasiddartha"><img src="https://avatars.githubusercontent.com/u/147021873?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="harshasiddartha"/></a>
|
|
948
|
+
<a href="https://github.com/karasHou" title="karasHou"><img src="https://avatars.githubusercontent.com/u/27048083?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="karasHou"/></a>
|
|
949
|
+
<a href="https://github.com/jhbae200" title="jhbae200"><img src="https://avatars.githubusercontent.com/u/20170610?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="jhbae200"/></a>
|
|
950
|
+
<a href="https://github.com/xiaobai-web715" title="xiaobai-web715"><img src="https://avatars.githubusercontent.com/u/81091224?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="xiaobai-web715"/></a>
|
|
951
|
+
<a href="https://github.com/miusuncle" title="miusuncle"><img src="https://avatars.githubusercontent.com/u/7549857?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="miusuncle"/></a>
|
|
952
|
+
<a href="https://github.com/rbbydotdev" title="rbbydotdev"><img src="https://avatars.githubusercontent.com/u/101137670?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="rbbydotdev"/></a>
|
|
953
|
+
<a href="https://github.com/zhanghaotian2018" title="zhanghaotian2018"><img src="https://avatars.githubusercontent.com/u/169218899?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="zhanghaotian2018"/></a>
|
|
954
|
+
</p>
|
|
955
|
+
<!-- CONTRIBUTORS:END -->
|
|
956
|
+
|
|
957
|
+
## Sponsors
|
|
958
|
+
|
|
959
|
+
Special thanks to [@megaphonecolin](https://github.com/megaphonecolin), [@sdraper69](https://github.com/sdraper69), [@reynaldichernando](https://github.com/reynaldichernando), [@gamma-app](https://github.com/gamma-app) and [@jrjohnson](https://github.com/jrjohnson),for supporting this project!
|
|
960
|
+
|
|
961
|
+
If you'd like to support this project too, you can [become a sponsor](https://github.com/sponsors/tinchox5).
|
|
962
|
+
|
|
963
|
+
## Star History
|
|
964
|
+
|
|
965
|
+
[](https://www.star-history.com/#zumerlab/snapdom&Date)
|
|
966
|
+
|
|
967
|
+
## License
|
|
968
|
+
|
|
969
|
+
MIT © Zumerlab
|