@zumer/snapdom 2.15.0-dev.1 → 2.16.0-colin.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
@@ -1,11 +1,14 @@
1
1
  <p align="center">
2
- <a href="http://zumerlab.github.io/snapdom">
2
+ <a href="https://snapdom.dev">
3
3
  <img src="https://raw.githubusercontent.com/zumerlab/snapdom/main/docs/assets/newhero.png" width="80%">
4
4
  </a>
5
5
  </p>
6
6
 
7
7
  <p align="center">
8
- <a href="https://www.npmjs.com/package/@zumer/snapdom">
8
+ <a href="https://snapdom.dev">
9
+ <img alt="Website" src="https://img.shields.io/badge/Website-snapdom.dev-2ea44f?style=flat-square">
10
+ </a>
11
+ <a href="https://www.npmjs.com/package/@zumer/snapdom">
9
12
  <img alt="NPM version" src="https://img.shields.io/npm/v/@zumer/snapdom?style=flat-square&label=Version">
10
13
  </a>
11
14
  <a href="https://www.npmjs.com/package/@zumer/snapdom">
@@ -36,16 +39,15 @@
36
39
  **SnapDOM** is a next-generation **DOM Capture Engine** — the fast, modern alternative to **html2canvas**, **dom-to-image**, and **html-to-image**.
