@emasoft/svg-matrix 1.0.28 → 1.0.29

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.
Files changed (46) hide show
  1. package/README.md +325 -0
  2. package/bin/svg-matrix.js +985 -378
  3. package/bin/svglinter.cjs +4172 -433
  4. package/bin/svgm.js +723 -180
  5. package/package.json +16 -4
  6. package/src/animation-references.js +71 -52
  7. package/src/arc-length.js +160 -96
  8. package/src/bezier-analysis.js +257 -117
  9. package/src/bezier-intersections.js +411 -148
  10. package/src/browser-verify.js +240 -100
  11. package/src/clip-path-resolver.js +350 -142
  12. package/src/convert-path-data.js +279 -134
  13. package/src/css-specificity.js +78 -70
  14. package/src/flatten-pipeline.js +751 -263
  15. package/src/geometry-to-path.js +511 -182
  16. package/src/index.js +191 -46
  17. package/src/inkscape-support.js +18 -7
  18. package/src/marker-resolver.js +278 -164
  19. package/src/mask-resolver.js +209 -98
  20. package/src/matrix.js +147 -67
  21. package/src/mesh-gradient.js +187 -96
  22. package/src/off-canvas-detection.js +201 -104
  23. package/src/path-analysis.js +187 -107
  24. package/src/path-data-plugins.js +628 -167
  25. package/src/path-simplification.js +0 -1
  26. package/src/pattern-resolver.js +125 -88
  27. package/src/polygon-clip.js +111 -66
  28. package/src/svg-boolean-ops.js +194 -118
  29. package/src/svg-collections.js +22 -18
  30. package/src/svg-flatten.js +282 -164
  31. package/src/svg-parser.js +427 -200
  32. package/src/svg-rendering-context.js +147 -104
  33. package/src/svg-toolbox.js +16381 -3370
  34. package/src/svg2-polyfills.js +93 -224
  35. package/src/transform-decomposition.js +46 -41
  36. package/src/transform-optimization.js +89 -68
  37. package/src/transforms2d.js +49 -16
  38. package/src/transforms3d.js +58 -22
  39. package/src/use-symbol-resolver.js +150 -110
  40. package/src/vector.js +67 -15
  41. package/src/vendor/README.md +110 -0
  42. package/src/vendor/inkscape-hatch-polyfill.js +401 -0
  43. package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
  44. package/src/vendor/inkscape-mesh-polyfill.js +843 -0
  45. package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
  46. package/src/verification.js +288 -124
package/README.md CHANGED
@@ -255,6 +255,126 @@ svgm --show-plugins
255
255
 
256
256
  Run `svgm --help` for all options.
257
257
 
