snapdom

zumerlabMIT活跃维护

把 DOM 元素高性能截取并导出成 PNG、SVG、GIF 等多种格式的浏览器端引擎,可替代 html2canvas 做页面截图。

High-performance engine for capturing, modifying, and converting DOM elements into any format.

v3.7.3发布于 2026-11-09 18:41 UTC8,233star +4 / 7 天311fork同步于 2026-11-10 11:31 UTC

怎么上手

由 README 提炼,执行前请确认
npm i @zumer/snapdom
硬件要求按 README 说明

README 未说明;纯浏览器端运行,无核心依赖,未提及 GPU 或显存要求

下载:源码 zip·GitHub Releases(安装包)本站不镜像安装包

项目说明

由 README 自动翻译提炼

在浏览器里把渲染好的 DOM 元素抓取并导出成 PNG、SVG、canvas 等多种格式的高性能引擎

把 DOM 元素高性能截取并导出成 PNG、SVG、GIF 等多种格式的浏览器端引擎,可替代 html2canvas 做页面截图。

核心能力

  • 一次抓取后复用结果导出 PNG、JPG、WebP、SVG、canvas 或 Blob
  • 未变更元素复用上次抓取结果以加速重复截图
  • 长页面自动分块渲染编码,突破浏览器 canvas 尺寸上限
  • 插件体系扩展出 HTML、PDF、可编辑矢量、GIF、视频等输出
  • 支持 exclude/filter 选择器排除或过滤节点并保留布局

容易踩的坑

插件 4.x 要求 SnapDOM 3.3.0 及以上(限 3.x 内);没有 CommonJS 构建,只能用 ESM 或 script 标签;PNG 长图导出依赖 CompressionStream,旧浏览器会降级缩放并告警;PDF、矢量、HTML 等输出需额外安装 @zumer/snapdom-plugins

内容由模型根据 README 生成,可能不完整或不准确,以官方文档为准。看原文 README

最近发版

5 个版本v3.7.3
  1. v3.7.3最新正式版2026-11-09 · 2026-11-09 18:41 UTC
  2. v3.7.22026-11-08 · 2026-11-08 19:25 UTC
    查看更新说明
  3. v3.7.12026-11-03 · 2026-11-03 20:18 UTC
    查看更新说明

    What's Changed

    New Contributors

    Full Changelog: https://github.com/zumerlab/snapdom/compare/v3.7.0...v3.7.1

  4. v3.7.02026-10-29 · 2026-10-29 20:14 UTC
    查看更新说明
  5. v3.6.02026-10-28 · 2026-10-28 22:44 UTC
    查看更新说明

    Features

    • engine: ship html-in-canvas in the default build, with a guide 8294802
    • stream: add animations: false to stop following running animations 932df5e

    Full Changelog: https://github.com/zumerlab/snapdom/compare/v3.5.0...v3.6.0

用户点评

