-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfont_to_svg_path.js
More file actions
165 lines (155 loc) · 6.11 KB
/
font_to_svg_path.js
File metadata and controls
165 lines (155 loc) · 6.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#!/usr/bin/env node
/**
* Helper: convert text+local font file to a single SVG path.
* Tries google-font-to-svg-path; falls back to opentype.js for local fonts.
* Usage: node font_to_svg_path.js --font <path> --text <text> [--fontSize 72] [--letterSpacing 0]
* Outputs JSON: { path: string, width: number, height: number }
*/
const fs = require('fs');
const path = require('path');
function arg(name, def) {
const i = process.argv.indexOf(`--${name}`);
if (i >= 0 && i + 1 < process.argv.length) return process.argv[i + 1];
return def;
}
async function tryGoogleFontToSvgPath(fontFile, text, fontSize, letterSpacing) {
try {
const gfts = require('google-font-to-svg-path');
const toPath = gfts.default || gfts;
const res = await toPath({
text,
font: `local:${path.resolve(fontFile)}`,
fontSize,
letterSpacing,
normalize: true,
});
return { path: res.path || '', width: Number(res.width || 0), height: Number(res.height || 0) };
} catch (e) {
return null;
}
}
async function viaOpenType(fontFile, text, fontSize, letterSpacing) {
const opentype = require('opentype.js');
const font = await new Promise((resolve, reject) => {
opentype.load(fontFile, (err, f) => (err ? reject(err) : resolve(f)));
});
const scale = 1;
const baseline = 0; // opentype path is relative to baseline
let x = 0;
let y = baseline;
let d = '';
const commandsToPath = (cmds) => {
let s = '';
for (const c of cmds) {
if (c.type === 'M') s += `M${c.x} ${-c.y}`;
else if (c.type === 'L') s += `L${c.x} ${-c.y}`;
else if (c.type === 'C') s += `C${c.x1} ${-c.y1} ${c.x2} ${-c.y2} ${c.x} ${-c.y}`;
else if (c.type === 'Q') s += `Q${c.x1} ${-c.y1} ${c.x} ${-c.y}`;
else if (c.type === 'Z') s += 'Z';
}
return s;
};
for (const ch of text) {
const glyph = font.charToGlyph(ch);
const gPath = glyph.getPath(x, y, fontSize, { kerning: true });
d += commandsToPath(gPath.commands);
const adv = glyph.advanceWidth * (fontSize / font.unitsPerEm);
x += adv + letterSpacing;
}
// width as accumulated x, approximate height from font metrics
const width = x;
const ascent = font.ascender * (fontSize / font.unitsPerEm);
const descent = font.descender * (fontSize / font.unitsPerEm);
const height = ascent - descent;
return { path: d, width, height };
}
async function viaOpenTypeMulti(fontFile, text, fontSize, letterSpacing, lineSpacingFactor, align = 'left') {
const opentype = require('opentype.js');
const font = await new Promise((resolve, reject) => {
opentype.load(fontFile, (err, f) => (err ? reject(err) : resolve(f)));
});
const ascent = font.ascender * (fontSize / font.unitsPerEm);
const descent = font.descender * (fontSize / font.unitsPerEm);
const baseLineHeight = ascent - descent;
const lineHeight = baseLineHeight * (1 + (isFinite(lineSpacingFactor) ? lineSpacingFactor : 0.2));
const commandsToPath = (cmds) => {
let s = '';
for (const c of cmds) {
if (c.type === 'M') s += `M${c.x} ${-c.y}`;
else if (c.type === 'L') s += `L${c.x} ${-c.y}`;
else if (c.type === 'C') s += `C${c.x1} ${-c.y1} ${c.x2} ${-c.y2} ${c.x} ${-c.y}`;
else if (c.type === 'Q') s += `Q${c.x1} ${-c.y1} ${c.x} ${-c.y}`;
else if (c.type === 'Z') s += 'Z';
}
return s;
};
const lines = String(text).split(/\r?\n/);
// First pass: measure each line width
const widths = lines.map((line) => {
let x = 0;
for (const ch of line) {
const glyph = font.charToGlyph(ch);
const adv = glyph.advanceWidth * (fontSize / font.unitsPerEm);
x += adv + letterSpacing;
}
return x;
});
const maxWidth = widths.reduce((a, b) => Math.max(a, b), 0);
// Second pass: build path with alignment offset per line
let d = '';
lines.forEach((line, idx) => {
let x = 0;
const w = widths[idx];
let xOffset = 0;
const a = String(align || 'left').toLowerCase();
if (a === 'right') xOffset = maxWidth - w;
else if (a === 'center' || a === 'centre') xOffset = (maxWidth - w) / 2;
const y = idx * lineHeight;
for (const ch of line) {
const glyph = font.charToGlyph(ch);
const gPath = glyph.getPath(x + xOffset, y, fontSize, { kerning: true });
d += commandsToPath(gPath.commands);
const adv = glyph.advanceWidth * (fontSize / font.unitsPerEm);
x += adv + letterSpacing;
}
});
const totalHeight = Math.max(lineHeight * lines.length, baseLineHeight);
return { path: d, width: maxWidth, height: totalHeight };
}
(async () => {
try {
const fontFile = arg('font');
const text = arg('text', '') || '';
const fontSize = parseFloat(arg('fontSize', '72'));
const letterSpacing = parseFloat(arg('letterSpacing', '0'));
const lineSpacing = parseFloat(arg('lineSpacing', '0.2'));
const align = String(arg('align', 'left') || 'left').toLowerCase();
if (!fontFile) throw new Error('Missing --font');
let result = null;
const multi = text.includes('\n') || text.includes('\r');
if (multi) {
// Multi-line: OpenType with alignment
result = await viaOpenTypeMulti(fontFile, text, fontSize, letterSpacing, lineSpacing, align);
} else {
// Single-line: if alignment requested, use OpenType so we can offset reliably
if (align !== 'left') {
result = await viaOpenTypeMulti(fontFile, text, fontSize, letterSpacing, lineSpacing, align);
} else {
// Try google font path first (fast), then fallback
result = await tryGoogleFontToSvgPath(fontFile, text, fontSize, letterSpacing);
if (!result) {
try {
result = await viaOpenType(fontFile, text, fontSize, letterSpacing);
} catch (e) {
console.error('Please install dependencies: npm i google-font-to-svg-path opentype.js');
throw e;
}
}
}
}
process.stdout.write(JSON.stringify(result));
} catch (err) {
console.error(String((err && err.stack) || err));
process.exit(1);
}
})();