258
+ ### Embed External Dependencies
259
+
260
+ Make SVGs self-contained by embedding external resources as data URIs:
261
+
262
+ ```bash
263
+ # Embed all external dependencies
264
+ svgm --embed input.svg -o output.svg
265
+
266
+ # Embed specific resource types
267
+ svgm --embed-images --embed-css --embed-fonts input.svg -o output.svg
268
+
269
+ # Embed external SVG references with mode selection
270
+ svgm --embed-external-svgs --embed-svg-mode extract input.svg -o output.svg
271
+
272
+ # Embed audio files (for interactive SVGs)
273
+ svgm --embed-audio input.svg -o output.svg
274
+
275
+ # Subset fonts to only include used characters (smaller file size)
276
+ svgm --embed-fonts --embed-subset-fonts input.svg -o output.svg
277
+ ```
278
+
279
+ **Embed options:**
280
+
281
+ | Option | What It Does |
282
+ |--------|--------------|
283
+ | `--embed` | Enable all embedding (images, CSS, fonts, scripts, audio) |
284
+ | `--embed-images` | Embed raster images as data URIs |
285
+ | `--embed-external-svgs` | Embed referenced SVG files |
286
+ | `--embed-svg-mode <mode>` | How to embed SVGs: `extract` (symbols only) or `full` |
287
+ | `--embed-css` | Embed external stylesheets |
288
+ | `--embed-fonts` | Embed web fonts as base64 |
289
+ | `--embed-scripts` | Embed external JavaScript |
290
+ | `--embed-audio` | Embed audio files as data URIs |
291
+ | `--embed-subset-fonts` | Subset fonts to used characters only |
292
+ | `--embed-recursive` | Recursively resolve nested dependencies |
293
+ | `--embed-max-depth <n>` | Max recursion depth (default: 10) |
294
+ | `--embed-timeout <ms>` | Fetch timeout in milliseconds (default: 30000) |
295
+ | `--embed-on-missing <mode>` | Action on missing resource: `warn`, `fail`, or `skip` |
296
+
297
+ ### Export Embedded Resources
298
+
299
+ Extract embedded resources from self-contained SVGs back to external files:
300
+
301
+ ```bash
302
+ # Export all embedded resources to a folder
303
+ svgm --export input.svg -o output.svg --export-dir ./resources/
304
+
305
+ # Dry run - show what would be exported without writing files
306
+ svgm --export --export-dry-run input.svg
307
+
308
+ # Export only images
309
+ svgm --export --export-images input.svg -o output.svg --export-dir ./images/
310
+
311
+ # Extract resources without modifying the SVG
312
+ svgm --export --export-only input.svg --export-dir ./resources/
313
+
314
+ # Custom filename prefix for exported files
315
+ svgm --export --export-prefix myapp_ input.svg -o output.svg --export-dir ./assets/
316
+ ```
317
+
318
+ **Export options:**
319
+
320
+ | Option | What It Does |
321
+ |--------|--------------|
322
+ | `--export` | Enable resource extraction mode |
323
+ | `--export-dir <path>` | Output directory for extracted files |
324
+ | `--export-prefix <str>` | Filename prefix for exported files |
325
+ | `--export-images` | Export embedded images only |
326
+ | `--export-audio` | Export embedded audio only |
327
+ | `--export-video` | Export embedded video only |
328
+ | `--export-scripts` | Export inline scripts to .js files |
329
+ | `--export-styles` | Export inline styles to .css files |
330
+ | `--export-fonts` | Export embedded fonts |
331
+ | `--export-only` | Extract files only, don't modify SVG |
332
+ | `--export-dry-run` | Preview extraction without writing files |
333
+ | `--export-ids <ids>` | Only export from specific element IDs |
334
+
335
+ ### YAML Configuration
336
+
337
+ Instead of CLI flags, you can use a YAML configuration file:
338
+
339
+ ```yaml
340
+ # svgm.yml
341
+ precision: 4
342
+ multipass: true
343
+ pretty: true
344
+ indent: 2
345
+
346
+ embed:
347
+ images: true
348
+ externalSVGs: true
349
+ externalSVGMode: extract
350
+ css: true
351
+ fonts: true
352
+ scripts: true
353
+ audio: true
354
+ subsetFonts: true
355
+ recursive: true
356
+ maxRecursionDepth: 10
357
+ timeout: 30000
358
+ onMissingResource: warn
359
+
360
+ export:
361
+ outputDir: ./resources/
362
+ filenamePrefix: resource_
363
+ images: true
364
+ audio: true
365
+ video: true
366
+ scripts: true
367
+ styles: true
368
+ fonts: true
369
+ extractOnly: false
370
+ dryRun: false
371
+ ```
372
+
373
+ ```bash
374
+ # Use config file
375
+ svgm -c svgm.yml input.svg -o output.svg
376
+ ```
377
+
258
378
  ### Namespace Preservation
259
379
 
260
380
  Preserve vendor-specific namespaces during optimization:
@@ -417,6 +537,187 @@ const features = SVG2Polyfills.detectSVG2Features(doc);
417
537
  SVG2Polyfills.injectPolyfills(doc);