登录用户的真实使用记录,可按版本和硬件筛选
共 0 条

    还没有人点评。第一个说出真实体验的人最有价值。

    README 原文

    未翻译

    SnapDOM

    SnapDOM is a high-performance DOM engine that captures a rendered element in the browser and exports it as PNG, SVG or canvas. Plugins turn the same capture into self-contained HTML, PDF, editable vectors, GIF, video or context for agents. Everything runs in the page, with no core dependencies.

    Documentation and demos · Technical features · Official plugins · 简体中文

    Why SnapDOM

    • Speed. Run the live benchmark in your browser: first and repeat captures against html2canvas and the other libraries, sorted by measured time. Unchanged elements reuse their last capture.
    • Fidelity. The output matches what the browser painted. Every release runs a pixel suite of real demo pages before it ships.
    • Flexibility. Capture once, export many formats. Official plugins add outputs, and the same plugin hooks let you write your own.
    • Maintenance. About one release a week over the past year, MIT licensed, with a public changelog.

    Quick start

    npm i @zumer/snapdom
    
    import { snapdom } from '@zumer/snapdom';
    
    const card = document.querySelector('#card');
    const image = await snapdom.toPng(card);
    document.body.appendChild(image);
    

    Capture once when you need several outputs:

    const result = await snapdom(card);
    
    const image = await result.toPng();
    const canvas = await result.toCanvas();
    const blob = await result.toBlob({ format: 'png' });
    await result.download({ format: 'jpg', filename: 'card' });
    

    The result keeps that capture even if the source element later changes. Call snapdom(card) again to capture its new state.

    What you can build

    UseOutputProvided by
    Share a card, chart, invoice or dashboardSVG, PNG, JPG, WebP, canvas or BlobCore
    Reuse a capture in a texture, overlay or transitionCanvas plus capture geometryCore
    Save a page fragment for later displayHTML with captured styles and fontshtml-export plugin
    Give an agent or a log a view of page contentText/JSON context, or an image with an element mapcontext-export / agent-map plugins
    Download a document or record changing contentSearchable, paginated PDF, animated GIF or browser-encoded videopdf / gif-export / video-export plugins
    Export editable artwork or paste into FigmaNative SVG shapes and text, or a Figma clipboard payloadvector plugin

    Image, HTML and context exports use the captured state. GIF and video plugins record the live element over time.

    Installation

    Install the core and, when you need them, the official plugins:

    npm i @zumer/snapdom@latest @zumer/snapdom-plugins@latest
    

    Plugins 4.x require SnapDOM 3.3.0 or newer within 3.x. They include the PDF and Vector exporters, MIT licensed like the core. Their sources live in packages/plugins/.

    Or load it in a browser:

    <script src="https://unpkg.com/@zumer/snapdom@latest/dist/snapdom.js"></script>
    <script>
      snapdom.toPng(document.querySelector('#card')).then(image => {
        document.body.appendChild(image);
      });
    </script>
    

    As an ES module from a CDN:

    import { snapdom } from 'https://esm.sh/@zumer/snapdom@latest';
    import { htmlExport } from 'https://esm.sh/@zumer/snapdom-plugins@latest/html-export';
    

    https://unpkg.com/@zumer/snapdom@latest/dist/snapdom.mjs serves the same module. These examples load the latest published core and plugins.

    Build outputs

    FileUse
    dist/snapdom.mjsES module for imports and bundlers
    dist/snapdom.jsScript tag exposing window.snapdom
    types/snapdom.d.tsTypeScript declarations

    There is no CommonJS build. @zumer/snapdom/plugins and the package root share the same runtime and plugin registry.

    Usage

    Choose an output

    Result methodReturns
    toPng(), toJpg(), toWebp()An HTMLImageElement
    toSvg()An SVG-backed HTMLImageElement
    toCanvas()An HTMLCanvasElement
    toBlob()An SVG Blob unless a format was explicitly set on the capture or export
    toRaw() / urlThe capture's SVG data URL
    download()Downloads the chosen format
    to(name, options?)Runs a core or plugin exporter by name

    One-step shortcuts such as snapdom.toPng(element, options) capture and export in one call. Results also have toJpeg() as an alias for toJpg(). toImg() remains available; prefer toSvg() for an SVG image.

    Set size and exclude content

    const result = await snapdom(card, {
      width: 800,
      dpr: 1,
      backgroundColor: '#ffffff',
      exclude: '.capture-ignore',
      excludeMode: 'remove'
    });
    

    width and height define output size. If only one is set, the aspect ratio is preserved. scale applies when neither is set, and dpr multiplies the pixel dimensions. download() always uses DPR 1, so the file matches the element's CSS size; pass scale: devicePixelRatio for a file as sharp as the screen.

    Common optionDefaultPurpose
    scale / dpr1 / device pixel ratioOutput resolution
    width / heightUnsetOutput dimensions
    embedFonts'auto'Embed the web fonts the capture uses
    backgroundColorTransparent; white for JPG/WebPOutput background
    excludeNoneSelectors or predicates; true means exclude
    excludeMode'hide'Keep an invisible spacer, or use 'remove'
    filterNonePredicate; true keeps a node and false filters it out
    filterMode'hide'Independent layout mode for nodes rejected by filter
    clipUnsetCapture the viewport or a page-coordinate rectangle
    captureSelectionfalseInclude the user's text selection
    canvasUnsetReuse an existing canvas
    invalidatefalseRefresh after changes such as programmatic CSSOM edits
    fasttruefalse keeps the page responsive during long captures
    debugfalseLog fallbacks and errors to the console; without it SnapDOM stays quiet and result.warnings reports them

    All options include shadows, transforms, fonts, CORS, fallbacks and layout reconciliation.

    Export a long page

    With the default SVG engine, PNG and JPEG file exports automatically handle captures beyond the browser's canvas limits. Capture once, then download one full-resolution file or get its Blob:

    const capture = await snapdom(article);
    await capture.download({ format: 'png', scale: 2 });
    // Or upload/store the file yourself:
    const blob = await capture.toBlob({ format: 'png', scale: 2, dpr: 1 });
    // JPEG works the same way, up to 65,535px per side:
    await capture.download({ format: 'jpg', scale: 2, quality: 0.9 });
    

    SnapDOM renders the frozen capture in windows and encodes them into one PNG or JPEG. It preserves the requested dimensions, including shadows and padding, without allocating a canvas for the whole image. download() always uses DPR 1; toBlob() uses the capture's DPR unless you override it. Ordinary-sized exports use the native encoder.

    PNG requires CompressionStream; older browsers keep the existing downscale and warning, as does a JPEG past 65,535px per side, the format's limit. WebP cannot go past 16,383px per side. Very large files still take time and memory to encode, and the application opening them has its own image limits.

    Export a long page in pieces

    toCanvas(), toPng(), toJpg() and WebP exports still use a single canvas and scale down past SnapDOM's size guards (16,384px per side in Safari, 32,767px in Chrome/Firefox, plus an area limit). When you need separate canvases, use crop:

    const capture = await snapdom(article);
    const { contentX, contentY, w0, h0 } = capture.meta;
    const pieceHeight = 4000; // CSS px
    
    for (let y = 0; y < h0; y += pieceHeight) {
      const canvas = await capture.toCanvas({
        scale: 2,
        dpr: 1,
        crop: { x: contentX, y: contentY + y, width: w0, height: Math.min(pieceHeight, h0 - y) }
      });
      // save or upload this piece, then move on
    }
    

    crop is a rectangle, not a switch. It is in the coordinates of capture.meta, and each call returns one canvas. The capture runs once; only the rasterization repeats. Keep pieceHeight × scale × dpr under the limit, and set dpr yourself, since it defaults to the device pixel ratio. How tiling works.

    Export HTML or structured context

    import { htmlExport, contextExport } from '@zumer/snapdom-plugins';
    
    const result = await snapdom(card, {
      plugins: [htmlExport(), contextExport({ format: 'json' })]
    });
    
    const html = await result.toHtml();
    const context = await result.toContext();
    

    The same plugin system supports overlays, redaction and custom exporters. Local plugins override global plugins by name. See the official plugin reference and plugin specification.

    Export PDF and editable vectors

    PDF returns a Blob with searchable, selectable text, links, pagination and optional form fields. Vector returns an SVG string without <foreignObject>, preserving supported shapes and editable text.

    import { snapdom } from '@zumer/snapdom';
    import { pdf } from '@zumer/snapdom-plugins/pdf';
    import { vector } from '@zumer/snapdom-plugins/vector';
    
    const documentCapture = await snapdom(element, { plugins: [pdf()] });
    const blob = await documentCapture.toPdf({ page: 'a4', margin: 36 });
    
    const artwork = await snapdom(element, { plugins: [vector()] });
    const svg = await artwork.toVector();
    copyButton.addEventListener('click', () => artwork.toFigma());
    

    Pass PDF capture settings to pdf() and paper, margin and download settings to toPdf(). toFigma() returns Promise<void> and needs the async clipboard API on HTTPS or localhost, called from a user action. Vector requires a connected source element; unsupported paint is diagnosed by the engine. See the PDF reference and Vector contract.

    Capture HTML strings

    const result = await snapdom.fromString('<article>Hello</article>');
    const image = await result.toPng();
    

    Sanitize untrusted HTML first, with DOMPurify or equivalent. The string is parsed and activated like markup you wrote yourself: inline handlers such as <img onerror> run in the caller's origin, and can keep running after the mount is removed. fromString() mounts the markup offscreen and removes it after capture.

    Feed a WebGL texture

    const canvas = document.createElement('canvas');
    const texture = new THREE.CanvasTexture(canvas);
    texture.colorSpace = THREE.SRGBColorSpace;
    
    async function refresh(element) {
      await snapdom.toCanvas(element, { canvas, scale: 1, dpr: 1 });
      texture.needsUpdate = true;
    }
    

    result.meta contains the capture geometry needed to place an exported image over the source interface. The labs show textures, mirrors and transitions.

    Limitations

    • SnapDOM needs a browser DOM. A server-side Node.js process needs a browser environment to run it.
    • Cross-origin images, fonts and stylesheets need readable resources or an appropriate proxy. crossorigin does not grant access unless the server also allows it. Cross-origin iframes use placeholders.
    • SVG output includes HTML inside <foreignObject>. It is suitable for browsers; support varies in other SVG viewers and document tools.
    • Output depends on browser rendering and image limits; for tall PNG files see Export a long page. Safari may fall back to PNG when WebP encoding is unavailable.
    • Canvas, video and other changing surfaces are captured fresh. JavaScript CSSOM edits are not observable automatically; use invalidate: true after them.
    • Core captures visible input values. Semantic plugins redact sensitive field values in their text/map output, but their attached image needs redactInputs or exclude if you want those pixels hidden too.

    Technical features and browser behavior covers the details.

    What's new in v3

    • Eligible unchanged captures reuse the first result. Safe local changes rebuild only affected subtrees; other changes use a full capture.
    • Web fonts embed automatically when used. System-font captures skip that work.
    • The style pass avoids redundant reads, and per-capture state is isolated for concurrent captures.
    • Safari image decoding and drawing retain their browser-specific handling.
    • snapdom.preCapture() can prepare captures on user intent. It learns a control when a capture starts during its press/click event, then prepares that capture on later hover or focus.
    snapdom.preCapture();
    button.onclick = () => snapdom.toPng(card);
    

    Image compression and resource caching remain automatic. They do not make rasterization or image encoding free. See performance measurements and the cache guide.

    SnapDOM has two rendering engines: SVG, the default, and html-in-canvas, which paints the same captured clone through the browser's native canvas API. Select the second with engine: 'html-in-canvas'.

    The second engine ships in the default build and runs where the browser has the API: Chrome 157 and newer, without a flag. Firefox and Safari have no implementation, so there, and on any capture it cannot paint, SnapDOM runs the SVG engine on the same clone and you get the same result object. Captures of body or html, transformed or zoomed roots, clip, width/height and outerShadows use SVG, and so do captures with a plugin that has a beforeRender or afterRender hook (html-export, pdf, vector, and redact-inputs with blocks or attributes); every other official plugin runs on the native engine. A successful native capture produces a bitmap, so its URL and toRaw() return PNG, not serialized SVG. SVG stays the default because it is faster (the native paint waits for a rendering update, about 35 ms on a small card where SVG takes 3, see BENCHMARKS.md); pick html-in-canvas when scrollbars, or anything else the SVG image draws differently, have to match the page. Both engines paint the same clone, so <audio controls>, <progress> and <meter> look the same with either one, and not quite like the page. See ARCHITECTURE.md and the guide.

    Migrating from v2

    The main capture pattern remains snapdom(element, options). This guide compares v3 with v2.x.x, whose source remains available. Review these changes before upgrading:

    In v2In v3What to change
    Web fonts were opt-inembedFonts: 'auto'Usually nothing; use false only if you want to omit them
    Raster width/height could be multiplied by scaleWidth/height win over scalePass the final size: width: 400 instead of width: 200, scale: 2
    burst opted into repeat memoizationEligible captures memoize automatically; burst is no longer documented or supportedRemove burst (the engine still reads it for internal use, do not rely on it) and use invalidate: true for one fresh capture after unobservable changes such as sheet.insertRule()
    preCache prepared resourcesRemoved; preCapture() learns capture intentRemove preCache; preCapture() is not a drop-in rename
    fast: false cloned through idle callbacksfast: false pauses about every frame, at nearly the same total timeKeep fast: false
    filter / filterMode and exclude / excludeMode could be used togetherBoth controls and their independent modes remain supported; exclude also accepts predicatesKeep existing rules and modes; use the additional predicate form only when useful
    cache: 'auto' or 'full'Both map to 'soft'Usually omit it; 'disabled' / false is for debugging
    compress controlled embedded image downsamplingImage optimization is automatic; compress is no longer documented or supportedRemove compress; the engine still reads it for internal use, do not rely on it
    resolvePicturePlaceholders / pictureResolver configured lazy-image preparationResponsive/lazy image resolution happens on the clone; these options are no longer documented or supportedRemove the options and handle custom loading/timeouts in your app before capture; the engine still reads resolvePicturePlaceholders for internal use, do not rely on it
    Some visible input values were redactedCore masks passwords onlyAdd redactInputs() for other fields
    afterExport returns became the next hook's payload, not the caller's resultReturns are ignored; hooks receive the same export payloadStop chaining through return values; use defineExports to produce a different output
    Plugins v2.x.x exposed @zumer/snapdom-plugins/html-in-canvasThat subpath is removedUse the core option engine: 'html-in-canvas'; browsers without the API get SVG
    TypeScript exported PluginExportFacadeThe named type is removed; ctx.exports still provides core exportersInfer it in defineExports, or use NonNullable<CaptureContext['exports']>

    Use filter and exclude together

    filter and exclude are independent controls, as in v2. filter(node) returns true to keep a node and false to filter it out; filterMode controls how a filtered node affects layout. exclude specifies additional omissions using selectors or predicates; an exclusion predicate returns true to omit the node. excludeMode controls those omissions. You can keep both controls and different modes in the same capture:

    // Works in v2 and v3: hide private fields, remove the toolbar.
    await snapdom(card, {
      filter: node => !node.matches('[data-private]'),
      filterMode: 'hide',
      exclude: ['.toolbar'],
      excludeMode: 'remove'
    });
    

    'hide' keeps an invisible spacer; 'remove' drops the node and allows reflow. Both omit its content. V3 additionally lets exclude mix selectors and predicates, such as exclude: ['.toolbar', node => node.dataset.export === 'omit']; any matching rule excludes the node. This is optional and does not replace filter or merge the two modes. The separate CSS-effect plugin named filter is also available.

    Both modes default to 'hide'. Per node, data-capture="exclude" is checked first, then exclude, then filter. The first omission decides the mode and stops evaluation for that node: if it matches exclude, excludeMode wins even when filter would reject it with a different mode. filter uses the v2 truthiness rule: any falsy return filters the node out.

    Prefer selectors to callbacks in polling loops

    A function-valued filter, exclude, excludeStyleProps or fallbackURL turns off memoization and differential recapture, so a polling loop with a predicate pays a full capture on every tick. When the rule can be written as a selector, pass the selector and keep the reuse.

    Captures with function-valued filter, exclude, excludeStyleProps or fallbackURL run fresh so applicable callbacks can read current application state on each new capture. They do not reuse an unchanged capture or an earlier callback's style/fallback decision. You do not need invalidate just because a callback's closure changed:

    let privateMode = false;
    const options = {
      exclude: node => privateMode && node.matches('[data-private]'),
      excludeMode: 'remove'
    };
    const before = await snapdom(card, options);
    privateMode = true;
    const after = await snapdom(card, options); // evaluates the current policy
    

    before still contains its original captured state; exporting it again does not apply a new policy. Use a new capture such as after. Keep exclusion and style predicates synchronous and boolean-returning. invalidate: true remains necessary after unobservable changes such as direct CSSOM edits.

    Capture-affecting plugins suspend memoization unless they declare pure: true. Declare it only for deterministic hooks; timestamps and callbacks reading external state must run again. See the v3 plugin contract.

    Performance benchmarks

    Recorded measurements separate first captures, repeat captures and image-heavy scenes. The live comparison runs in your browser and identifies the package version it loads.

    For a useful comparison, use the same scene, output format, scale and DPR. Compare the images as well as the times.

    Documentation

    Development

    From this checkout:

    npm install
    npx playwright install
    npm test            # Chromium; test:firefox, test:webkit, or test:full for all three
    

    Every test script runs npm run check (lint, types, release checks, bundle) and then the browser suite, visual demos included. Visual baselines live in __snapshots__/, which is not committed: on a fresh clone, record them once with node scripts/vitest.mjs run --browser.headless before relying on npm test. npm run site serves the docs with the local build. npm test never changes files; use npm run lint:fix to apply lint fixes. See ARCHITECTURE.md for implementation notes.

    Contributors

    Sponsors

    Special thanks to @megaphonecolin, @sdraper69, @reynaldichernando, @gamma-app, @jrjohnson, and @ryanander for supporting this project!

    If you'd like to support this project too, you can become a sponsor.

    Show your support

    If SnapDOM saved you time, a star on GitHub helps other developers find it.

    Shipping something built with SnapDOM? Add the badge to your README:

    Built with SnapDOM

    [![Built with SnapDOM](https://img.shields.io/badge/built%20with-SnapDOM-blue)](https://snapdom.dev)
    

    Projects using SnapDOM

    Projects using SnapDOM include:

    See the full gallery at snapdom.dev/made-with. Shipping SnapDOM? Open a PR to add your project — real, verifiable projects only.

    License

    MIT © Zumerlab