@emasoft/svg-matrix 1.2.1 → 1.3.0
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 +75 -0
- package/bin/svgfonts.js +1412 -0
- package/bin/svgm.js +1 -1
- package/dist/svg-matrix.global.min.js +8 -0
- package/dist/svg-matrix.min.js +2 -2
- package/dist/svg-toolbox.global.min.js +493 -0
- package/dist/svg-toolbox.min.js +16 -16
- package/dist/svgm.min.js +60 -60
- package/dist/version.json +44 -16
- package/package.json +11 -3
- package/src/bezier-intersections.js +1 -1
- package/src/browser-verify.js +0 -1
- package/src/clip-path-resolver.js +3 -1
- package/src/font-manager.js +1013 -0
- package/src/index.js +2 -2
- package/src/inkscape-support.js +2 -2
- package/src/mask-resolver.js +14 -6
- package/src/mesh-gradient.js +0 -2
- package/src/off-canvas-detection.js +14 -22
- package/src/svg-boolean-ops.js +0 -5
- package/src/svg-collections.js +11 -0
- package/src/svg-matrix-lib.js +2 -2
- package/src/svg-parser.js +0 -24
- package/src/svg-rendering-context.js +2 -4
- package/src/svg-toolbox-lib.js +2 -2
- package/src/svgm-lib.js +2 -2
- package/src/transform-optimization.js +93 -142
- package/src/verification.js +0 -2
- package/templates/svgm_replacement_map.yml +53 -0
|
@@ -0,0 +1,1013 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Font Management Module for SVG-Matrix
|
|
3
|
+
*
|
|
4
|
+
* Core font management functionality extracted from svg-toolbox.js.
|
|
5
|
+
* Provides utilities for:
|
|
6
|
+
* - Font embedding (convert external fonts to base64 data URIs)
|
|
7
|
+
* - Font extraction (extract embedded fonts to files)
|
|
8
|
+
* - Font listing and analysis
|
|
9
|
+
* - Google Fonts character subsetting
|
|
10
|
+
* - Local system font detection
|
|
11
|
+
* - YAML replacement map processing
|
|
12
|
+
*
|
|
13
|
+
* @module font-manager
|
|
14
|
+
* @license MIT
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync, readdirSync } from "fs";
|
|
18
|
+
import { join, dirname, basename, extname, resolve } from "path";
|
|
19
|
+
import { homedir, platform } from "os";
|
|
20
|
+
import { execSync, execFileSync } from "child_process";
|
|
21
|
+
import yaml from "js-yaml";
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// CONSTANTS
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Default replacement map filename
|
|
29
|
+
* @constant {string}
|
|
30
|
+
*/
|
|
31
|
+
export const DEFAULT_REPLACEMENT_MAP = "svgm_replacement_map.yml";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Environment variable for custom replacement map path
|
|
35
|
+
* @constant {string}
|
|
36
|
+
*/
|
|
37
|
+
export const ENV_REPLACEMENT_MAP = "SVGM_REPLACEMENT_MAP";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Common web-safe fonts that browsers have built-in
|
|
41
|
+
* @constant {string[]}
|
|
42
|
+
*/
|
|
43
|
+
export const WEB_SAFE_FONTS = [
|
|
44
|
+
"Arial",
|
|
45
|
+
"Arial Black",
|
|
46
|
+
"Comic Sans MS",
|
|
47
|
+
"Courier New",
|
|
48
|
+
"Georgia",
|
|
49
|
+
"Impact",
|
|
50
|
+
"Times New Roman",
|
|
51
|
+
"Trebuchet MS",
|
|
52
|
+
"Verdana",
|
|
53
|
+
"Lucida Console",
|
|
54
|
+
"Lucida Sans Unicode",
|
|
55
|
+
"Palatino Linotype",
|
|
56
|
+
"Tahoma",
|
|
57
|
+
"Geneva",
|
|
58
|
+
"Helvetica",
|
|
59
|
+
"sans-serif",
|
|
60
|
+
"serif",
|
|
61
|
+
"monospace",
|
|
62
|
+
"cursive",
|
|
63
|
+
"fantasy",
|
|
64
|
+
"system-ui",
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Font formats and their MIME types
|
|
69
|
+
* @constant {Object}
|
|
70
|
+
*/
|
|
71
|
+
export const FONT_FORMATS = {
|
|
72
|
+
".woff2": "font/woff2",
|
|
73
|
+
".woff": "font/woff",
|
|
74
|
+
".ttf": "font/ttf",
|
|
75
|
+
".otf": "font/otf",
|
|
76
|
+
".eot": "application/vnd.ms-fontobject",
|
|
77
|
+
".svg": "image/svg+xml",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* System font directories by platform
|
|
82
|
+
* @constant {Object}
|
|
83
|
+
*/
|
|
84
|
+
export const SYSTEM_FONT_PATHS = {
|
|
85
|
+
darwin: [
|
|
86
|
+
"/Library/Fonts",
|
|
87
|
+
"/System/Library/Fonts",
|
|
88
|
+
join(homedir(), "Library/Fonts"),
|
|
89
|
+
],
|
|
90
|
+
linux: [
|
|
91
|
+
"/usr/share/fonts",
|
|
92
|
+
"/usr/local/share/fonts",
|
|
93
|
+
join(homedir(), ".fonts"),
|
|
94
|
+
join(homedir(), ".local/share/fonts"),
|
|
95
|
+
],
|
|
96
|
+
win32: [
|
|
97
|
+
join(process.env.WINDIR || "C:\\Windows", "Fonts"),
|
|
98
|
+
join(homedir(), "AppData/Local/Microsoft/Windows/Fonts"),
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// FONT CHARACTER EXTRACTION
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Extract all text content from SVG and map to fonts.
|
|
108
|
+
* Returns a Map where keys are font family names and values are Sets of characters used.
|
|
109
|
+
*
|
|
110
|
+
* @param {Object} element - SVG element to scan (DOM element or JSDOM document)
|
|
111
|
+
* @returns {Map<string, Set<string>>} Font to characters map
|
|
112
|
+
*/
|
|
113
|
+
export function extractFontCharacterMap(element) {
|
|
114
|
+
const fontMap = new Map();
|
|
115
|
+
|
|
116
|
+
const addCharsToFont = (fontFamily, text) => {
|
|
117
|
+
if (!fontFamily || !text) return;
|
|
118
|
+
// Normalize font family name (remove quotes, trim, take first in stack)
|
|
119
|
+
const normalizedFont = fontFamily
|
|
120
|
+
.replace(/['"]/g, "")
|
|
121
|
+
.trim()
|
|
122
|
+
.split(",")[0]
|
|
123
|
+
.trim();
|
|
124
|
+
if (!fontMap.has(normalizedFont)) {
|
|
125
|
+
fontMap.set(normalizedFont, new Set());
|
|
126
|
+
}
|
|
127
|
+
const charSet = fontMap.get(normalizedFont);
|
|
128
|
+
for (const char of text) {
|
|
129
|
+
charSet.add(char);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const walk = (el) => {
|
|
134
|
+
if (!el) return;
|
|
135
|
+
|
|
136
|
+
// Get font-family from style attribute
|
|
137
|
+
const style = el.getAttribute?.("style") || "";
|
|
138
|
+
const fontMatch = style.match(/font-family:\s*([^;]+)/i);
|
|
139
|
+
const fontFromStyle = fontMatch ? fontMatch[1] : null;
|
|
140
|
+
|
|
141
|
+
// Get font-family from font-family attribute
|
|
142
|
+
const fontFromAttr = el.getAttribute?.("font-family");
|
|
143
|
+
|
|
144
|
+
// Get font-family from CSS face attribute (for foreignObject content)
|
|
145
|
+
const faceAttr = el.getAttribute?.("face");
|
|
146
|
+
|
|
147
|
+
const fontFamily = fontFromStyle || fontFromAttr || faceAttr;
|
|
148
|
+
|
|
149
|
+
// Get text content from this element
|
|
150
|
+
// Some DOM implementations (like our svg-parser) store text directly in textContent
|
|
151
|
+
// without creating actual childNode text nodes. Handle both cases.
|
|
152
|
+
let directTextContent = "";
|
|
153
|
+
|
|
154
|
+
// First try childNodes for standard DOM implementations
|
|
155
|
+
if (el.childNodes && el.childNodes.length > 0) {
|
|
156
|
+
for (const node of el.childNodes) {
|
|
157
|
+
if (node.nodeType === 3) {
|
|
158
|
+
// TEXT_NODE
|
|
159
|
+
directTextContent += node.nodeValue || "";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// If no text found via childNodes, but this is a text/tspan element,
|
|
165
|
+
// use textContent directly (but only if it has no children to avoid double-counting)
|
|
166
|
+
const isTextElement = el.tagName === "text" || el.tagName === "tspan";
|
|
167
|
+
if (!directTextContent && isTextElement && el.textContent) {
|
|
168
|
+
// Only use textContent if there are no child elements (which would have their own fonts)
|
|
169
|
+
const hasChildElements = el.children && el.children.length > 0;
|
|
170
|
+
if (!hasChildElements) {
|
|
171
|
+
directTextContent = el.textContent;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (fontFamily && directTextContent.trim()) {
|
|
176
|
+
addCharsToFont(fontFamily, directTextContent);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Also check for text in <text> and <tspan> elements with inherited font
|
|
180
|
+
if (isTextElement) {
|
|
181
|
+
// Try to get inherited font from ancestors if no font on this element
|
|
182
|
+
let inheritedFont = fontFamily;
|
|
183
|
+
if (!inheritedFont && el.parentNode) {
|
|
184
|
+
const parentStyle = el.parentNode.getAttribute?.("style") || "";
|
|
185
|
+
const parentFontMatch = parentStyle.match(/font-family:\s*([^;]+)/i);
|
|
186
|
+
inheritedFont = parentFontMatch
|
|
187
|
+
? parentFontMatch[1]
|
|
188
|
+
: el.parentNode.getAttribute?.("font-family");
|
|
189
|
+
}
|
|
190
|
+
if (inheritedFont && !fontFamily && directTextContent.trim()) {
|
|
191
|
+
addCharsToFont(inheritedFont, directTextContent);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Recurse into children
|
|
196
|
+
if (el.children) {
|
|
197
|
+
for (const child of el.children) {
|
|
198
|
+
walk(child);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
walk(element);
|
|
204
|
+
return fontMap;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Convert font character map to URL-safe text parameter.
|
|
209
|
+
* @param {Set<string>} charSet - Set of characters
|
|
210
|
+
* @returns {string} URL-encoded unique characters
|
|
211
|
+
*/
|
|
212
|
+
export function charsToTextParam(charSet) {
|
|
213
|
+
const uniqueChars = [...charSet].sort().join("");
|
|
214
|
+
return encodeURIComponent(uniqueChars);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ============================================================================
|
|
218
|
+
// GOOGLE FONTS UTILITIES
|
|
219
|
+
// ============================================================================
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check if URL is a Google Fonts URL.
|
|
223
|
+
* @param {string} url - URL to check
|
|
224
|
+
* @returns {boolean}
|
|
225
|
+
*/
|
|
226
|
+
export function isGoogleFontsUrl(url) {
|
|
227
|
+
return (
|
|
228
|
+
url &&
|
|
229
|
+
(url.includes("fonts.googleapis.com") || url.includes("fonts.gstatic.com"))
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Extract font family name from Google Fonts URL.
|
|
235
|
+
* @param {string} url - Google Fonts URL
|
|
236
|
+
* @returns {string|null} Font family name
|
|
237
|
+
*/
|
|
238
|
+
export function extractFontFamilyFromGoogleUrl(url) {
|
|
239
|
+
try {
|
|
240
|
+
const urlObj = new URL(url);
|
|
241
|
+
const family = urlObj.searchParams.get("family");
|
|
242
|
+
if (family) {
|
|
243
|
+
// Handle "Fira+Mono" or "Fira Mono:400,700"
|
|
244
|
+
return family.split(":")[0].replace(/\+/g, " ");
|
|
245
|
+
}
|
|
246
|
+
} catch {
|
|
247
|
+
// Try regex fallback
|
|
248
|
+
const match = url.match(/family=([^&:]+)/);
|
|
249
|
+
if (match) {
|
|
250
|
+
return match[1].replace(/\+/g, " ");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Add text parameter to Google Fonts URL for character subsetting.
|
|
258
|
+
* This dramatically reduces font file size by only including needed glyphs.
|
|
259
|
+
*
|
|
260
|
+
* @param {string} url - Original Google Fonts URL
|
|
261
|
+
* @param {string} textParam - URL-encoded characters to include
|
|
262
|
+
* @returns {string} Modified URL with text parameter
|
|
263
|
+
*/
|
|
264
|
+
export function addTextParamToGoogleFontsUrl(url, textParam) {
|
|
265
|
+
if (!textParam) return url;
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const urlObj = new URL(url);
|
|
269
|
+
urlObj.searchParams.set("text", decodeURIComponent(textParam));
|
|
270
|
+
return urlObj.toString();
|
|
271
|
+
} catch {
|
|
272
|
+
// Fallback: append to URL string
|
|
273
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
274
|
+
return `${url}${separator}text=${textParam}`;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Build Google Fonts URL for a font family.
|
|
280
|
+
* @param {string} fontFamily - Font family name
|
|
281
|
+
* @param {Object} [options={}] - Options
|
|
282
|
+
* @param {string[]} [options.weights=['400']] - Font weights to include
|
|
283
|
+
* @param {string[]} [options.styles=['normal']] - Font styles
|
|
284
|
+
* @param {string} [options.text] - Characters to subset
|
|
285
|
+
* @param {string} [options.display='swap'] - Font-display value
|
|
286
|
+
* @returns {string} Google Fonts URL
|
|
287
|
+
*/
|
|
288
|
+
export function buildGoogleFontsUrl(fontFamily, options = {}) {
|
|
289
|
+
const {
|
|
290
|
+
weights = ["400"],
|
|
291
|
+
styles = ["normal"],
|
|
292
|
+
text,
|
|
293
|
+
display = "swap",
|
|
294
|
+
} = options;
|
|
295
|
+
|
|
296
|
+
const encodedFamily = fontFamily.replace(/ /g, "+");
|
|
297
|
+
const weightStr = weights.join(",");
|
|
298
|
+
|
|
299
|
+
let url = `https://fonts.googleapis.com/css2?family=${encodedFamily}:wght@${weightStr}&display=${display}`;
|
|
300
|
+
|
|
301
|
+
if (text) {
|
|
302
|
+
url += `&text=${encodeURIComponent(text)}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return url;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* List of common Google Fonts (for heuristic matching)
|
|
310
|
+
* @constant {string[]}
|
|
311
|
+
*/
|
|
312
|
+
export const POPULAR_GOOGLE_FONTS = [
|
|
313
|
+
"Roboto",
|
|
314
|
+
"Open Sans",
|
|
315
|
+
"Lato",
|
|
316
|
+
"Montserrat",
|
|
317
|
+
"Oswald",
|
|
318
|
+
"Source Sans Pro",
|
|
319
|
+
"Raleway",
|
|
320
|
+
"PT Sans",
|
|
321
|
+
"Merriweather",
|
|
322
|
+
"Noto Sans",
|
|
323
|
+
"Ubuntu",
|
|
324
|
+
"Playfair Display",
|
|
325
|
+
"Nunito",
|
|
326
|
+
"Poppins",
|
|
327
|
+
"Inter",
|
|
328
|
+
"Fira Code",
|
|
329
|
+
"Fira Sans",
|
|
330
|
+
"Work Sans",
|
|
331
|
+
"Quicksand",
|
|
332
|
+
"Inconsolata",
|
|
333
|
+
"Source Code Pro",
|
|
334
|
+
"JetBrains Mono",
|
|
335
|
+
"IBM Plex Sans",
|
|
336
|
+
"IBM Plex Mono",
|
|
337
|
+
"Libre Baskerville",
|
|
338
|
+
"Crimson Text",
|
|
339
|
+
"EB Garamond",
|
|
340
|
+
"Spectral",
|
|
341
|
+
"Bitter",
|
|
342
|
+
"Zilla Slab",
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
// ============================================================================
|
|
346
|
+
// LOCAL FONT DETECTION
|
|
347
|
+
// ============================================================================
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Get system font directories for current platform
|
|
351
|
+
* @returns {string[]} Array of font directory paths
|
|
352
|
+
*/
|
|
353
|
+
export function getSystemFontDirs() {
|
|
354
|
+
const os = platform();
|
|
355
|
+
return SYSTEM_FONT_PATHS[os] || [];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Recursively read directory, compatible with Node 18.0+
|
|
360
|
+
* Node 18.17+ has native recursive support, older versions need manual recursion
|
|
361
|
+
* @private
|
|
362
|
+
* @param {string} dir - Directory to read
|
|
363
|
+
* @returns {Array<{name: string, path: string, isDirectory: () => boolean}>}
|
|
364
|
+
*/
|
|
365
|
+
function readdirRecursive(dir) {
|
|
366
|
+
const results = [];
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
370
|
+
|
|
371
|
+
for (const entry of entries) {
|
|
372
|
+
const fullPath = join(dir, entry.name);
|
|
373
|
+
|
|
374
|
+
if (entry.isDirectory()) {
|
|
375
|
+
// Recurse into subdirectory
|
|
376
|
+
results.push(...readdirRecursive(fullPath));
|
|
377
|
+
} else {
|
|
378
|
+
results.push({
|
|
379
|
+
name: entry.name,
|
|
380
|
+
path: fullPath,
|
|
381
|
+
isDirectory: () => false,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
} catch {
|
|
386
|
+
// Skip inaccessible directories
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return results;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Check if a font is installed locally on the system.
|
|
394
|
+
* @param {string} fontFamily - Font family name to check
|
|
395
|
+
* @returns {Promise<{found: boolean, path?: string}>}
|
|
396
|
+
*/
|
|
397
|
+
export async function checkLocalFont(fontFamily) {
|
|
398
|
+
const fontDirs = getSystemFontDirs();
|
|
399
|
+
const normalizedName = fontFamily.toLowerCase().replace(/ /g, "");
|
|
400
|
+
|
|
401
|
+
// Common font file naming patterns
|
|
402
|
+
const patterns = [
|
|
403
|
+
fontFamily.replace(/ /g, ""),
|
|
404
|
+
fontFamily.replace(/ /g, "-"),
|
|
405
|
+
fontFamily.replace(/ /g, "_"),
|
|
406
|
+
normalizedName,
|
|
407
|
+
];
|
|
408
|
+
|
|
409
|
+
for (const dir of fontDirs) {
|
|
410
|
+
if (!existsSync(dir)) continue;
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
const files = readdirRecursive(dir);
|
|
414
|
+
|
|
415
|
+
for (const file of files) {
|
|
416
|
+
if (file.isDirectory()) continue;
|
|
417
|
+
|
|
418
|
+
const ext = extname(file.name).toLowerCase();
|
|
419
|
+
if (!FONT_FORMATS[ext]) continue;
|
|
420
|
+
|
|
421
|
+
const baseName = basename(file.name, ext).toLowerCase();
|
|
422
|
+
|
|
423
|
+
for (const pattern of patterns) {
|
|
424
|
+
if (baseName.includes(pattern.toLowerCase())) {
|
|
425
|
+
return {
|
|
426
|
+
found: true,
|
|
427
|
+
path: file.path,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
// Skip inaccessible directories
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return { found: false };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* List all installed system fonts.
|
|
442
|
+
* @returns {Promise<Array<{name: string, path: string, format: string}>>}
|
|
443
|
+
*/
|
|
444
|
+
export async function listSystemFonts() {
|
|
445
|
+
const fonts = [];
|
|
446
|
+
const fontDirs = getSystemFontDirs();
|
|
447
|
+
|
|
448
|
+
for (const dir of fontDirs) {
|
|
449
|
+
if (!existsSync(dir)) continue;
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
const files = readdirRecursive(dir);
|
|
453
|
+
|
|
454
|
+
for (const file of files) {
|
|
455
|
+
if (file.isDirectory()) continue;
|
|
456
|
+
|
|
457
|
+
const ext = extname(file.name).toLowerCase();
|
|
458
|
+
const format = FONT_FORMATS[ext];
|
|
459
|
+
if (!format) continue;
|
|
460
|
+
|
|
461
|
+
const name = basename(file.name, ext);
|
|
462
|
+
fonts.push({
|
|
463
|
+
name,
|
|
464
|
+
path: file.path,
|
|
465
|
+
format: ext.slice(1),
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
} catch {
|
|
469
|
+
// Skip inaccessible directories
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return fonts;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ============================================================================
|
|
477
|
+
// EXTERNAL TOOL INTEGRATION
|
|
478
|
+
// ============================================================================
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Check if a command exists in PATH
|
|
482
|
+
* Cross-platform: uses 'where' on Windows, 'which' on Unix
|
|
483
|
+
* @param {string} cmd - Command name
|
|
484
|
+
* @returns {boolean}
|
|
485
|
+
*/
|
|
486
|
+
export function commandExists(cmd) {
|
|
487
|
+
try {
|
|
488
|
+
// Windows uses 'where', Unix uses 'which'
|
|
489
|
+
const checkCmd = platform() === "win32" ? "where" : "which";
|
|
490
|
+
execSync(`${checkCmd} ${cmd}`, { stdio: "ignore" });
|
|
491
|
+
return true;
|
|
492
|
+
} catch {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Download font using FontGet (npm package)
|
|
499
|
+
* @param {string} fontFamily - Font family name
|
|
500
|
+
* @param {string} outputDir - Output directory
|
|
501
|
+
* @returns {Promise<{success: boolean, path?: string, error?: string}>}
|
|
502
|
+
*/
|
|
503
|
+
export async function downloadWithFontGet(fontFamily, outputDir) {
|
|
504
|
+
if (!commandExists("fontget")) {
|
|
505
|
+
return { success: false, error: "FontGet not installed. Run: npm install -g fontget" };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
mkdirSync(outputDir, { recursive: true });
|
|
510
|
+
execFileSync("fontget", [fontFamily, "-o", outputDir], {
|
|
511
|
+
stdio: "pipe",
|
|
512
|
+
timeout: 60000,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// Find downloaded file
|
|
516
|
+
const files = readdirSync(outputDir);
|
|
517
|
+
const fontFile = files.find((f) => Object.keys(FONT_FORMATS).some((ext) => f.endsWith(ext)));
|
|
518
|
+
|
|
519
|
+
if (fontFile) {
|
|
520
|
+
return { success: true, path: join(outputDir, fontFile) };
|
|
521
|
+
}
|
|
522
|
+
return { success: false, error: "No font file found after download" };
|
|
523
|
+
} catch (err) {
|
|
524
|
+
return { success: false, error: err.message };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Download font using fnt (Homebrew package)
|
|
530
|
+
* @param {string} fontFamily - Font family name
|
|
531
|
+
* @param {string} outputDir - Output directory
|
|
532
|
+
* @returns {Promise<{success: boolean, path?: string, error?: string}>}
|
|
533
|
+
*/
|
|
534
|
+
export async function downloadWithFnt(fontFamily, outputDir) {
|
|
535
|
+
if (!commandExists("fnt")) {
|
|
536
|
+
return { success: false, error: "fnt not installed. Run: brew install alexmyczko/fnt/fnt" };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
mkdirSync(outputDir, { recursive: true });
|
|
541
|
+
execFileSync("fnt", ["install", fontFamily], {
|
|
542
|
+
stdio: "pipe",
|
|
543
|
+
timeout: 60000,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// fnt installs to ~/.fonts by default
|
|
547
|
+
const fntFontsDir = join(homedir(), ".fonts");
|
|
548
|
+
if (existsSync(fntFontsDir)) {
|
|
549
|
+
const files = readdirSync(fntFontsDir);
|
|
550
|
+
const normalizedName = fontFamily.toLowerCase().replace(/ /g, "");
|
|
551
|
+
const fontFile = files.find((f) => {
|
|
552
|
+
const base = basename(f, extname(f)).toLowerCase();
|
|
553
|
+
return (
|
|
554
|
+
base.includes(normalizedName) &&
|
|
555
|
+
Object.keys(FONT_FORMATS).some((ext) => f.endsWith(ext))
|
|
556
|
+
);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
if (fontFile) {
|
|
560
|
+
const srcPath = join(fntFontsDir, fontFile);
|
|
561
|
+
const destPath = join(outputDir, fontFile);
|
|
562
|
+
copyFileSync(srcPath, destPath);
|
|
563
|
+
return { success: true, path: destPath };
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return { success: false, error: "Font installed but file not found" };
|
|
567
|
+
} catch (err) {
|
|
568
|
+
return { success: false, error: err.message };
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ============================================================================
|
|
573
|
+
// REPLACEMENT MAP HANDLING
|
|
574
|
+
// ============================================================================
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Load font replacement map from YAML file.
|
|
578
|
+
*
|
|
579
|
+
* @param {string} [mapPath] - Path to YAML file. If not provided, checks:
|
|
580
|
+
* 1. SVGM_REPLACEMENT_MAP environment variable
|
|
581
|
+
* 2. ./svgm_replacement_map.yml in current directory
|
|
582
|
+
* @returns {{replacements: Object, options: Object} | null} Parsed map or null if not found
|
|
583
|
+
*/
|
|
584
|
+
export function loadReplacementMap(mapPath) {
|
|
585
|
+
// Priority: explicit path > env var > default file
|
|
586
|
+
const pathsToTry = [
|
|
587
|
+
mapPath,
|
|
588
|
+
process.env[ENV_REPLACEMENT_MAP],
|
|
589
|
+
join(process.cwd(), DEFAULT_REPLACEMENT_MAP),
|
|
590
|
+
join(process.cwd(), "svgm_replacement_map_default.yml"),
|
|
591
|
+
].filter(Boolean);
|
|
592
|
+
|
|
593
|
+
for (const p of pathsToTry) {
|
|
594
|
+
if (existsSync(p)) {
|
|
595
|
+
try {
|
|
596
|
+
const content = readFileSync(p, "utf8");
|
|
597
|
+
// Use FAILSAFE_SCHEMA for security (no function execution)
|
|
598
|
+
const parsed = yaml.load(content, { schema: yaml.FAILSAFE_SCHEMA });
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
replacements: parsed.replacements || {},
|
|
602
|
+
options: parsed.options || {
|
|
603
|
+
default_embed: true,
|
|
604
|
+
default_subset: true,
|
|
605
|
+
fallback_source: "google",
|
|
606
|
+
auto_download: true,
|
|
607
|
+
},
|
|
608
|
+
path: p,
|
|
609
|
+
};
|
|
610
|
+
} catch (err) {
|
|
611
|
+
throw new Error(`Failed to parse replacement map ${p}: ${err.message}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Apply font replacements to an SVG document.
|
|
621
|
+
*
|
|
622
|
+
* @param {Object} doc - Parsed SVG document
|
|
623
|
+
* @param {Object} replacements - Font replacement map {original: replacement}
|
|
624
|
+
* @returns {{modified: boolean, replaced: Array<{from: string, to: string}>}}
|
|
625
|
+
*/
|
|
626
|
+
export function applyFontReplacements(doc, replacements) {
|
|
627
|
+
const result = { modified: false, replaced: [] };
|
|
628
|
+
|
|
629
|
+
const replaceInStyle = (styleStr) => {
|
|
630
|
+
let modified = styleStr;
|
|
631
|
+
for (const [original, replacement] of Object.entries(replacements)) {
|
|
632
|
+
const pattern = new RegExp(
|
|
633
|
+
`(font-family:\\s*)(['"]?)${escapeRegex(original)}\\2`,
|
|
634
|
+
"gi"
|
|
635
|
+
);
|
|
636
|
+
if (pattern.test(modified)) {
|
|
637
|
+
const newValue =
|
|
638
|
+
typeof replacement === "string" ? replacement : replacement.replacement;
|
|
639
|
+
modified = modified.replace(pattern, `$1$2${newValue}$2`);
|
|
640
|
+
result.replaced.push({ from: original, to: newValue });
|
|
641
|
+
result.modified = true;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return modified;
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const replaceInAttribute = (el, attrName) => {
|
|
648
|
+
const value = el.getAttribute(attrName);
|
|
649
|
+
if (!value) return;
|
|
650
|
+
|
|
651
|
+
for (const [original, replacement] of Object.entries(replacements)) {
|
|
652
|
+
const pattern = new RegExp(`^(['"]?)${escapeRegex(original)}\\1$`, "i");
|
|
653
|
+
if (pattern.test(value.trim())) {
|
|
654
|
+
const newValue =
|
|
655
|
+
typeof replacement === "string" ? replacement : replacement.replacement;
|
|
656
|
+
el.setAttribute(attrName, newValue);
|
|
657
|
+
result.replaced.push({ from: original, to: newValue });
|
|
658
|
+
result.modified = true;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
// Walk all elements
|
|
664
|
+
const walk = (el) => {
|
|
665
|
+
if (!el) return;
|
|
666
|
+
|
|
667
|
+
// Replace in style attribute
|
|
668
|
+
const style = el.getAttribute?.("style");
|
|
669
|
+
if (style) {
|
|
670
|
+
const newStyle = replaceInStyle(style);
|
|
671
|
+
if (newStyle !== style) {
|
|
672
|
+
el.setAttribute("style", newStyle);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Replace in font-family attribute
|
|
677
|
+
replaceInAttribute(el, "font-family");
|
|
678
|
+
|
|
679
|
+
// Replace in face attribute (for foreignObject)
|
|
680
|
+
replaceInAttribute(el, "face");
|
|
681
|
+
|
|
682
|
+
// Recurse
|
|
683
|
+
if (el.children) {
|
|
684
|
+
for (const child of el.children) {
|
|
685
|
+
walk(child);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
// Also check <style> elements
|
|
691
|
+
const styleElements = doc.querySelectorAll?.("style") || [];
|
|
692
|
+
for (const styleEl of styleElements) {
|
|
693
|
+
if (styleEl.textContent) {
|
|
694
|
+
const newContent = replaceInStyle(styleEl.textContent);
|
|
695
|
+
if (newContent !== styleEl.textContent) {
|
|
696
|
+
styleEl.textContent = newContent;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
walk(doc.documentElement || doc);
|
|
702
|
+
|
|
703
|
+
return result;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Escape string for use in regex
|
|
708
|
+
* @param {string} str - String to escape
|
|
709
|
+
* @returns {string}
|
|
710
|
+
*/
|
|
711
|
+
function escapeRegex(str) {
|
|
712
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ============================================================================
|
|
716
|
+
// FONT LISTING AND ANALYSIS
|
|
717
|
+
// ============================================================================
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Font information structure
|
|
721
|
+
* @typedef {Object} FontInfo
|
|
722
|
+
* @property {string} family - Font family name
|
|
723
|
+
* @property {string} type - 'embedded' | 'external' | 'system'
|
|
724
|
+
* @property {string} [source] - URL or path for external fonts
|
|
725
|
+
* @property {number} [size] - Size in bytes for embedded fonts
|
|
726
|
+
* @property {Set<string>} [usedChars] - Characters used in SVG
|
|
727
|
+
*/
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* List all fonts used in an SVG document.
|
|
731
|
+
*
|
|
732
|
+
* @param {Object} doc - Parsed SVG document
|
|
733
|
+
* @returns {FontInfo[]} Array of font information
|
|
734
|
+
*/
|
|
735
|
+
export function listFonts(doc) {
|
|
736
|
+
const fonts = new Map();
|
|
737
|
+
|
|
738
|
+
// Get character usage map
|
|
739
|
+
const charMap = extractFontCharacterMap(doc.documentElement || doc);
|
|
740
|
+
|
|
741
|
+
// Find @font-face rules in <style> elements
|
|
742
|
+
const styleElements = doc.querySelectorAll?.("style") || [];
|
|
743
|
+
for (const styleEl of styleElements) {
|
|
744
|
+
const css = styleEl.textContent || "";
|
|
745
|
+
const fontFaceRegex = /@font-face\s*\{([^}]*)\}/gi;
|
|
746
|
+
let match;
|
|
747
|
+
|
|
748
|
+
while ((match = fontFaceRegex.exec(css)) !== null) {
|
|
749
|
+
const block = match[1];
|
|
750
|
+
|
|
751
|
+
// Extract font-family
|
|
752
|
+
const familyMatch = block.match(/font-family:\s*(['"]?)([^;'"]+)\1/i);
|
|
753
|
+
const family = familyMatch ? familyMatch[2].trim() : null;
|
|
754
|
+
if (!family) continue;
|
|
755
|
+
|
|
756
|
+
// Extract src url
|
|
757
|
+
const srcMatch = block.match(/src:\s*url\((['"]?)([^)'"]+)\1\)/i);
|
|
758
|
+
const src = srcMatch ? srcMatch[2] : null;
|
|
759
|
+
|
|
760
|
+
// Determine type
|
|
761
|
+
let type = "embedded";
|
|
762
|
+
let source = null;
|
|
763
|
+
|
|
764
|
+
if (src) {
|
|
765
|
+
if (src.startsWith("data:")) {
|
|
766
|
+
type = "embedded";
|
|
767
|
+
// Calculate size from base64
|
|
768
|
+
const base64Match = src.match(/base64,(.+)/);
|
|
769
|
+
if (base64Match) {
|
|
770
|
+
const size = Math.ceil((base64Match[1].length * 3) / 4);
|
|
771
|
+
if (!fonts.has(family)) {
|
|
772
|
+
fonts.set(family, { family, type, size, usedChars: charMap.get(family) });
|
|
773
|
+
}
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
} else {
|
|
777
|
+
type = "external";
|
|
778
|
+
source = src;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (!fonts.has(family)) {
|
|
783
|
+
fonts.set(family, { family, type, source, usedChars: charMap.get(family) });
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Add fonts from character map that aren't in @font-face (system fonts)
|
|
789
|
+
for (const [family, chars] of charMap) {
|
|
790
|
+
if (!fonts.has(family)) {
|
|
791
|
+
fonts.set(family, {
|
|
792
|
+
family,
|
|
793
|
+
type: WEB_SAFE_FONTS.includes(family) ? "system" : "unknown",
|
|
794
|
+
usedChars: chars,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return Array.from(fonts.values());
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ============================================================================
|
|
803
|
+
// BACKUP SYSTEM
|
|
804
|
+
// ============================================================================
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Create a backup of a file before modifying it.
|
|
808
|
+
*
|
|
809
|
+
* @param {string} filePath - Path to file to backup
|
|
810
|
+
* @param {Object} [options={}] - Options
|
|
811
|
+
* @param {boolean} [options.noBackup=false] - Skip backup creation
|
|
812
|
+
* @param {number} [options.maxBackups=100] - Maximum numbered backups
|
|
813
|
+
* @returns {string|null} Path to backup file, or null if skipped
|
|
814
|
+
*/
|
|
815
|
+
export function createBackup(filePath, options = {}) {
|
|
816
|
+
const { noBackup = false, maxBackups = 100 } = options;
|
|
817
|
+
|
|
818
|
+
if (noBackup || !existsSync(filePath)) {
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const backupPath = `${filePath}.bak`;
|
|
823
|
+
|
|
824
|
+
// If backup exists, create numbered backup
|
|
825
|
+
let counter = 1;
|
|
826
|
+
let finalBackupPath = backupPath;
|
|
827
|
+
|
|
828
|
+
while (existsSync(finalBackupPath)) {
|
|
829
|
+
finalBackupPath = `${filePath}.bak.${counter++}`;
|
|
830
|
+
if (counter > maxBackups) {
|
|
831
|
+
throw new Error(`Too many backup files (>${maxBackups}). Clean up old backups.`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
copyFileSync(filePath, finalBackupPath);
|
|
836
|
+
return finalBackupPath;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// ============================================================================
|
|
840
|
+
// VALIDATION
|
|
841
|
+
// ============================================================================
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Validate SVG after font operations.
|
|
845
|
+
*
|
|
846
|
+
* @param {string} svgContent - SVG content to validate
|
|
847
|
+
* @param {string} [operation] - Operation that was performed
|
|
848
|
+
* @returns {{valid: boolean, warnings: string[], errors: string[]}}
|
|
849
|
+
*/
|
|
850
|
+
export async function validateSvgAfterFontOperation(svgContent, operation) {
|
|
851
|
+
const result = { valid: true, warnings: [], errors: [] };
|
|
852
|
+
|
|
853
|
+
try {
|
|
854
|
+
// Dynamic import JSDOM
|
|
855
|
+
const { JSDOM } = await import("jsdom");
|
|
856
|
+
const dom = new JSDOM(svgContent, { contentType: "image/svg+xml" });
|
|
857
|
+
const doc = dom.window.document;
|
|
858
|
+
|
|
859
|
+
// Check for parser errors
|
|
860
|
+
const parseError = doc.querySelector("parsererror");
|
|
861
|
+
if (parseError) {
|
|
862
|
+
result.valid = false;
|
|
863
|
+
result.errors.push(`Invalid SVG: ${parseError.textContent}`);
|
|
864
|
+
return result;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Verify SVG root element exists
|
|
868
|
+
const svg = doc.querySelector("svg");
|
|
869
|
+
if (!svg) {
|
|
870
|
+
result.valid = false;
|
|
871
|
+
result.errors.push("Missing <svg> root element");
|
|
872
|
+
return result;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Operation-specific validation
|
|
876
|
+
if (operation === "embed") {
|
|
877
|
+
// Check for remaining external font URLs
|
|
878
|
+
const styleElements = doc.querySelectorAll("style");
|
|
879
|
+
for (const style of styleElements) {
|
|
880
|
+
const css = style.textContent || "";
|
|
881
|
+
const urlMatches = css.match(/url\((['"]?)(?!data:)([^)'"]+)\1\)/gi);
|
|
882
|
+
if (urlMatches) {
|
|
883
|
+
for (const match of urlMatches) {
|
|
884
|
+
if (
|
|
885
|
+
match.includes("fonts.googleapis.com") ||
|
|
886
|
+
match.includes("fonts.gstatic.com")
|
|
887
|
+
) {
|
|
888
|
+
result.warnings.push(`External font URL still present: ${match}`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (operation === "replace") {
|
|
896
|
+
// Verify font families are valid
|
|
897
|
+
const fonts = listFonts(doc);
|
|
898
|
+
for (const font of fonts) {
|
|
899
|
+
if (font.type === "unknown" && font.usedChars?.size > 0) {
|
|
900
|
+
result.warnings.push(
|
|
901
|
+
`Font "${font.family}" is used but not defined or recognized`
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
dom.window.close();
|
|
908
|
+
} catch (err) {
|
|
909
|
+
result.valid = false;
|
|
910
|
+
result.errors.push(`Validation failed: ${err.message}`);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
return result;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// ============================================================================
|
|
917
|
+
// TEMPLATE GENERATION
|
|
918
|
+
// ============================================================================
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Generate YAML replacement map template
|
|
922
|
+
* @returns {string} YAML template content
|
|
923
|
+
*/
|
|
924
|
+
export function generateReplacementMapTemplate() {
|
|
925
|
+
return `# SVG Font Replacement Map
|
|
926
|
+
# ========================
|
|
927
|
+
# This file defines font replacements for SVG processing.
|
|
928
|
+
#
|
|
929
|
+
# Format:
|
|
930
|
+
# original_font: replacement_font
|
|
931
|
+
#
|
|
932
|
+
# Examples:
|
|
933
|
+
# "Arial": "Inter" # Replace Arial with Inter
|
|
934
|
+
# "Times New Roman": "Noto Serif"
|
|
935
|
+
#
|
|
936
|
+
# Font sources (in priority order):
|
|
937
|
+
# 1. Local system fonts
|
|
938
|
+
# 2. Google Fonts (default, free)
|
|
939
|
+
# 3. FontGet (npm: fontget)
|
|
940
|
+
# 4. fnt (brew: alexmyczko/fnt/fnt)
|
|
941
|
+
#
|
|
942
|
+
# Options per font:
|
|
943
|
+
# embed: true # Embed as base64 (default: true)
|
|
944
|
+
# subset: true # Only include used glyphs (default: true)
|
|
945
|
+
# source: "google" # Force specific source
|
|
946
|
+
# weight: "400,700" # Specific weights to include
|
|
947
|
+
# style: "normal,italic" # Specific styles
|
|
948
|
+
#
|
|
949
|
+
# Advanced format:
|
|
950
|
+
# "Arial":
|
|
951
|
+
# replacement: "Inter"
|
|
952
|
+
# embed: true
|
|
953
|
+
# subset: true
|
|
954
|
+
# source: "google"
|
|
955
|
+
# weights: ["400", "500", "700"]
|
|
956
|
+
|
|
957
|
+
replacements:
|
|
958
|
+
# Add your font mappings here
|
|
959
|
+
# "Original Font": "Replacement Font"
|
|
960
|
+
|
|
961
|
+
options:
|
|
962
|
+
default_embed: true
|
|
963
|
+
default_subset: true
|
|
964
|
+
fallback_source: "google"
|
|
965
|
+
auto_download: true
|
|
966
|
+
`;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// ============================================================================
|
|
970
|
+
// EXPORTS
|
|
971
|
+
// ============================================================================
|
|
972
|
+
|
|
973
|
+
export default {
|
|
974
|
+
// Character extraction
|
|
975
|
+
extractFontCharacterMap,
|
|
976
|
+
charsToTextParam,
|
|
977
|
+
|
|
978
|
+
// Google Fonts
|
|
979
|
+
isGoogleFontsUrl,
|
|
980
|
+
extractFontFamilyFromGoogleUrl,
|
|
981
|
+
addTextParamToGoogleFontsUrl,
|
|
982
|
+
buildGoogleFontsUrl,
|
|
983
|
+
POPULAR_GOOGLE_FONTS,
|
|
984
|
+
|
|
985
|
+
// Local fonts
|
|
986
|
+
getSystemFontDirs,
|
|
987
|
+
checkLocalFont,
|
|
988
|
+
listSystemFonts,
|
|
989
|
+
|
|
990
|
+
// External tools
|
|
991
|
+
commandExists,
|
|
992
|
+
downloadWithFontGet,
|
|
993
|
+
downloadWithFnt,
|
|
994
|
+
|
|
995
|
+
// Replacement map
|
|
996
|
+
loadReplacementMap,
|
|
997
|
+
applyFontReplacements,
|
|
998
|
+
generateReplacementMapTemplate,
|
|
999
|
+
|
|
1000
|
+
// Font listing
|
|
1001
|
+
listFonts,
|
|
1002
|
+
|
|
1003
|
+
// Backup and validation
|
|
1004
|
+
createBackup,
|
|
1005
|
+
validateSvgAfterFontOperation,
|
|
1006
|
+
|
|
1007
|
+
// Constants
|
|
1008
|
+
DEFAULT_REPLACEMENT_MAP,
|
|
1009
|
+
ENV_REPLACEMENT_MAP,
|
|
1010
|
+
WEB_SAFE_FONTS,
|
|
1011
|
+
FONT_FORMATS,
|
|
1012
|
+
SYSTEM_FONT_PATHS,
|
|
1013
|
+
};
|