418
538
  ```
419
539
 
540
+ ### Embedding and Exporting Resources
541
+
542
+ ```js
543
+ import { embedExternalDependencies, exportEmbeddedResources } from '@emasoft/svg-matrix';
544
+
545
+ // Embed external resources into SVG
546
+ const embedded = await embedExternalDependencies(svgString, {
547
+ basePath: '/path/to/file.svg',
548
+ embedImages: true,
549
+ embedFonts: true,
550
+ embedCSS: true,
551
+ embedScripts: true,
552
+ embedAudio: true,
553
+ subsetFonts: true,
554
+ onMissingResource: 'warn',
555
+ timeout: 30000,
556
+ });
557
+
558
+ // Export embedded resources back to external files
559
+ const result = await exportEmbeddedResources(embeddedSvg, {
560
+ outputDir: './extracted/',
561
+ filenamePrefix: 'resource_',
562
+ extractImages: true,
563
+ extractAudio: true,
564
+ extractVideo: true,
565
+ extractScripts: true,
566
+ extractStyles: true,
567
+ extractFonts: true,
568
+ extractOnly: false, // true = extract without modifying SVG
569
+ dryRun: false, // true = preview only, don't write files
570
+ elementIds: null, // filter by element IDs
571
+ onProgress: (phase, current, total) => console.log(`${phase}: ${current}/${total}`),
572
+ });
573
+
574
+ console.log(result.extractedFiles); // Array of extracted file info
575
+ console.log(result.summary); // { images, audio, scripts, stylesheets, fonts, totalSize }
576
+ console.log(result.doc); // Modified SVG string (null if extractOnly)
577
+ ```
578
+
579
+ ---
580
+
581
+ ## SVG Embedding Options
582
+
583
+ When using SVG files in web pages, the embedding method affects what features work:
584
+
585
+ | Embedding Method | Animation (SMIL) | JavaScript | Audio | Use Case |
586
+ |------------------|------------------|------------|-------|----------|
587
+ | `<img src="file.svg">` | Yes | No | No | Static display, icons |
588
+ | `<object data="file.svg">` | Yes | Yes | No* | Interactive SVGs |
589
+ | `<embed src="file.svg">` | Yes | Yes | No* | Legacy support |
590
+ | `<iframe src="file.svg">` | Yes | Yes | No* | Isolated context |
591
+ | Inline `<svg>...</svg>` | Yes | Yes | No* | Full DOM access |
592
+ | Standalone (file://) | Yes | Yes | No | Direct file viewing |
593
+ | Standalone (http://) | Yes | Yes | No* | Web server |
594
+
595
+ *Audio requires user interaction due to browser autoplay policies.
596
+
597
+ ### SVG Audio Playback Limitations
598
+
599
+ Modern browsers (Chrome, Firefox, Safari) enforce strict autoplay policies that significantly limit audio playback in SVG files:
600
+
601
+ | Scenario | Protocol | Example | Audio |
602
+ |----------|----------|---------|-------|
603
+ | Standalone SVG | `file://` | `file:///path/to/sample.svg` | ❌ **Blocked** |
604
+ | Standalone SVG | `http://` | `http://localhost:8080/sample.svg` | ❌ **Blocked** |
605
+ | HTML with hardcoded audio data URIs | `file://` | `file:///path/to/sample.html` | ❌ **Blocked** (even after click) |
606
+ | HTML with hardcoded audio data URIs | `http://` | `http://localhost:8080/sample.html` | ❌ **Blocked** (even after click) |
607
+ | HTML extracts audio from SVG dynamically | `file://` | `file:///path/to/sample.html` | ✅ **Works** (after click) |
608
+ | HTML extracts audio from SVG dynamically | `http://` | `http://localhost:8080/sample.html` | ✅ **Works** (after click) |
609
+
610
+ **Key findings:**
611
+ - Audio elements inside SVG `<foreignObject>` are blocked by browsers regardless of protocol or embedding method
612
+ - Audio sources must be set **dynamically** (not hardcoded in HTML) for playback to work
613
+ - Empty `<audio>` elements + dynamic `src` assignment on load + `play()` on click = success
614
+ - Hardcoded data URIs in HTML `<source>` tags fail even with user click
615
+ - There is no workaround for truly autonomous SVG audio playback
616
+
617
+ **Technical reasons:**
618
+ 1. SVG has no native `<audio>` element - audio requires HTML `<foreignObject>`
619
+ 2. Browser autoplay policies block audio without direct user gesture
620
+ 3. Click events inside SVG don't propagate as "trusted" user gestures for audio
621
+ 4. This is a browser security feature, not an SVG limitation
622
+
623
+ **Recommended approach for SVG with audio:**
624
+
625
+ Extract audio sources from the SVG at runtime and play them via HTML `<audio>` elements. This keeps the SVG file unchanged while enabling audio playback. See `samples/SVG_WITH_EMBEDDED_AUDIO/test-embed-with-audio.html` for a complete working example.
626
+
627
+ ```html
628
+ <!-- HTML wrapper that extracts audio from SVG -->
629
+ <div class="player-container">
630
+ <object id="svgObject" data="animation.svg" type="image/svg+xml"></object>
631
+ <div class="click-overlay" id="overlay">Click to Play</div>
632
+ </div>
633
+ <audio id="audio_external" preload="auto"></audio>
634
+
635
+ <script>
636
+ const svgObject = document.getElementById('svgObject');
637
+ const audio = document.getElementById('audio_external');
638
+ let svgRoot = null;
639
+
640
+ // On SVG load: pause animation and extract audio source
641
+ svgObject.addEventListener('load', function() {
642
+ const svgDoc = svgObject.contentDocument;
643
+ svgRoot = svgDoc.documentElement;
644
+ svgRoot.pauseAnimations(); // Pause SMIL animation
645
+
646
+ // Extract audio source from SVG (audio data stays in SVG file)
647
+ const svgAudio = svgDoc.getElementById('audio1');
648
+ if (svgAudio) {
649
+ const source = svgAudio.querySelector('source');
650
+ if (source) audio.src = source.src; // Copy data URI to HTML audio
651
+ }
652
+ });
653
+
654
+ // On user click: start animation and audio together
655
+ document.getElementById('overlay').addEventListener('click', function() {
656
+ this.style.display = 'none';
657
+ svgRoot.unpauseAnimations(); // Resume SMIL animation
658
+ audio.play(); // Play audio from HTML context
659
+ });
660
+ </script>
661
+ ```
662
+
663
+ This approach:
664
+ 1. Keeps SVG file unchanged (read-only) - audio data URIs remain in SVG
665
+ 2. Extracts audio sources at runtime to HTML `<audio>` elements
666
+ 3. Pauses SMIL animation until user clicks
667
+ 4. Starts animation and audio simultaneously for perfect sync
668
+ 5. Audio plays from HTML context where browser allows it
669
+
670
+ ### Audio Alternatives Research (2024-2025)
671
+
672
+ Extensive testing confirms there is **no way to bypass Chrome's autoplay policy** without user interaction. This is by design for user experience.
673
+
674
+ #### What Doesn't Work
675
+
676
+ | Method | Status | Why It Fails |
677
+ |--------|--------|--------------|
678
+ | SVG 2.0 native `<audio>` | ❌ | Same autoplay restrictions apply |
679
+ | SMIL `<audio>` element | ❌ | Poor browser support + autoplay blocked |
680
+ | `<foreignObject>` bypass | ❌ | No special privileges for embedded HTML |
681
+ | Web Audio API (AudioContext) | ❌ | Starts in "suspended" state, same restrictions |
682
+ | CSS audio / `background-sound` | ❌ | Does not exist in any specification |
683
+ | Data URIs / Blob URLs | ❌ | Encoding method doesn't affect autoplay policy |
684
+ | SVG `onload` event | ❌ | Not considered a user gesture |
685
+ | SMIL animation `beginEvent` | ❌ | Animation events are not user gestures |
686
+ | `xlink:href` to audio file | ❌ | Creates link, doesn't trigger playback |
687
+
688
+ #### What Works
689
+
690
+ | Method | Status | Notes |
691
+ |--------|--------|-------|
692
+ | User click/touch/keydown | ✅ | **Mandatory** - no exceptions |
693
+ | HTML wrapper with dynamic audio extraction | ✅ | **Best practice** (see example above) |
694
+ | PWA (Progressive Web App) | ✅ | Desktop only, requires user to install app |
695
+ | Media Engagement Index (MEI) | ⚠️ | Chrome-only, long-term strategy |
696
+
697
+ #### Media Engagement Index (MEI)
698
+
699
+ Chrome tracks media consumption per domain. After meeting these criteria, autoplay may be allowed on future visits:
700
+ - User plays media for >7 seconds
701
+ - Audio is unmuted
702
+ - Tab is active and visible
703
+ - Video element is >200x140 pixels
704
+
705
+ This is a **long-term strategy** for apps with repeat users, not an immediate solution.
706
+
707
+ #### Browser Compatibility
708
+
709
+ | Feature | Chrome | Firefox | Safari | Edge |
710
+ |---------|--------|---------|--------|------|
711
+ | User gesture required | ✅ | ✅ | ✅ | ✅ |
712
+ | Web Audio API | ✅ | ✅ | ⚠️ iOS quirks | ✅ |
713
+ | Data URI audio | ✅ | ⚠️ >1MB issues | ✅ | ✅ |
714
+ | MEI autoplay | ✅ | ❌ | ❌ | ✅ |
715
+ | PWA autoplay | ✅ | ⚠️ | ⚠️ | ✅ |
716
+
717
+ **Conclusion:** User interaction is mandatory. The HTML wrapper approach with dynamic audio extraction (`test-embed-with-audio.html`) is the industry best practice recommended by browser vendors.
718
+
719
+ ---
720
+
420
721
  ### Exclusive Features (Not in SVGO)
