@@ -76,6 +76,13 @@ It captures any HTML element as a scalable SVG image, preserving styles, fonts,
7676 - [ noShadows] ( #no-shadows )
7777 - [ Cache control] ( #cache-control )
7878- [ preCache] ( #precache--optional-helper )
79+ - [ Plugins (BETA)] ( #plugins-beta )
80+ - [ Registering Plugins] ( #registering-plugins )
81+ - [ Plugin Lifecycle Hooks] ( #plugin-lifecycle-hooks )
82+ - [ Context Object] ( #context-object )
83+ - [ Custom Exports via Plugins] ( #custom-exports-via-plugins )
84+ - [ Example: Overlay Filter Plugin] ( #example-overlay-filter-plugin )
85+ - [ Full Plugin Template] ( #full-plugin-template )
7986- [ Limitations] ( #limitations )
8087- [ ⚡ Performance Benchmarks (Chromium)] ( #performance-benchmarks )
8188 - [ Simple elements] ( #simple-elements )
@@ -409,6 +416,229 @@ await preCache({
409416});
410417` ` `
411418
419+ ## Plugins (BETA)
420+
421+ 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.
422+
423+ A plugin is a simple object with a unique ` name` and one or more lifecycle **hooks**.
424+ Hooks can be synchronous or ` async ` , and they receive a shared **` context` ** object.
425+
426+ ### Registering Plugins
427+
428+ **Global registration** (applies to all captures):
429+
430+ ` ` ` js
431+ import { snapdom } from ' @zumer/snapdom' ;
432+
433+ // You can register instances, factories, or [factory, options]
434+ snapdom .plugins (
435+ myPluginInstance,
436+ [myPluginFactory, { optionA: true }],
437+ { plugin: anotherFactory, options: { level: 2 } }
438+ );
439+ ` ` `
440+
441+ **Per-capture registration** (only for that specific call):
442+
443+ ` ` ` js
444+ const out = await snapdom (element, {
445+ plugins: [
446+ [overlayFilterPlugin, { color: ' rgba(0,0,0,0.25)' }],
447+ [myFullPlugin, { providePdf: true }]
448+ ]
449+ });
450+ ` ` `
451+
452+ * **Execution order = registration order** (first registered, first executed).
453+ * **Per-capture plugins** run **before** global ones.
454+ * Duplicates are automatically skipped by ` name` ; a per-capture plugin with the same ` name` overrides its global version.
455+
456+ ### Plugin Lifecycle Hooks
457+
458+ | Hook | Purpose |
459+ | ------------------------------ | ------------------------------------------------------------------------------------ |
460+ | ` beforeSnap (context)` | Before any clone/style work. Ideal for adjusting global capture options. |
461+ | ` beforeClone (context)` | Before DOM cloning. Can modify live DOM (use carefully). |
462+ | ` afterClone (context)` | After the element subtree has been cloned. Safe to modify styles in the cloned tree. |
463+ | ` beforeRender (context)` | Right before SVG/dataURL serialization. |
464+ | ` afterRender (context)` | After serialization (you can inspect ` context .svgString ` or ` context .dataURL ` ). |
465+ | ` beforeExport (context)` | Before each export call (` toPng` , ` toSvg` , etc.). |
466+ | ` afterExport (context, result)` | After each export call — can transform the returned result. |
467+ | ` afterSnap (context)` | Runs **once**, after the **first export** finishes. Perfect for cleanup. |
468+ | ` defineExports (context)` | Returns a map of **custom exporters**, e.g. ` { pdf: async (ctx , opts ) => Blob }` . |
469+
470+ > Returned values from ` afterExport` are chained to the next plugin (transform pipeline).
471+
472+ ### Context Object
473+
474+ Every hook receives a single ` context` object that contains normalized capture state:
475+
476+ * **Input & options:**
477+ ` element` , ` debug` , ` fast` , ` scale` , ` dpr` , ` width` , ` height` , ` backgroundColor` , ` quality` , ` useProxy` , ` cache` , ` straighten` , ` noShadows` , ` embedFonts` , ` localFonts` , ` iconFonts` , ` excludeFonts` , ` exclude` , ` excludeMode` , ` filter` , ` filterMode` , ` fallbackURL` .
478+
479+ * **Intermediate values (depending on stage):**
480+ ` clone` , ` classCSS` , ` styleCache` , ` fontsCSS` , ` baseCSS` , ` svgString` , ` dataURL` .
481+
482+ * **During export:**
483+ ` context .export = { type, options, url }`
484+ where ` type` is the exporter name (` " png" ` , ` " jpeg" ` , ` " svg" ` , ` " blob" ` , etc.), and ` url` is the serialized SVG base.
485+
486+ > 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.
487+
488+
489+ ## Custom Exports via Plugins
490+
491+ Plugins can add new exports using ` defineExports (context)` .
492+ For each export key you return (e.g., ` " pdf" ` ), SnapDOM automatically exposes a helper method named **` toPdf ()` ** on the capture result.
493+
494+ **Register the plugin (global or per capture):**
495+
496+ ` ` ` js
497+ import { snapdom } from ' @zumer/snapdom' ;
498+
499+ // global
500+ snapdom .plugins (pdfExportPlugin ());
501+
502+ // or per capture
503+ const out = await snapdom (element, { plugins: [pdfExportPlugin ()] });
504+ ` ` `
505+
506+ **Call the custom export:**
507+
508+ ` ` ` js
509+ const out = await snapdom (document .querySelector (' #report' ));
510+
511+ // because the plugin returns { pdf: async (ctx, opts) => ... }
512+ const pdfBlob = await out .toPdf ({
513+ // exporter-specific options (width, height, quality, filename, etc.)
514+ });
515+ ` ` `
516+
517+ ### Example: Overlay Filter Plugin
518+
519+ Adds a translucent overlay or color filter **only** to the captured clone (not your live DOM).
520+ Useful for highlighting or dimming sections before export.
521+
522+ ` ` ` js
523+ /**
524+ * Ultra-simple overlay filter for SnapDOM (HTML-only).
525+ * Inserts a full-size <div> overlay on the cloned root.
526+ *
527+ * @param {{ color?: string; blur?: number }} [options]
528+ * color: overlay color (rgba/hex/hsl). Default: 'rgba(0,0,0,0.25)'
529+ * blur: optional blur in px (default: 0)
530+ */
531+ export function overlayFilterPlugin (options = {}) {
532+ const color = options .color ?? ' rgba(0,0,0,0.25)' ;
533+ const blur = Math .max (0 , options .blur ?? 0 );
534+
535+ return {
536+ name: ' overlay-filter' ,
537+
538+ /**
539+ * Add a full-coverage overlay to the cloned HTML root.
540+ * @param {any} context
541+ */
542+ async afterClone (context ) {
543+ const root = context .clone ;
544+ if (! (root instanceof HTMLElement )) return ; // HTML-only
545+
546+ // Ensure containing block so absolute overlay anchors to the root
547+ if (getComputedStyle (root).position === ' static' ) {
548+ root .style .position = ' relative' ;
549+ }
550+
551+ const overlay = document .createElement (' div' );
552+ overlay .style .position = ' absolute' ;
553+ overlay .style .left = ' 0' ;
554+ overlay .style .top = ' 0' ;
555+ overlay .style .right = ' 0' ;
556+ overlay .style .bottom = ' 0' ;
557+ overlay .style .background = color;
558+ overlay .style .pointerEvents = ' none' ;
559+ if (blur) overlay .style .filter = ` blur(${ blur} px)` ;
560+
561+ root .appendChild (overlay);
562+ }
563+ };
564+ }
565+
566+ ` ` `
567+
568+ **Usage:**
569+
570+ ` ` ` js
571+ import { snapdom } from ' @zumer/snapdom' ;
572+
573+ // Global registration
574+ snapdom .plugins ([overlayFilterPlugin, { color: ' rgba(0,0,0,0.3)' , blur: 2 }]);
575+
576+ // Per-capture
577+ const out = await snapdom (document .querySelector (' #card' ), {
578+ plugins: [[overlayFilterPlugin, { color: ' rgba(255,200,0,0.15)' }]]
579+ });
580+
581+ const png = await out .toPng ();
582+ document .body .appendChild (png);
583+ ` ` `
584+
585+ > The overlay is injected **only in the cloned tree**, never in your live DOM, ensuring perfect fidelity and zero flicker.
586+
587+
588+ ### Full Plugin Template
589+
590+ Use this as a starting point for custom logic or exporters.
591+
592+ ` ` ` js
593+ export function myPlugin (options = {}) {
594+ return {
595+ /** Unique name used for de-duplication/overrides */
596+ name: ' my-plugin' ,
597+
598+ /** Early adjustments before any clone/style work. */
599+ async beforeSnap (context ) {},
600+
601+ /** Before subtree cloning (use sparingly if touching the live DOM). */
602+ async beforeClone (context ) {},
603+
604+ /** After subtree cloning (safe to modify the cloned tree). */
605+ async afterClone (context ) {},
606+
607+ /** Right before serialization (SVG/dataURL). */
608+ async beforeRender (context ) {},
609+
610+ /** After serialization; inspect context.svgString/context.dataURL if needed. */
611+ async afterRender (context ) {},
612+
613+ /** Before EACH export call (toPng/toSvg/toBlob/...). */
614+ async beforeExport (context ) {},
615+
616+ /**
617+ * After EACH export call.
618+ * If you return a value, it becomes the result for the next plugin (chaining).
619+ */
620+ async afterExport (context , result ) { return result; },
621+
622+ /**
623+ * Define custom exporters (auto-added as helpers like out.toPdf()).
624+ * Return a map { [key: string]: (ctx:any, opts:any) => Promise<any> }.
625+ */
626+ async defineExports (context ) { return {}; },
627+
628+ /** Runs ONCE after the FIRST export finishes (cleanup). */
629+ async afterSnap (context ) {}
630+ };
631+ }
632+ ` ` `
633+
634+ **Quick recap:**
635+
636+ * Plugins can modify capture behavior (` beforeSnap` , ` afterClone` , etc.).
637+ * You can inject visuals or transformations safely into the cloned tree.
638+ * New exporters defined in ` defineExports ()` automatically become helpers like ` out .toPdf ()` .
639+ * All hooks can be asynchronous, run in order, and share the same ` context` .
640+
641+
412642## Limitations
413643
414644* External images should be CORS-accessible (use ` useProxy` option for handling CORS denied)
@@ -431,7 +661,6 @@ Values are **average capture time (ms)** → lower is better.
431661| Large Scroll (2000×1500) | **0.5 ms** | 0.8 ms | 186.3 ms | 3.2 ms |
432662| Very Large (4000×2000) | **0.5 ms** | 0.9 ms | 425.9 ms | 3.3 ms |
433663
434- ---
435664
436665### Complex elements
437666
0 commit comments