|
| 1 | +# SnapDOM — Features |
| 2 | + |
| 3 | +A complete technical overview of what **SnapDOM** captures, embeds and exports. SnapDOM serializes a DOM subtree into a self-contained SVG (via `<foreignObject>`) and rasterizes it to your target format — ultra-fast, dependency-free, and 100% based on standard Web APIs. |
| 4 | + |
| 5 | +> 📖 Full API, options and guides: **[snapdom.dev/docs](https://snapdom.dev/docs/)** |
| 6 | +> |
| 7 | +> 🌐 简体中文: **[FEATURES_CN.md](FEATURES_CN.md)** |
| 8 | +
|
| 9 | +## Table of Contents |
| 10 | + |
| 11 | +- [Capture & clone](#capture--clone) |
| 12 | +- [Styles](#styles) |
| 13 | +- [Images & backgrounds](#images--backgrounds) |
| 14 | +- [Fonts & icon fonts](#fonts--icon-fonts) |
| 15 | +- [Export formats](#export-formats) |
| 16 | +- [Options](#options) |
| 17 | +- [Plugin system](#plugin-system) |
| 18 | +- [Caching & preCache](#caching--precache) |
| 19 | +- [Cross-browser handling](#cross-browser-handling) |
| 20 | +- [Node-level control attributes](#node-level-control-attributes) |
| 21 | + |
| 22 | +## Capture & clone |
| 23 | + |
| 24 | +Deep node-by-node clone that snapshots the computed style of every node, preserving what the browser actually renders. |
| 25 | + |
| 26 | +- **Shadow DOM** — traverses `shadowRoot`, extracts and scopes its CSS, seeds the required CSS custom properties, and resolves `<slot>` content via `assignedNodes({ flatten: true })` (slotted subtrees are marked to avoid double-cloning). |
| 27 | +- **Same-origin iframes** — rasterized inline (fonts read from the iframe's own document). Cross-origin iframes can't be read, so they render as a striped placeholder (or a hidden spacer when `placeholders` is off). |
| 28 | +- **`<canvas>`** — snapshotted to a PNG `<img>` (with Safari-safe retries), preserving intrinsic and CSS box size. |
| 29 | +- **`<video>`** — current frame drawn to an image; falls back to the `poster`; honors `object-fit: contain`. |
| 30 | +- **`<audio controls>`** — replaced with a drawn player mock sized to the element. |
| 31 | +- **Form control state** — `<input>` `value` / `checked` / `indeterminate`, `<textarea>` value, and `<select>` selection are preserved. State attributes (`disabled`, `required`, `readonly`, `min`, `max`, `pattern`, `aria-invalid`) are copied so `:disabled`, `:required`, `:read-only`, `:invalid` and `:in-range` styles render. `::placeholder` color is preserved. Firefox checkboxes/radios get a drawn replacement. |
| 32 | +- **`<img>`** — `srcset` is frozen, pre-transform dimensions recorded, px sizes frozen when the author used `%`/`auto`, and `object-fit` / `object-position` preserved. |
| 33 | +- **SVG** — paint properties (fill, stroke and its longhands, opacity variants, fill/clip rule, markers, visibility, display) copied as inline styles; external `<defs>` / `<symbol>` referenced by `<use>` are inlined so `var()` resolves at the use site. |
| 34 | +- **Scroll position** — scrolled containers are reproduced by translating their inner content and clipping overflow; fixed/absolute descendants are adjusted, and sticky headers/footers are frozen in place. |
| 35 | +- **Skipped by design** — `meta`, `script`, `noscript`, `title`, `link`, `template`, the SnapDOM sandbox, and nested `<foreignObject>`. |
| 36 | + |
| 37 | +Non-renderable content is handled gracefully: invalid XML control characters are stripped, `content-visibility` is forced visible so off-screen content is captured, and root margins are neutralized. |
| 38 | + |
| 39 | +## Styles |
| 40 | + |
| 41 | +- **Computed-style inlining** — every node's full computed style is snapshotted and deduplicated into generated CSS classes to keep output compact. Authored inline styles are replaced with computed values so stylesheet `!important` still wins. |
| 42 | +- **Preserved details** — text-decoration longhands (line/color/style/thickness, underline-offset, skip-ink), `-webkit-text-stroke` + `paint-order`, and (when embedding fonts) font-feature/variation/kerning/variant/optical-sizing settings. |
| 43 | +- **`counter()` / `counters()`** — a full CSS counter resolver (counter-reset with nesting, counter-increment, counter-set, and counter-style formatting), used in pseudo-element `content`. |
| 44 | +- **`-webkit-line-clamp` & `text-overflow: ellipsis`** — baked into real text (with `…`) because Firefox and Safari don't honor them inside `<foreignObject>`. |
| 45 | +- **Transforms** — base and individual `translate`/`rotate`/`scale` are read into a total matrix with origin-aware bounding-box math (see the `outerTransforms` option). |
| 46 | +- **Shadows, blur & outline bleed** — box-shadow, filter blur, drop-shadow and outline expand the viewBox when `outerShadows` is on; otherwise root shadows are visually stripped (see `outerShadows`). |
| 47 | +- **Masks, backgrounds & border-image** — see [Images & backgrounds](#images--backgrounds). |
| 48 | +- **Custom scrollbars** — `::-webkit-scrollbar` rules are injected so custom scrollbar styling appears. |
| 49 | +- **`excludeStyleProps`** — skip properties from the snapshot by RegExp or predicate (e.g. drop all CSS variables). |
| 50 | + |
| 51 | +## Images & backgrounds |
| 52 | + |
| 53 | +- **`<img>` inlining** — resolves `currentSrc`/`src`, fetches to a data URL, caches, and ensures dimensions (batched to respect HTTP/1.1 connection limits). SVG `<image href>` is inlined too. |
| 54 | +- **Backgrounds & masks** — inlines `url()` layers in `background-image` (and the `background` shorthand), `mask` / `-webkit-mask*`, and `border-image`, preserving multi-layer values and layout longhands (position, size, repeat, origin, clip, blend-mode, composite…). Supports `background-clip: text`. |
| 55 | +- **`<picture>` & lazy images** — resolves `<picture>` sources and common lazy attributes (`data-src`, `data-lazy-src`, `data-original`, `data-hi-res-src`, `data-srcset`, …) to real URLs before cloning. |
| 56 | +- **CORS / proxy** — a non-throwing fetch layer with in-flight deduplication, an error cache, timeouts, and inferred credentials. `useProxy` accepts flexible templates (`{url}`, `{urlRaw}`, `?url=` suffix, and more); already-proxied and `data:`/`blob:` URLs are skipped. |
| 57 | +- **Failure fallbacks** — a configurable `fallbackURL` (string or callback), then a placeholder box, then a hidden spacer. |
| 58 | +- **`compress`** — perceptual downsampling of inlined rasters to their visible resolution (display box × scale × dpr), preserving the source codec and never upscaling. On by default; set `compress: false` to embed verbatim. |
| 59 | +- **Decode-size guard** — SVG raster size is clamped to safe limits (max 16384px per side, ~268M px area) and downscaled with a warning if exceeded. |
| 60 | + |
| 61 | +## Fonts & icon fonts |
| 62 | + |
| 63 | +- **`@font-face` embedding** (`embedFonts`) — scans document (and iframe) stylesheets and embeds only the `@font-face` rules for the families/weights/styles/stretch **actually used**, intersected with the **used unicode codepoints**, keeping payloads small. Supports near-weight matching and synthetic-italic fallback. |
| 64 | +- **Icon fonts** — auto-detects Font Awesome, Material Icons / Symbols, Ionicons, Glyphicons, Feather, Bootstrap Icons, Remix, Heroicons, Layui and Lucide (plus a heuristic), and renders glyphs (including ligature icons) to images. Extend detection via the `iconFonts` option or `window.__SNAPDOM_ICON_FONTS__`. |
| 65 | +- **`localFonts`** — supply your own fonts as `{ family, src, weight?, style?, stretchPct? }` to fetch and embed. |
| 66 | +- **`excludeFonts`** — exclude by `{ families?, domains?, subsets? }`. |
| 67 | +- **Cross-origin stylesheets** — gated by `fontStylesheetDomains` (plus known math libraries like KaTeX/MathJax). |
| 68 | +- **`preCache`** — preloads images, background images and fonts before capture; defaults to `embedFonts: true` and `cache: 'full'`. |
| 69 | + |
| 70 | +## Export formats |
| 71 | + |
| 72 | +A `snapdom(el)` call returns a reusable result object; capture once, export many times. |
| 73 | + |
| 74 | +| Method | Returns | |
| 75 | +|---|---| |
| 76 | +| `toRaw()` | Raw `data:image/svg+xml` URL | |
| 77 | +| `toSvg()` / `toImg()` | SVG `HTMLImageElement` | |
| 78 | +| `toCanvas()` | `HTMLCanvasElement` | |
| 79 | +| `toBlob()` | `Blob` (SVG text blob or rasterized) | |
| 80 | +| `toPng()` | PNG image | |
| 81 | +| `toJpg()` | JPG image (white background) | |
| 82 | +| `toWebp()` | WebP image | |
| 83 | +| `download()` | Triggers a file download | |
| 84 | + |
| 85 | +The same methods exist as one-shot static shortcuts (`snapdom.toPng(el)`, `snapdom.download(el)`, …). Lossy formats (JPEG/WebP) auto-flatten transparency to white. Downloads use the Web Share API on iOS. Exports run through a serial per-session queue with `beforeExport` / `afterExport` / `afterSnap` hooks. |
| 86 | + |
| 87 | +## Options |
| 88 | + |
| 89 | +Defaults as normalized in `src/core/context.js`. |
| 90 | + |
| 91 | +| Option | Default | Behavior | |
| 92 | +|---|---|---| |
| 93 | +| `debug` | `false` | Debug warnings | |
| 94 | +| `fast` | `true` | Skip idle delay for speed | |
| 95 | +| `scale` | `1` | Output scale multiplier | |
| 96 | +| `exclude` | `[]` | CSS selectors to exclude | |
| 97 | +| `excludeMode` | `'hide'` | `'hide'` (spacer) or `'remove'` | |
| 98 | +| `filter` | `null` | Node predicate `(node) => boolean` | |
| 99 | +| `filterMode` | `'hide'` | `'hide'` or `'remove'` | |
| 100 | +| `placeholders` | `true` | Show placeholders for failed images / cross-origin iframes | |
| 101 | +| `embedFonts` | `false` | Embed matched `@font-face` | |
| 102 | +| `iconFonts` | `[]` | Extra icon-font names/regexes | |
| 103 | +| `localFonts` | `[]` | User font descriptors | |
| 104 | +| `excludeFonts` | `undefined` | `{ families, domains, subsets }` | |
| 105 | +| `fontStylesheetDomains` | `[]` | Extra cross-origin CSS domains | |
| 106 | +| `fallbackURL` | `undefined` | Fallback image URL or callback | |
| 107 | +| `cache` | `'soft'` | `disabled` / `soft` / `auto` / `full` | |
| 108 | +| `useProxy` | `''` | CORS proxy template/base | |
| 109 | +| `width` | `null` | Output width (aspect-preserving) | |
| 110 | +| `height` | `null` | Output height | |
| 111 | +| `format` | `'png'` | `png` / `jpg`→`jpeg` / `webp` / `svg` | |
| 112 | +| `type` | `'svg'` | Output type `svg` / `img` / `canvas` / `blob` | |
| 113 | +| `quality` | `0.92` | Lossy encode quality | |
| 114 | +| `dpr` | `devicePixelRatio \|\| 1` | Device pixel ratio | |
| 115 | +| `backgroundColor` | `null` (`#ffffff` for jpeg/webp) | Flatten background | |
| 116 | +| `filename` | `'snapDOM'` | Download filename base | |
| 117 | +| `outerTransforms` | `true` | Normalize root translate/rotate vs. expand bbox for transforms | |
| 118 | +| `outerShadows` | `false` | Strip root shadows vs. expand bleed for shadows/blur/outline | |
| 119 | +| `compress` | `true` | Perceptual raster downsampling | |
| 120 | +| `safariWarmupAttempts` | `3` | Safari warmup iterations (1–3) | |
| 121 | +| `excludeStyleProps` | `null` | RegExp/predicate to skip style props | |
| 122 | +| `resolvePicturePlaceholders` | `true` | Built-in `<picture>` / lazy resolver | |
| 123 | +| `pictureResolver` | `{}` | `{ timeout, concurrency, resolveLazySrc, silent }` | |
| 124 | +| `plugins` | — | Per-capture plugin list (local-first) | |
| 125 | + |
| 126 | +## Plugin system |
| 127 | + |
| 128 | +Plugins are plain objects with lifecycle hooks, registered globally (`snapdom.plugins(...)`, deduped by `name`) or per-capture (`{ plugins: [...] }`, where locals override globals by name). |
| 129 | + |
| 130 | +- **Hooks** (in order): `beforeSnap → beforeClone → afterClone → beforeRender → afterRender → beforeExport → afterExport → afterSnap`. |
| 131 | +- **Custom exporters** — a plugin's `defineExports` can add or override export formats; each becomes a `to<Name>()` helper on the result object and gets the same export pipeline as core formats. |
| 132 | +- **Accepted forms** — plain object, `[factory, options]`, `{ plugin, options }`, or a factory function. |
| 133 | + |
| 134 | +See [`PLUGIN_SPEC.md`](PLUGIN_SPEC.md) and [`CONTRIBUTING_PLUGINS.md`](CONTRIBUTING_PLUGINS.md). |
| 135 | + |
| 136 | +## Caching & preCache |
| 137 | + |
| 138 | +- **Buckets** — FIFO evicting maps for `image`, `background`, `resource`, `baseStyle` and `defaultStyle`; `WeakMap`s for computed styles and layout measurement hints; a `Set` for fonts; and a per-session bucket. |
| 139 | +- **Policies** (`cache` option): |
| 140 | + - `disabled` — clear all caches every capture. |
| 141 | + - `soft` (default) — reset session style/node maps, keep persistent caches. |
| 142 | + - `auto` — reset style/node maps only, keep the style cache too. |
| 143 | + - `full` — keep everything. |
| 144 | +- **Invalidation** — a MutationObserver on the DOM and `<head>` plus font `loadingdone`/`ready` events bump a style-snapshot epoch, so stale snapshots are dropped automatically. |
| 145 | +- **`preCache`** — warm the caches ahead of time (defaults to `cache: 'full'`). |
| 146 | + |
| 147 | +## Cross-browser handling |
| 148 | + |
| 149 | +- **Safari warmup** — works around [WebKit #219770](https://bugs.webkit.org/show_bug.cgi?id=219770) (the first canvas draw with an embedded-font SVG is blank) by running small pre-captures + `drawImage` to prime the pipeline when fonts are embedded or the element has background/mask/canvas content. Configurable via `safariWarmupAttempts`. |
| 150 | +- **Safari canvas** — box-shadow is rewritten to an SVG drop-shadow, and image compositing is awaited before drawing. |
| 151 | +- **Firefox** — checkboxes and radios get drawn replacements. |
| 152 | +- **iOS** — `download()` falls back to the Web Share API. |
| 153 | + |
| 154 | +## Node-level control attributes |
| 155 | + |
| 156 | +Fine-grained control directly in your markup: |
| 157 | + |
| 158 | +- `data-capture="exclude"` — drop this node (per `excludeMode`). |
| 159 | +- `data-capture="placeholder"` + `data-placeholder-text` — render a placeholder box instead of the node. |
| 160 | +- `data-snapdom-sandbox` / `#snapdom-sandbox` — skipped entirely. |
| 161 | + |
| 162 | +--- |
| 163 | + |
| 164 | +Want the full API and every option with examples? → **[snapdom.dev/docs](https://snapdom.dev/docs/)** |
0 commit comments