421
722
 
422
723
  | Function | Description |
@@ -426,6 +727,8 @@ SVG2Polyfills.injectPolyfills(doc);
426
727
  | `flattenGradients()` | Bake gradients into fills |
427
728
  | `flattenPatterns()` | Expand pattern tiles |
428
729
  | `flattenUseElements()` | Inline use/symbol references |
730
+ | `embedExternalDependencies()` | Embed external resources as data URIs |
731
+ | `exportEmbeddedResources()` | Extract embedded resources to files |
429
732
  | `detectCollisions()` | GJK collision detection |
430
733
  | `validateSVG()` | W3C schema validation |
431
734
  | `decomposeTransform()` | Matrix decomposition |
@@ -500,6 +803,28 @@ import { Matrix, Vector, Transforms2D } from '@emasoft/svg-matrix';
500
803
 
501
804
  ---
502
805
 
806
+ ## Third-Party Licenses
807
+
808
+ This project is licensed under the MIT License (see [LICENSE](LICENSE)).
809
+
810
+ ### SVG 2.0 Polyfill Dependencies
811
+
812
+ When using the `--svg2-polyfills` option with `svgm` or `svg-matrix`, the following third-party code is embedded in the output SVG:
813
+
814
+ **Inkscape mesh.js Polyfill** by Tavmjong Bah
815
+
816
+ - **Purpose:** Provides browser compatibility for SVG 2.0 mesh gradients via canvas fallback
817
+ - **License:** GNU General Public License version 3 or later (GPLv3)
818
+ - **Source:** [https://gitlab.com/Tavmjong/mesh.js/](https://gitlab.com/Tavmjong/mesh.js/)
819
+ - **Location:** `src/vendor/inkscape-mesh-polyfill.js`
820
+
821
+ **Important:** When you use `--svg2-polyfills`, the generated SVG file will contain GPLv3-licensed JavaScript code. This means:
822
+ - The output SVG file is subject to GPLv3 terms
823
+ - If you distribute the SVG, you must provide source code access as required by GPLv3
824
+ - Without `--svg2-polyfills`, all generated files remain under MIT license
825
+
826
+ ---
827
+
503
828
  <p align="center">
504
829
  <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='30' viewBox='0 0 200 30'%3E%3Cg stroke='%23ddd' stroke-width='0.5' fill='none'%3E%3Cline x1='0' y1='15' x2='60' y2='15'/%3E%3Crect x='70' y='10' width='10' height='10' transform='rotate(45 75 15)'/%3E%3Ccircle cx='100' cy='15' r='5'/%3E%3Crect x='120' y='10' width='10' height='10' transform='rotate(45 125 15)'/%3E%3Cline x1='140' y1='15' x2='200' y2='15'/%3E%3C/g%3E%3C/svg%3E" alt=""/>
505
830
  </p>