37
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.
38
41
 
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
42
+ > 📖 **[Documentation, guides & live demos → snapdom.dev](https://snapdom.dev)**
43
+
44
+ ## Features
45
+
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.
47
47
 
48
- ## Demo
48
+ 👉 **See the complete technical feature list in [FEATURES.md](FEATURES.md).**
49
+
50
+ ## Website & Live Demos
49
51
 
50
52
  [https://snapdom.dev](https://snapdom.dev)
51
53
 
@@ -72,95 +74,23 @@ await result.download({ format: 'jpg', filename: 'card.jpg' });
72
74
 
73
75
  ---
74
76
 
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
77
  ## Table of Contents
110
78
 
111
79
  - [Quick Start](#quick-start)
112
- - [Capture Flow](#capture-flow)
80
+ - [Features](#features)
81
+ - [Website & Live Demos](#website--live-demos)
113
82
  - [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)
83
+ - [Build Outputs](#build-outputs)
119
84
  - [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)
85
+ - [Documentation](#documentation) — full API, Options, Plugins & Cache reference on [snapdom.dev/docs](https://snapdom.dev/docs/)
150
86
  - [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)
87
+ - [Performance Benchmarks](#performance-benchmarks)
156
88
  - [Development](#development)
157
- - [Contributors 🙌](#contributors)
158
- - [💖 Sponsors](#sponsors)
159
- - [Star History](#star-history)
89
+ - [Contributors](#contributors)
90
+ - [Sponsors](#sponsors)
91
+ - [Show your support](#show-your-support)
160
92
  - [License](#license)
161
93
 
162
-
163
-
164
94
  ## Installation
165
95
 
166
96
  ### NPM / Yarn (stable)
@@ -261,579 +191,61 @@ const blob = await snapdom.toBlob(el);
261
191
  document.body.appendChild(png);
262
192
  ```
263
193
 
264
- ## API
194
+ ## Documentation
265
195
 
266
- ### `snapdom(el, options?)`
196
+ The full reference lives on **[snapdom.dev/docs](https://snapdom.dev/docs/)** — kept there so it stays in sync and searchable:
267
197
 
268
- Returns an object with reusable export methods:
198
+ - **[API reference](https://snapdom.dev/docs/api/)** — the `snapdom()` reusable object, shortcut methods, and exporter-specific options.
199
+ - **[Options](https://snapdom.dev/docs/options/)** — every capture option (`scale`, `dpr`, `embedFonts`, `useProxy`, `exclude`/`filter`, `compress`, `outerTransforms`, `outerShadows`, `cache`…) explained with examples.
200
+ - **[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).
201
+ - **[Cache & preCache](https://snapdom.dev/docs/cache/)** — control caching between captures and preload resources.
269
202
 
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
- ```
203
+ ### API at a glance
284
204
 
285
- ### Shortcut methods
205
+ `snapdom(el, options?)` returns a reusable object (`toPng`, `toSvg`, `toCanvas`, `toBlob`, `toJpg`, `toWebp`, `download`, `url`). For single exports, use the shortcuts:
286
206
 
287
- | Method | Description |
207
+ | Method | Description |
288
208
  | ------------------------------ | --------------------------------- |
289
- | `snapdom.toImg(el, options?)` | Returns an SVG `HTMLImageElement` (deprecated) |
290
209
  | `snapdom.toSvg(el, options?)` | Returns an SVG `HTMLImageElement` |
291
- | `snapdom.toCanvas(el, options?)` | Returns a `Canvas` |
210
+ | `snapdom.toCanvas(el, options?)` | Returns a `Canvas` |
292
211
  | `snapdom.toBlob(el, options?)` | Returns an SVG or raster `Blob` |
293
212
  | `snapdom.toPng(el, options?)` | Returns a PNG image |
294
213
  | `snapdom.toJpg(el, options?)` | Returns a JPG image |
295
214
  | `snapdom.toWebp(el, options?)` | Returns a WebP image |
296
- | `snapdom.download(el, options?)` | Triggers a download |
297
-
298
- ### Exporter-specific options
215
+ | `snapdom.download(el, options?)` | Triggers a download |
299
216
 
300
- Some exporters accept a small set of **export-only options** in addition to the global capture options.
217
+ ### Options at a glance
301
218
 
302
- #### `download()`
219
+ All options are optional and can be passed to `snapdom(el, options)` or any shortcut method.
303
220
 
304
221
  | 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
- | `safariWarmupAttempts` | number | `3` | Safari only: iterations to prime font/decode (WebKit #219770). Use `1` if 3 causes lag |
361
- | `compress` | boolean | `true` | Downsample inlined raster images to their visible resolution. Faster raster captures + smaller SVG output, fidelity-neutral. Set `false` to embed images verbatim |
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
- ### compress
372
-
373
- Inlined raster images are embedded at their full natural resolution even when shown in a small box — pixels the output can never display, which only bloat the payload and slow rasterization. With `compress` (on by default) SnapDOM downsamples each oversized raster to (a touch below) the resolution actually visible (`display box × scale × dpr`), preserving aspect ratio and never upscaling. The source codec is kept (PNGs stay lossless), and because the image was already shown far smaller than its natural size the quality loss is imperceptible in practice. Images already at or below their visible size are left untouched. Covers `<img>` (incl. cloned canvas/video), non-repeating CSS `background-image`, and SVG `<image>`.
374
-
375
- The benefit depends on the export:
376
-
377
- - **Raster** (`toPng` / `toJpg` / `toCanvas` / `toBlob`): **much faster** — the browser decodes/composites small images instead of huge ones. Output size is essentially unchanged (the final bitmap is the same visible pixels).
378
- - **SVG** (`toSvg` / raw): **much smaller** output — the downsampled images are embedded in the SVG.
379
-
380
- It is a no-op for images already shown at (or below) their natural resolution, so the common case pays virtually nothing. Pass `compress: false` to embed images verbatim (e.g. if you intend to scale the SVG up later).
381
-
382
- ```js
383
- // default: images downsampled to visible resolution
384
- await snapdom.toPng(el);
385
-
386
- // keep images at full resolution
387
- await snapdom.toPng(el, { compress: false });
388
- ```
389
-
390
- ### Fallback image on `<img>` load failure
391
-
392
- 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).
393
-
394
- ```js
395
- // 1) Fixed URL fallback
396
- await snapdom.toSvg(element, {
397
- fallbackURL: '/images/fallback.png'
398
- });
399
-
400
- // 2) Dynamic placeholder via callback
401
- await snapdom.toSvg(element, {
402
- fallbackURL: ({ width: 300, height: 150 }) =>
403
- `https://placehold.co/${width}x${height}`
404
- });
405
-
406
- // 3) With proxy (if your fallback host has no CORS)
407
- await snapdom.toSvg(element, {
408
- fallbackURL: ({ width = 300, height = 150 }) =>
409
- `https://dummyimage.com/${width}x${height}/cccccc/666.png&text=img`,
410
- useProxy: 'https://proxy.corsfix.com/?'
411
- });
412
- ```
413
-
414
- Notes:
415
- - If the fallback image also fails to load, snapDOM replaces the `<img>` with a placeholder block preserving width/height.
416
- - Width/height used by the callback are gathered from the original element (dataset, style/attrs, etc.) when available.
417
-
418
-
419
- ### Dimensions (`scale`, `width`, `height`)
420
-
421
- * If `scale` is provided, it **takes precedence** over `width`/`height`.
422
- * If only `width` is provided, height scales proportionally (and vice versa).
423
- * Providing both `width` and `height` forces an exact size (may distort).
424
-
425
- ### Cross-Origin Images & Fonts (`useProxy`)
426
-
427
- 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`:
428
-
429
- ```js
430
- await snapdom.toPng(el, {
431
- useProxy: 'https://proxy.corsfix.com/?' // Note: Any cors proxy could be used 'https://proxy.corsfix.com/?'
432
- });
433
- ```
434
-
435
-
436
- * The proxy is only used as a **fallback**; same-origin and CORS-enabled assets skip it.
437
-
438
- ### Fonts
439
-
440
- #### `embedFonts`
441
- 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**.
442
-
443
- #### `localFonts`
444
- If you serve fonts yourself or have data URLs, you can declare them here to avoid extra CSS discovery:
445
-
446
- ```js
447
- await snapdom.toPng(el, {
448
- embedFonts: true,
449
- localFonts: [
450
- { family: 'Inter', src: '/fonts/Inter-Variable.woff2', weight: 400, style: 'normal' },
451
- { family: 'Inter', src: '/fonts/Inter-Italic.woff2', style: 'italic' }
452
- ]
453
- });
454
- ```
455
-
456
- #### `iconFonts`
457
- Add custom icon families (names or regex matchers). Useful for private icon sets:
458
-
459
- ```js
460
- await snapdom.toPng(el, {
461
- iconFonts: ['MyIcons', /^(Remix|Feather) Icons?$/i]
462
- });
463
- ```
464
-
465
- #### `excludeFonts`
466
- Skip specific non-icon fonts to speed up capture or avoid unnecessary downloads.
467
-
468
- ```js
469
- await snapdom.toPng(el, {
470
- embedFonts: true,
471
- excludeFonts: {
472
- families: ['Noto Serif', 'SomeHeavyFont'], // skip by family name
473
- domains: ['fonts.gstatic.com', 'cdn.example'], // skip by source host
474
- subsets: ['cyrillic-ext'] // skip by unicode-range subset tag
475
- }
476
- });
477
- ```
478
- *Notes*
479
- - `excludeFonts` only applies to **non-icon** fonts. Icon fonts are always embedded.
480
- - Matching is case-insensitive for `families`. Hosts are matched by substring against the resolved URL.
481
-
482
-
483
- #### Filtering nodes: `exclude` vs `filter`
484
-
485
- * `exclude`: remove by **selector**.
486
- * `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.
487
- * `filter`: advanced predicate per element (return `false` to drop).
488
- * `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.
489
-
490
- **Example: filter out elements with `display:none`:**
491
- ```js
492
- /**
493
- * Example filter: skip elements with display:none
494
- * @param {Element} el
495
- * @returns {boolean} true = keep, false = exclude
496
- */
497
- function filterHidden(el) {
498
- const cs = window.getComputedStyle(el);
499
- if (cs.display === 'none') return false;
500
- return true;
501
- }
502
-
503
- await snapdom.toPng(document.body, { filter: filterHidden });
504
- ```
505
-
506
- **Example with `exclude`:** remove banners or tooltips by selector
507
- ```js
508
- await snapdom.toPng(el, {
509
- exclude: ['.cookie-banner', '.tooltip', '[data-test="debug"]']
510
- });
511
- ```
512
-
513
- ### outerTransforms
514
-
515
- 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.
516
-
517
- - **`outerTransforms: true (default)`**
518
- **Keeps the original `transforms` and `rotate`**.
519
-
520
-
521
- ### outerShadows
522
- - **`outerShadows: false (default)`**
523
- 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.
524
-
525
- > 💡 **Tip:** Using both (`outerTransforms: false` + `outerShadows: false`) produces a strict, minimal bounding box with no visual bleed.
526
-
527
- **Example**
528
-
529
- ```js
530
- // outerTransforms and remove shadow bleed
531
- await snapdom.toSvg(el, { outerTransforms: true, outerShadows: true });
532
- ```
533
-
534
- ## Cache control
535
-
536
- SnapDOM maintains internal caches for images, backgrounds, resources, styles, and fonts.
537
- You can control how they are cleared between captures using the `cache` option:
538
-
539
- | Mode | Description |
540
- | ----------- | --------------------------------------------------------------------------- |
541
- | `"disabled"`| No cache |
542
- | `"soft"` | Clears session caches (`styleMap`, `nodeMap`, `styleCache`) _(default)_ |
543
- | `"auto"` | Minimal cleanup: only clears transient maps |
544
- | `"full"` | Keeps all caches (nothing is cleared, maximum performance) |
545
-
546
- **Examples:**
547
-
548
- ```js
549
- // Use minimal but fast cache
550
- await snapdom.toPng(el, { cache: 'auto' });
551
-
552
- // Keep everything in memory between captures
553
- await snapdom.toPng(el, { cache: 'full' });
554
-
555
- // Force a full cleanup on every capture
556
- await snapdom.toPng(el, { cache: 'disabled' });
557
- ```
558
-
559
- ## `preCache()` – Optional helper
560
-
561
- Preloads external resources to avoid first-capture stalls (helpful for big/complex trees).
562
-
563
- ```js
564
- import { preCache } from '@zumer/snapdom';
565
-
566
- await preCache({
567
- root: document.body,
568
- embedFonts: true,
569
- localFonts: [{ family: 'Inter', src: '/fonts/Inter.woff2', weight: 400 }],
570
- useProxy: 'https://proxy.corsfix.com/?'
571
- });
572
- ```
573
-
574
- ## Plugins (BETA)
575
-
576
- 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.
577
-
578
- A plugin is a simple object with a unique `name` and one or more lifecycle **hooks**.
579
- Hooks can be synchronous or `async`, and they receive a shared **`context`** object.
580
-
581
- ### Official Plugins
582
-
583
- Install the official plugin package:
584
-
585
- ```bash
586
- npm install @zumer/snapdom-plugins
587
- ```
588
-
589
- ```js
590
- import { filter } from '@zumer/snapdom-plugins/filter';
591
- import { timestampOverlay } from '@zumer/snapdom-plugins/timestamp-overlay';
592
- ```
593
-
594
- | Plugin | Category | Description |
595
- |--------|----------|-------------|
596
- | `picture-resolver` | Capture | Resolves lazy-loaded `<picture>` placeholders. Detects base64 stubs and fetches the real image before capture. |
597
- | `timestamp-overlay` | Transform | Adds a configurable timestamp label on the captured clone. Supports multiple date formats and positions. |
598
- | `filter` | Transform | Applies CSS filter effects to captures. Ships with presets: `grayscale`, `sepia`, `blur`, `vintage`, `dramatic`. |
599
- | `replace-text` | Transform | Find-and-replace text in the captured clone. Supports strings and regex patterns. |
600
- | `color-tint` | Transform | Tints the entire capture to a specified color using an overlay with `mix-blend-mode`. |
601
- | `ascii-export` | Export | Adds a `toAscii()` method that converts captures to ASCII art. Configurable width, charset, and luminance. |
602
- | `pdf-image` | Export | Exports the capture as a PNG embedded in a downloadable PDF. Supports portrait and landscape orientations. |
603
- | `html-in-canvas` | Export | Uses the experimental WICG `drawElementImage` API for direct DOM-to-canvas rendering where supported. |
604
- | `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. |
605
-
606
- ### Community Plugins
607
-
608
- 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).
609
-
610
- ### Build a Plugin in 5 Minutes
611
-
612
- SnapDOM's hook system gives you full control over every stage of the capture pipeline:
613
-
614
- 1. **Clone the template** — `npx degit zumerlab/snapdom/packages/plugin-template my-plugin`
615
- 2. **Write your hook logic** — `export function myPlugin() {}`
616
- 3. **Get listed** — open a PR adding one line to `community-plugins.md`
617
-
618
- See [PLUGIN_SPEC.md](PLUGIN_SPEC.md) for the full specification and [CONTRIBUTING_PLUGINS.md](CONTRIBUTING_PLUGINS.md) for submission guidelines.
619
-
620
- ### Registering Plugins
621
-
622
- **Global registration** (applies to all captures):
623
-
624
- ```js
625
- import { snapdom } from '@zumer/snapdom';
626
-
627
- // You can register instances, factories, or [factory, options]
628
- snapdom.plugins(
629
- myPluginInstance,
630
- [myPluginFactory, { optionA: true }],
631
- { plugin: anotherFactory, options: { level: 2 } }
632
- );
633
- ```
634
-
635
- **Per-capture registration** (only for that specific call):
636
-
637
- ```js
638
- const out = await snapdom(element, {
639
- plugins: [
640
- [overlayFilterPlugin, { color: 'rgba(0,0,0,0.25)' }],
641
- [myFullPlugin, { providePdf: true }]
642
- ]
643
- });
644
- ```
645
-
646
- * **Execution order = registration order** (first registered, first executed).
647
- * **Per-capture plugins** run **before** global ones.
648
- * Duplicates are automatically skipped by `name`; a per-capture plugin with the same `name` overrides its global version.
649
-
650
- ### Plugin Lifecycle Hooks
651
-
652
- Hooks run in capture order (see [Capture Flow](#capture-flow)):
653
-
654
- | Hook | Stage | Purpose |
655
- |------|-------|---------|
656
- | `beforeSnap` | Start | Adjust options before any work. |
657
- | `beforeClone` | Pre-clone | Before DOM clone (modify live DOM carefully). |
658
- | `afterClone` | Post-clone | Modify cloned tree safely (e.g. inject overlay). |
659
- | `beforeRender` | Pre-serialize | Right before SVG → data URL. |
660
- | `afterRender` | Post-serialize | Inspect `context.svgString` / `context.dataURL`. |
661
- | `beforeExport` | Per export | Before each `toPng`, `toSvg`, etc. |
662
- | `afterExport` | Per export | Transform returned result. |
663
- | `afterSnap` | Once | After first export; cleanup. |
664
- | `defineExports` | Setup | Add custom exporters (e.g. `toPdf`). |
665
-
666
- > Returned values from `afterExport` are chained to the next plugin (transform pipeline).
667
-
668
- ### Context Object
669
-
670
- Every hook receives a single `context` object that contains normalized capture state:
671
-
672
- * **Input & options:**
673
- `element`, `debug`, `fast`, `scale`, `dpr`, `width`, `height`, `backgroundColor`, `quality`, `useProxy`, `cache`, `outerTransforms`, `outerShadows`, `safariWarmupAttempts`, `compress`, `embedFonts`, `localFonts`, `iconFonts`, `excludeFonts`, `exclude`, `excludeMode`, `filter`, `filterMode`, `fallbackURL`.
674
-
675
- * **Intermediate values (depending on stage):**
676
- `clone`, `classCSS`, `styleCache`, `fontsCSS`, `baseCSS`, `svgString`, `dataURL`.
677
-
678
- * **During export:**
679
- `context.export = { type, options, url }`
680
- where `type` is the exporter name (`"png"`, `"jpeg"`, `"svg"`, `"blob"`, etc.), and `url` is the serialized SVG base.
681
-
682
- > 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.
683
-
684
-
685
- ## Custom Exports via Plugins
686
-
687
- Plugins can add new exports using `defineExports(context)`.
688
- For each export key you return (e.g., `"pdf"`), SnapDOM automatically exposes a helper method named **`toPdf()`** on the capture result.
689
-
690
- **Register the plugin (global or per capture):**
691
-
692
- ```js
693
- import { snapdom } from '@zumer/snapdom';
694
-
695
- // global
696
- snapdom.plugins(pdfExportPlugin());
697
-
698
- // or per capture
699
- const out = await snapdom(element, { plugins: [pdfExportPlugin()] });
700
- ```
701
-
702
- **Call the custom export:**
703
-
704
- ```js
705
- const out = await snapdom(document.querySelector('#report'));
706
-
707
- // because the plugin returns { pdf: async (ctx, opts) => ... }
708
- const pdfBlob = await out.toPdf({
709
- // exporter-specific options (width, height, quality, filename, etc.)
710
- });
711
- ```
712
-
713
- ### Example: Overlay Filter Plugin
714
-
715
- Adds a translucent overlay or color filter **only** to the captured clone (not your live DOM).
716
- Useful for highlighting or dimming sections before export.
717
-
718
- ```js
719
- /**
720
- * Ultra-simple overlay filter for SnapDOM (HTML-only).
721
- * Inserts a full-size <div> overlay on the cloned root.
722
- *
723
- * @param {{ color?: string; blur?: number }} [options]
724
- * color: overlay color (rgba/hex/hsl). Default: 'rgba(0,0,0,0.25)'
725
- * blur: optional blur in px (default: 0)
726
- */
727
- export function overlayFilterPlugin(options = {}) {
728
- const color = options.color ?? 'rgba(0,0,0,0.25)';
729
- const blur = Math.max(0, options.blur ?? 0);
730
-
731
- return {
732
- name: 'overlay-filter',
733
-
734
- /**
735
- * Add a full-coverage overlay to the cloned HTML root.
736
- * @param {any} context
737
- */
738
- async afterClone(context) {
739
- const root = context.clone;
740
- if (!(root instanceof HTMLElement)) return; // HTML-only
741
-
742
- // Ensure containing block so absolute overlay anchors to the root
743
- if (getComputedStyle(root).position === 'static') {
744
- root.style.position = 'relative';
745
- }
746
-
747
- const overlay = document.createElement('div');
748
- overlay.style.position = 'absolute';
749
- overlay.style.left = '0';
750
- overlay.style.top = '0';
751
- overlay.style.right = '0';
752
- overlay.style.bottom = '0';
753
- overlay.style.background = color;
754
- overlay.style.pointerEvents = 'none';
755
- if (blur) overlay.style.filter = `blur(${blur}px)`;
756
-
757
- root.appendChild(overlay);
758
- }
759
- };
760
- }
761
-
762
- ```
763
-
764
- **Usage:**
765
-
766
- ```js
767
- import { snapdom } from '@zumer/snapdom';
768
-
769
- // Global registration
770
- snapdom.plugins([overlayFilterPlugin, { color: 'rgba(0,0,0,0.3)', blur: 2 }]);
771
-
772
- // Per-capture
773
- const out = await snapdom(document.querySelector('#card'), {
774
- plugins: [[overlayFilterPlugin, { color: 'rgba(255,200,0,0.15)' }]]
775
- });
776
-
777
- const png = await out.toPng();
778
- document.body.appendChild(png);
779
- ```
780
-
781
- > The overlay is injected **only in the cloned tree**, never in your live DOM, ensuring perfect fidelity and zero flicker.
782
-
783
-
784
- ### Full Plugin Template
785
-
786
- Use this as a starting point for custom logic or exporters.
787
-
788
- ```js
789
- export function myPlugin(options = {}) {
790
- return {
791
- /** Unique name used for de-duplication/overrides */
792
- name: 'my-plugin',
793
-
794
- /** Early adjustments before any clone/style work. */
795
- async beforeSnap(context) {},
796
-
797
- /** Before subtree cloning (use sparingly if touching the live DOM). */
798
- async beforeClone(context) {},
799
-
800
- /** After subtree cloning (safe to modify the cloned tree). */
801
- async afterClone(context) {},
802
-
803
- /** Right before serialization (SVG/dataURL). */
804
- async beforeRender(context) {},
805
-
806
- /** After serialization; inspect context.svgString/context.dataURL if needed. */
807
- async afterRender(context) {},
808
-
809
- /** Before EACH export call (toPng/toSvg/toBlob/...). */
810
- async beforeExport(context) {},
811
-
812
- /**
813
- * After EACH export call.
814
- * If you return a value, it becomes the result for the next plugin (chaining).
815
- */
816
- async afterExport(context, result) { return result; },
817
-
818
- /**
819
- * Define custom exporters (auto-added as helpers like out.toPdf()).
820
- * Return a map { [key: string]: (ctx:any, opts:any) => Promise<any> }.
821
- */
822
- async defineExports(context) { return {}; },
823
-
824
- /** Runs ONCE after the FIRST export finishes (cleanup). */
825
- async afterSnap(context) {}
826
- };
827
- }
828
- ```
829
-
830
- **Quick recap:**
831
-
832
- * Plugins can modify capture behavior (`beforeSnap`, `afterClone`, etc.).
833
- * You can inject visuals or transformations safely into the cloned tree.
834
- * New exporters defined in `defineExports()` automatically become helpers like `out.toPdf()`.
835
- * All hooks can be asynchronous, run in order, and share the same `context`.
836
-
222
+ | ------ | ---- | ------- | ----------- |
223
+ | `scale` | `number` | `1` | Output scale multiplier |
224
+ | `dpr` | `number` | `devicePixelRatio` | Pixel density of the rasterized output |
225
+ | `width` / `height` | `number` | `null` | Target output size (keeps aspect ratio if only one is set) |
226
+ | `backgroundColor` | `string` | `null` (`#ffffff` for JPEG/WebP) | Background fill |
227
+ | `quality` | `number` | `0.92` | JPEG/WebP quality (0–1) |
228
+ | `format` | `'png' \| 'jpeg' \| 'webp' \| 'svg'` | `'png'` | Format for `download()` |
229
+ | `type` | `string` | `'svg'` | Blob type for `toBlob()` (`'png'`, `'jpeg'`…) |
230
+ | `filename` | `string` | `'snapDOM'` | Download filename |
231
+ | `embedFonts` | `boolean` | `false` | Inline `@font-face` so text renders with your real fonts |
232
+ | `iconFonts` | `string \| RegExp \| array` | `[]` | Icon font families (always embedded) |
233
+ | `localFonts` | `array` | `[]` | Explicit fonts: `{ family, src, weight?, style? }` |
234
+ | `excludeFonts` | `object` | — | Skip fonts by family / domain / subset |
235
+ | `exclude` | `string[]` | `[]` | CSS selectors to leave out of the capture |
236
+ | `filter` | `(el) => boolean` | `null` | Keep-predicate (return `false` to drop a node) |
237
+ | `excludeMode` / `filterMode` | `'hide' \| 'remove'` | `'hide'` | How excluded nodes are handled |
238
+ | `clip` | `'viewport' \| {x, y, width, height}` | `null` | Capture only a region; offscreen content is pruned |
239
+ | `compress` | `boolean` | `true` | Downsample inlined images to their visible resolution |
240
+ | `useProxy` | `string` | `''` | CORS proxy prefix for cross-origin images |
241
+ | `fallbackURL` | `string \| fn` | — | Fallback image for broken `<img>` |
242
+ | `cache` | `'soft' \| 'auto' \| 'full' \| 'disabled'` | `'soft'` | Cache policy between captures |
243
+ | `outerTransforms` | `boolean` | `true` | Keep root translate/rotate in the output |
244
+ | `outerShadows` | `boolean` | `false` | Expand bounds to include root shadows/blur/outline |
245
+ | `fast` | `boolean` | `true` | Skip idle delays for faster capture |
246
+ | `plugins` | `array` | — | Per-capture plugins (override globals by name) |
247
+
248
+ 📖 **[Full API & every option, explained with examples → snapdom.dev/docs](https://snapdom.dev/docs/)**
837
249
 
838
250
  ## Limitations
839
251
 
@@ -881,35 +293,6 @@ npm run test:benchmark
881
293
  ```
882
294
 
883
295
 
884
- ## Roadmap
885
-
886
- Planned improvements for future versions of SnapDOM:
887
-
888
- * [X] **Implement plugin system**
889
- SnapDOM will support external plugins to extend or override internal behavior (e.g. custom node transformers, exporters, or filters).
890
-
891
- * [ ] **Refactor to modular architecture**
892
- Internal logic will be split into smaller, focused modules to improve maintainability and code reuse.
893
-
894
- * [X] **Decouple internal logic from global options**
895
- 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)
896
-
897
- * [X] **Expose cache control**
898
- Users will be able to manually clear image and font caches or configure their own caching strategies.
899
-
900
- * [X] **Auto font preloading**
901
- Required fonts will be automatically detected and preloaded before capture, reducing the need for manual `preCache()` calls.
902
-
903
- * [X] **Document plugin development**
904
- A full guide will be provided for creating and registering custom SnapDOM plugins.
905
-
906
- * [ ] **Make export utilities tree-shakeable**
907
- Export functions like `toPng`, `toJpg`, `toBlob`, etc. will be restructured into independent modules to support tree shaking and minimal builds.
908
-
909
- Have ideas or feature requests?
910
- Feel free to share suggestions or feedback in [GitHub Discussions](https://github.com/zumerlab/snapdom/discussions).
911
-
912
-
913
296
  ## Development
914
297
 
915
298
  **Source layout:**
@@ -952,6 +335,7 @@ For detailed guidelines, see [CONTRIBUTING](https://github.com/zumerlab/snapdom/
952
335
  <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>
953
336
  <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>
954
337
  <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>
338
+ <a href="https://github.com/mosuzi" title="mosuzi"><img src="https://avatars.githubusercontent.com/u/43341701?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="mosuzi"/></a>
955
339
  <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>
956
340
  <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>
957
341
  <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>
@@ -967,9 +351,8 @@ For detailed guidelines, see [CONTRIBUTING](https://github.com/zumerlab/snapdom/
967
351
  <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>
968
352
  <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>
969
353
  <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>
354
+ <a href="https://github.com/adajoy" title="adajoy"><img src="https://avatars.githubusercontent.com/u/26210079?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="adajoy"/></a>
970
355
  <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>
971
- <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>
972
- <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>
973
356
  </p>
974
357
  <!-- CONTRIBUTORS:END -->
975
358
 
@@ -979,9 +362,34 @@ Special thanks to [@megaphonecolin](https://github.com/megaphonecolin), [@sdrape
979
362
 
980
363
  If you'd like to support this project too, you can [become a sponsor](https://github.com/sponsors/tinchox5).
981
364
 
982
- ## Star History
365
+ ## Show your support
366
+
367
+ If SnapDOM saved you time, a ⭐ on GitHub helps other developers find it — that's the whole ask.
368
+
369
+ Shipping something built with SnapDOM? Add the badge to your README:
370
+
371
+ [![Built with SnapDOM](https://img.shields.io/badge/built%20with-SnapDOM-blue)](https://snapdom.dev)
372
+
373
+ ```md
374
+ [![Built with SnapDOM](https://img.shields.io/badge/built%20with-SnapDOM-blue)](https://snapdom.dev)
375
+ ```
376
+
377
+ ### Projects using SnapDOM
378
+
379
+ SnapDOM runs in production across 290+ public repositories ([GitHub dependents graph](https://github.com/zumerlab/snapdom/network/dependents)). A few notable ones, each verified from its own `package.json`:
380
+
381
+ - [LobeHub](https://github.com/lobehub/lobehub) — platform for operating AI agents
382
+ - [Trilium Notes](https://github.com/TriliumNext/Trilium) — hierarchical personal knowledge base
383
+ - [Sealos](https://github.com/labring/sealos) — AI-native cloud operating system
384
+ - [Tencent tmagic-editor](https://github.com/Tencent/tmagic-editor) — low-code page editor
385
+ - [Playroom](https://github.com/seek-oss/playroom) — JSX design tool by SEEK
386
+ - [GPT-Vis](https://github.com/antvis/GPT-Vis) — AI-friendly data viz by Ant Group's AntV
387
+ - [Rabby Wallet](https://github.com/RabbyHub/Rabby) — browser wallet for EVM chains
388
+ - [uMap](https://github.com/umap-project/umap) — OpenStreetMap map builder
389
+ - [ListenBrainz](https://github.com/metabrainz/listenbrainz-server) — music tracker by MetaBrainz
390
+ - [SnapDIFF](https://zumerlab.com/snapdiff/) — in-browser visual regression testing *(by Zumerlab)*
983
391
 
984
- [![Star History Chart](https://api.star-history.com/svg?repos=zumerlab/snapdom&type=Date)](https://www.star-history.com/#zumerlab/snapdom&Date)
392
+ 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.
985
393
 
986
394
  ## License
987
395