@emasoft/svg-matrix 1.3.0 → 1.3.2
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 +14 -1
- package/bin/svgfonts.js +259 -5
- package/dist/svg-matrix.global.min.js +3 -3
- package/dist/svg-matrix.min.js +3 -3
- package/dist/svg-toolbox.global.min.js +39 -39
- package/dist/svg-toolbox.min.js +36 -36
- package/dist/svgm.min.js +26 -26
- package/dist/version.json +12 -12
- package/package.json +2 -2
- package/scripts/postinstall.js +10 -4
- package/scripts/version-sync.js +14 -8
- package/src/animation-optimization.js +3 -2
- package/src/animation-references.js +2 -1
- package/src/bezier-analysis.js +2 -1
- package/src/bezier-intersections.js +53 -18
- package/src/convert-path-data.js +2 -3
- package/src/flatten-pipeline.js +2 -3
- package/src/font-manager.js +935 -42
- package/src/index.js +2 -2
- package/src/inkscape-support.js +2 -1
- package/src/matrix.js +3 -3
- package/src/mesh-gradient.js +53 -1
- package/src/path-simplification.js +3 -3
- package/src/pattern-resolver.js +3 -2
- package/src/svg-matrix-lib.js +4 -3
- package/src/svg-parser.js +0 -4
- package/src/svg-toolbox-lib.js +2 -2
- package/src/svg-validation-data.js +1 -1
- package/src/svgm-lib.js +2 -2
- package/src/transforms3d.js +4 -15
package/README.md
CHANGED
|
@@ -840,15 +840,28 @@ svg-matrix vs standard JavaScript (float64):
|
|
|
840
840
|
| Arc length | `2.8e-13` | `< 1e-50` | 37+ digits |
|
|
841
841
|
| Bounding box | `1.1e-13` | `0` (exact) | 13+ digits |
|
|
842
842
|
| Self-intersection | Boolean only | `1.4e-58` | 58+ digits |
|
|
843
|
+
| Bezier-Bezier intersection | `4e-7` | `2.3e-10` | svgpathtools compatible |
|
|
843
844
|
|
|
844
845
|
---
|
|
845
846
|
|
|
846
847
|
## Installation
|
|
847
848
|
|
|
848
|
-
**Requires Node.js 24+**
|
|
849
|
+
**Requires Node.js 24+** or **Bun 1.0+**
|
|
850
|
+
|
|
851
|
+
### Package Managers
|
|
849
852
|
|
|
850
853
|
```bash
|
|
854
|
+
# Bun (recommended - fastest)
|
|
855
|
+
bun add @emasoft/svg-matrix
|
|
856
|
+
|
|
857
|
+
# npm
|
|
851
858
|
npm install @emasoft/svg-matrix
|
|
859
|
+
|
|
860
|
+
# pnpm
|
|
861
|
+
pnpm add @emasoft/svg-matrix
|
|
862
|
+
|
|
863
|
+
# yarn
|
|
864
|
+
yarn add @emasoft/svg-matrix
|
|
852
865
|
```
|
|
853
866
|
|
|
854
867
|
### In JavaScript/TypeScript
|
package/bin/svgfonts.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
statSync,
|
|
22
22
|
realpathSync,
|
|
23
23
|
} from "fs";
|
|
24
|
-
import { join,
|
|
24
|
+
import { join, basename, extname, resolve, isAbsolute } from "path";
|
|
25
25
|
import { createInterface } from "readline";
|
|
26
26
|
|
|
27
27
|
// Import library modules
|
|
@@ -84,8 +84,11 @@ const DEFAULT_CONFIG = {
|
|
|
84
84
|
// Embed options
|
|
85
85
|
subset: true,
|
|
86
86
|
full: false,
|
|
87
|
+
woff2: false,
|
|
87
88
|
source: null, // 'google' | 'local' | 'fontget' | 'fnt'
|
|
88
89
|
timeout: CONSTANTS.DEFAULT_TIMEOUT,
|
|
90
|
+
useCache: true,
|
|
91
|
+
searchAlternatives: false,
|
|
89
92
|
// Replace options
|
|
90
93
|
mapFile: null,
|
|
91
94
|
autoDownload: true,
|
|
@@ -94,6 +97,13 @@ const DEFAULT_CONFIG = {
|
|
|
94
97
|
restoreLinks: false,
|
|
95
98
|
// Template options
|
|
96
99
|
templateOutput: null,
|
|
100
|
+
// Cache options
|
|
101
|
+
cacheAction: "stats", // 'stats' | 'clean' | 'list'
|
|
102
|
+
maxAge: 30, // days
|
|
103
|
+
// Search options
|
|
104
|
+
query: null,
|
|
105
|
+
limit: 10,
|
|
106
|
+
threshold: 0.3,
|
|
97
107
|
};
|
|
98
108
|
|
|
99
109
|
let config = { ...DEFAULT_CONFIG };
|
|
@@ -139,6 +149,9 @@ ${colors.cyan}COMMANDS:${colors.reset}
|
|
|
139
149
|
list List fonts in SVG
|
|
140
150
|
interactive Interactive font management mode
|
|
141
151
|
template Generate replacement map template
|
|
152
|
+
cache Manage font cache (stats, clean)
|
|
153
|
+
search Search for fonts by name with similarity matching
|
|
154
|
+
dedupe Detect and merge duplicate @font-face rules
|
|
142
155
|
|
|
143
156
|
${colors.cyan}GLOBAL OPTIONS:${colors.reset}
|
|
144
157
|
-i, --input <file> Input SVG file(s)
|
|
@@ -156,8 +169,11 @@ ${colors.cyan}GLOBAL OPTIONS:${colors.reset}
|
|
|
156
169
|
${colors.cyan}EMBED OPTIONS:${colors.reset}
|
|
157
170
|
--subset Only embed glyphs used in SVG (default)
|
|
158
171
|
--full Embed complete font files
|
|
172
|
+
--woff2 Convert fonts to WOFF2 format (~30% smaller)
|
|
159
173
|
--source <name> Preferred font source (google|local|fontget|fnt)
|
|
160
174
|
--timeout <ms> Download timeout (default: 30000)
|
|
175
|
+
--no-cache Disable font caching (enabled by default)
|
|
176
|
+
--search-alternatives If font unavailable, search for alternatives
|
|
161
177
|
|
|
162
178
|
${colors.cyan}REPLACE OPTIONS:${colors.reset}
|
|
163
179
|
--map <file> Path to replacement YAML
|
|
@@ -170,14 +186,28 @@ ${colors.cyan}EXTRACT OPTIONS:${colors.reset}
|
|
|
170
186
|
${colors.cyan}TEMPLATE OPTIONS:${colors.reset}
|
|
171
187
|
--template-output <file> Output path for template (default: stdout)
|
|
172
188
|
|
|
189
|
+
${colors.cyan}CACHE OPTIONS:${colors.reset}
|
|
190
|
+
--cache-action <action> Cache action: stats, clean, list (default: stats)
|
|
191
|
+
--max-age <days> Max cache age in days for clean (default: 30)
|
|
192
|
+
|
|
193
|
+
${colors.cyan}SEARCH OPTIONS:${colors.reset}
|
|
194
|
+
--query <name> Font name to search for (required)
|
|
195
|
+
--limit <n> Max results to show (default: 10)
|
|
196
|
+
--threshold <0-1> Minimum similarity (default: 0.3)
|
|
197
|
+
|
|
173
198
|
${colors.cyan}EXAMPLES:${colors.reset}
|
|
174
199
|
svgfonts embed icon.svg # Embed fonts with subsetting
|
|
175
200
|
svgfonts embed --full icon.svg # Embed complete fonts
|
|
201
|
+
svgfonts embed --woff2 icon.svg # Embed with WOFF2 compression
|
|
176
202
|
svgfonts list document.svg # List all fonts in SVG
|
|
177
203
|
svgfonts replace --map fonts.yml *.svg # Apply replacement map
|
|
178
204
|
svgfonts extract -o ./fonts doc.svg # Extract embedded fonts
|
|
179
205
|
svgfonts interactive icon.svg # Interactive mode
|
|
180
206
|
svgfonts template > fonts.yml # Generate template
|
|
207
|
+
svgfonts cache # Show font cache stats
|
|
208
|
+
svgfonts cache --cache-action clean # Clean old cached fonts
|
|
209
|
+
svgfonts search --query "roboto" # Search for fonts by name
|
|
210
|
+
svgfonts dedupe icon.svg # Merge duplicate @font-face rules
|
|
181
211
|
|
|
182
212
|
${colors.cyan}ENVIRONMENT:${colors.reset}
|
|
183
213
|
SVGM_REPLACEMENT_MAP Path to default replacement map YAML
|
|
@@ -215,6 +245,9 @@ function parseArgs(args) {
|
|
|
215
245
|
case "list":
|
|
216
246
|
case "interactive":
|
|
217
247
|
case "template":
|
|
248
|
+
case "cache":
|
|
249
|
+
case "search":
|
|
250
|
+
case "dedupe":
|
|
218
251
|
if (!result.command) {
|
|
219
252
|
result.command = arg;
|
|
220
253
|
} else {
|
|
@@ -307,6 +340,41 @@ function parseArgs(args) {
|
|
|
307
340
|
result.templateOutput = args[++i];
|
|
308
341
|
break;
|
|
309
342
|
|
|
343
|
+
// Embed new options
|
|
344
|
+
case "--woff2":
|
|
345
|
+
result.woff2 = true;
|
|
346
|
+
break;
|
|
347
|
+
|
|
348
|
+
case "--no-cache":
|
|
349
|
+
result.useCache = false;
|
|
350
|
+
break;
|
|
351
|
+
|
|
352
|
+
case "--search-alternatives":
|
|
353
|
+
result.searchAlternatives = true;
|
|
354
|
+
break;
|
|
355
|
+
|
|
356
|
+
// Cache options
|
|
357
|
+
case "--cache-action":
|
|
358
|
+
result.cacheAction = args[++i];
|
|
359
|
+
break;
|
|
360
|
+
|
|
361
|
+
case "--max-age":
|
|
362
|
+
result.maxAge = parseInt(args[++i], 10);
|
|
363
|
+
break;
|
|
364
|
+
|
|
365
|
+
// Search options
|
|
366
|
+
case "--query":
|
|
367
|
+
result.query = args[++i];
|
|
368
|
+
break;
|
|
369
|
+
|
|
370
|
+
case "--limit":
|
|
371
|
+
result.limit = parseInt(args[++i], 10);
|
|
372
|
+
break;
|
|
373
|
+
|
|
374
|
+
case "--threshold":
|
|
375
|
+
result.threshold = parseFloat(args[++i]);
|
|
376
|
+
break;
|
|
377
|
+
|
|
310
378
|
default:
|
|
311
379
|
// Assume it's an input file
|
|
312
380
|
if (!arg.startsWith("-")) {
|
|
@@ -601,6 +669,176 @@ function cmdTemplate() {
|
|
|
601
669
|
}
|
|
602
670
|
}
|
|
603
671
|
|
|
672
|
+
// ============================================================================
|
|
673
|
+
// COMMAND: CACHE
|
|
674
|
+
// ============================================================================
|
|
675
|
+
function cmdCache() {
|
|
676
|
+
// Initialize cache if needed
|
|
677
|
+
FontManager.initFontCache();
|
|
678
|
+
|
|
679
|
+
switch (config.cacheAction) {
|
|
680
|
+
case "stats": {
|
|
681
|
+
const stats = FontManager.getFontCacheStats();
|
|
682
|
+
log(`\n${colors.bright}Font Cache Statistics${colors.reset}`);
|
|
683
|
+
log(`${"─".repeat(40)}`);
|
|
684
|
+
log(` Location: ${FontManager.FONT_CACHE_DIR}`);
|
|
685
|
+
log(` Total fonts: ${stats.totalFonts}`);
|
|
686
|
+
log(` Total size: ${formatSize(stats.totalSize)}`);
|
|
687
|
+
if (stats.oldestAge > 0) {
|
|
688
|
+
const oldestDays = Math.floor(stats.oldestAge / (24 * 60 * 60 * 1000));
|
|
689
|
+
const newestDays = Math.floor(stats.newestAge / (24 * 60 * 60 * 1000));
|
|
690
|
+
log(` Oldest entry: ${oldestDays} days ago`);
|
|
691
|
+
log(` Newest entry: ${newestDays} days ago`);
|
|
692
|
+
} else {
|
|
693
|
+
log(` Oldest entry: N/A`);
|
|
694
|
+
log(` Newest entry: N/A`);
|
|
695
|
+
}
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
case "clean": {
|
|
700
|
+
const maxAgeMs = config.maxAge * 24 * 60 * 60 * 1000;
|
|
701
|
+
const result = FontManager.cleanupFontCache(maxAgeMs);
|
|
702
|
+
if (result.removed > 0) {
|
|
703
|
+
logSuccess(`Cleaned ${result.removed} old cached fonts (${formatSize(result.freedBytes)})`);
|
|
704
|
+
} else {
|
|
705
|
+
log("No fonts to clean (all within max age)");
|
|
706
|
+
}
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
case "list": {
|
|
711
|
+
// Read the cache index directly to get list
|
|
712
|
+
const indexPath = join(FontManager.FONT_CACHE_DIR, FontManager.FONT_CACHE_INDEX);
|
|
713
|
+
log(`\n${colors.bright}Cached Fonts${colors.reset}`);
|
|
714
|
+
log(`${"─".repeat(60)}`);
|
|
715
|
+
if (existsSync(indexPath)) {
|
|
716
|
+
try {
|
|
717
|
+
const index = JSON.parse(readFileSync(indexPath, "utf8"));
|
|
718
|
+
const entries = Object.entries(index.fonts);
|
|
719
|
+
if (entries.length > 0) {
|
|
720
|
+
for (const [key, data] of entries) {
|
|
721
|
+
const age = Math.floor((Date.now() - data.timestamp) / (24 * 60 * 60 * 1000));
|
|
722
|
+
log(` ${key.slice(0, 30).padEnd(30)} ${formatSize(data.size || 0).padStart(10)} ${age}d ago`);
|
|
723
|
+
}
|
|
724
|
+
} else {
|
|
725
|
+
log(" No cached fonts");
|
|
726
|
+
}
|
|
727
|
+
} catch {
|
|
728
|
+
log(" No cached fonts");
|
|
729
|
+
}
|
|
730
|
+
} else {
|
|
731
|
+
log(" No cached fonts");
|
|
732
|
+
}
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
default:
|
|
737
|
+
logError(`Unknown cache action: ${config.cacheAction}`);
|
|
738
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// ============================================================================
|
|
743
|
+
// COMMAND: SEARCH
|
|
744
|
+
// ============================================================================
|
|
745
|
+
async function cmdSearch() {
|
|
746
|
+
if (!config.query) {
|
|
747
|
+
logError("Missing --query option. Usage: svgfonts search --query <font-name>");
|
|
748
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
log(`\n${colors.bright}Searching for fonts matching: "${config.query}"${colors.reset}`);
|
|
752
|
+
log(`${"─".repeat(60)}`);
|
|
753
|
+
|
|
754
|
+
const results = await FontManager.searchSimilarFonts(config.query, {
|
|
755
|
+
limit: config.limit,
|
|
756
|
+
threshold: config.threshold,
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
if (results.length === 0) {
|
|
760
|
+
log(`No fonts found matching "${config.query}" with similarity >= ${config.threshold}`);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
log(`\n${colors.cyan}# Similarity Font Name${" ".repeat(25)}Source${colors.reset}`);
|
|
765
|
+
log(`${"─".repeat(60)}`);
|
|
766
|
+
|
|
767
|
+
results.forEach((result, idx) => {
|
|
768
|
+
const simPercent = Math.round(result.similarity * 100);
|
|
769
|
+
const simBar = "█".repeat(Math.round(simPercent / 10)) + "░".repeat(10 - Math.round(simPercent / 10));
|
|
770
|
+
const simColor = simPercent >= 80 ? colors.green : simPercent >= 50 ? colors.yellow : colors.dim;
|
|
771
|
+
log(
|
|
772
|
+
`${(idx + 1).toString().padStart(2)}. ${simColor}${simBar}${colors.reset} ${simPercent.toString().padStart(3)}% ${result.name.padEnd(28)} ${colors.dim}${result.source}${colors.reset}`
|
|
773
|
+
);
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
log(`\n${colors.dim}Use 'svgfonts embed --source google <svg>' to embed a font${colors.reset}`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// ============================================================================
|
|
780
|
+
// COMMAND: DEDUPE
|
|
781
|
+
// ============================================================================
|
|
782
|
+
async function cmdDedupe(files) {
|
|
783
|
+
for (const file of files) {
|
|
784
|
+
logVerbose(`Processing: ${file}`);
|
|
785
|
+
|
|
786
|
+
const content = readFileSync(file, "utf8");
|
|
787
|
+
|
|
788
|
+
// Create backup
|
|
789
|
+
if (!config.noBackup && !config.dryRun) {
|
|
790
|
+
const backupPath = FontManager.createBackup(file, { noBackup: config.noBackup });
|
|
791
|
+
if (backupPath) {
|
|
792
|
+
logVerbose(`Backup created: ${backupPath}`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const doc = parseSVG(content);
|
|
797
|
+
|
|
798
|
+
// Detect duplicates first
|
|
799
|
+
const { duplicates, total } = FontManager.detectDuplicateFontFaces(doc);
|
|
800
|
+
|
|
801
|
+
if (duplicates.length === 0) {
|
|
802
|
+
log(`${basename(file)}: No duplicate @font-face rules found (${total} total)`);
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (config.dryRun) {
|
|
807
|
+
log(`[dry-run] Would merge ${duplicates.length} duplicate font group(s) in: ${file}`);
|
|
808
|
+
for (const dup of duplicates) {
|
|
809
|
+
log(` - "${dup.family}" (${dup.weight}/${dup.style}): ${dup.count} occurrences`);
|
|
810
|
+
}
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Merge duplicates
|
|
815
|
+
const result = FontManager.mergeDuplicateFontFaces(doc);
|
|
816
|
+
|
|
817
|
+
if (result.removed > 0) {
|
|
818
|
+
const output = serializeSVG(doc);
|
|
819
|
+
|
|
820
|
+
// Validate if requested
|
|
821
|
+
if (config.validate) {
|
|
822
|
+
const validation = await FontManager.validateSvgAfterFontOperation(output, "dedupe");
|
|
823
|
+
if (!validation.valid) {
|
|
824
|
+
logError(`Validation failed for ${file}: ${validation.errors.join(", ")}`);
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const outputPath = config.output || file;
|
|
830
|
+
writeFileSync(outputPath, output);
|
|
831
|
+
|
|
832
|
+
logSuccess(`Merged ${result.removed} duplicate @font-face rule(s): ${outputPath}`);
|
|
833
|
+
for (const dup of duplicates) {
|
|
834
|
+
log(` - "${dup.family}" (${dup.weight}/${dup.style})`);
|
|
835
|
+
}
|
|
836
|
+
} else {
|
|
837
|
+
log(`${basename(file)}: No duplicates merged`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
604
842
|
// ============================================================================
|
|
605
843
|
// COMMAND: INTERACTIVE
|
|
606
844
|
// ============================================================================
|
|
@@ -721,13 +959,13 @@ async function cmdInteractive(files) {
|
|
|
721
959
|
});
|
|
722
960
|
|
|
723
961
|
const question = (prompt) =>
|
|
724
|
-
new Promise((
|
|
962
|
+
new Promise((res) => rl.question(prompt, res));
|
|
725
963
|
|
|
726
964
|
/**
|
|
727
965
|
* Read single keypress (for arrow key navigation)
|
|
728
966
|
*/
|
|
729
|
-
const
|
|
730
|
-
new Promise((
|
|
967
|
+
const _readKey = () =>
|
|
968
|
+
new Promise((res) => {
|
|
731
969
|
const wasRaw = process.stdin.isRaw;
|
|
732
970
|
if (process.stdin.setRawMode) {
|
|
733
971
|
process.stdin.setRawMode(true);
|
|
@@ -737,7 +975,7 @@ async function cmdInteractive(files) {
|
|
|
737
975
|
if (process.stdin.setRawMode) {
|
|
738
976
|
process.stdin.setRawMode(wasRaw);
|
|
739
977
|
}
|
|
740
|
-
|
|
978
|
+
res(key.toString());
|
|
741
979
|
});
|
|
742
980
|
});
|
|
743
981
|
|
|
@@ -1361,6 +1599,18 @@ async function main() {
|
|
|
1361
1599
|
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
1362
1600
|
}
|
|
1363
1601
|
|
|
1602
|
+
// Handle cache command (no inputs needed)
|
|
1603
|
+
if (config.command === "cache") {
|
|
1604
|
+
cmdCache();
|
|
1605
|
+
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// Handle search command (no inputs needed)
|
|
1609
|
+
if (config.command === "search") {
|
|
1610
|
+
await cmdSearch();
|
|
1611
|
+
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1364
1614
|
// Collect files
|
|
1365
1615
|
const files = collectSvgFiles(config.inputs, config.recursive);
|
|
1366
1616
|
|
|
@@ -1394,6 +1644,10 @@ async function main() {
|
|
|
1394
1644
|
await cmdInteractive(files);
|
|
1395
1645
|
break;
|
|
1396
1646
|
|
|
1647
|
+
case "dedupe":
|
|
1648
|
+
await cmdDedupe(files);
|
|
1649
|
+
break;
|
|
1650
|
+
|
|
1397
1651
|
default:
|
|
1398
1652
|
// Default to embed
|
|
1399
1653
|
await cmdEmbed(files);
|