-
Notifications
You must be signed in to change notification settings - Fork 0
/
svg-annotator.js
executable file
·321 lines (280 loc) · 9.96 KB
/
svg-annotator.js
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
#!/usr/bin/env node
const process = require('process');
const cp = require('child_process');
const fs = require('fs');
const path = require('path');
//======================================================================
// Global variables
//======================================================================
const OUT = 'out';
const INKSCAPE = (process.platform === 'win32') ? 'inkscapecom.com' : 'inkscape';
const SCALE = 0.92;
let debug = false;
//======================================================================
// Helper Routine
//======================================================================
const debug_log = (obj) => {
if (!!debug) {
if ((typeof(obj) === 'object') && !Array.isArray(obj)) {
console.log(JSON.stringify(obj, null, 4));
} else {
console.log(obj);
}
}
}
//======================================================================
// Shell Emulation Routines
//======================================================================
const directories = [];
const pushd = (dir) => {
directories.push(process.cwd());
process.chdir(dir);
};
const popd = () => {
let dir = directories.pop();
if (dir) {
process.chdir(dir);
}
};
// https://stackoverflow.com/a/31104898/19336104
const termSync = (commandLine) => {
debug_log(`> ${commandLine}`);
cp.execSync(commandLine, {stdio: 'inherit'});
};
// Equivalent to `mkdir -p`
const mkdirSync = (dir) => {
const cwd = process.cwd();
dir.split(path.sep).forEach((subdir) => {
if (!fs.existsSync(subdir)) {
fs.mkdirSync(subdir);
}
process.chdir(subdir);
});
process.chdir(cwd);
};
const where = (command) => {
try {
if (process.platform === 'win32') {
return cp.execSync(`where ${command}`, {stdio: 'pipe'});
}
return cp.execSync(`command -v ${command}`, {stdio: 'pipe'});
} catch (err) {
// ignore error
}
return undefined;
};
const cmdExist = (command) => {
return !!where(command);
};
//======================================================================
// Package Manager Handler
//======================================================================
class Pacman {
constructor() {
this.name = this.#getPacman();
// https://www.digitalocean.com/community/tutorials/nodejs-npm-yarn-cheatsheet
if (this.name === 'yarn') {
this.cmdMap = {
'install': 'install',
'add': 'add'
};
} else {
this.cmdMap = {
'install': 'install',
'add': 'install'
};
}
}
#getPacman = () => {
// Preference order of package manager: pnpm > yarn > npm
// NOTE: Parent process can be queried by using process.env.npm_execpath - to derive package manager.
// https://stackoverflow.com/a/51793644
const exec = path.basename(process.env.npm_execpath ?? "").split(".").shift();
switch (exec) {
case "yarn":
return "yarn";
case "pnpm":
// 2 main extensions, pnpm.exe (none in UNIX) vs pnpm.cjs
return "pnpm";
case "npm-cli":
return "npm";
default:
const yarnExist = cmdExist("yarn");
const pnpmExist = cmdExist("pnpm");
return fs.existsSync("pnpm-lock.yaml") && pnpmExist
? "pnpm"
: fs.existsSync("yarn.lock") && yarnExist
? "yarn"
: pnpmExist
? "pnpm"
: yarnExist
? "yarn"
: "npm";
}
};
install = () => {
termSync(`${this.name} ${this.cmdMap['install']}`);
};
add = (dependency) => {
termSync(`${this.name} ${this.cmdMap['add']} ${dependency}`);
};
}
//======================================================================
// Script Entry
//======================================================================
const script = path.basename(__filename);
const timed = `Total time for ${script}`;
console.time(timed);
pushd(__dirname);
// Check dependencies
if (!cmdExist(INKSCAPE)) {
console.log(`"${INKSCAPE}" is not installed!`);
console.log('Output will only be in SVG...');
// process.exit(1);
}
// Prepare default package.json, variable cannot use package as name
let project = {
name: `${path.basename(script, path.extname(script))}`,
version: '1.0.0',
description: 'Primitive single-script (JavaScript) to annotate name into SVG image',
main: `${script}`,
scripts: {
start: 'node .',
},
author: `${process.env.USERNAME}`,
license: 'MIT',
};
// Select package manager
const pacman = new Pacman();
// Prepare new package.json
const packageExist = fs.existsSync('package.json');
if (packageExist) {
project = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
} else {
fs.writeFileSync('package.json', JSON.stringify(project, null, 4));
}
// Make sure dependencies are added, whether its new or started from single file.
const addDependencies = !project.hasOwnProperty('dependencies');
if (addDependencies || !project.dependencies.hasOwnProperty('jsdom')) {
pacman.add('jsdom');
}
if (addDependencies || !project.dependencies.hasOwnProperty('yargs')) {
pacman.add('yargs');
}
// Load package.json if it exist originally
if (packageExist) {
pacman.install();
}
//======================================================================
// Parse args
//======================================================================
// Ignore first 2 arguments <node exetubale> and <script>
const argv = require('yargs')(process.argv.slice(2))
.usage('Usage: node $0 -n [name] -t [template]')
.alias('v', 'version')
.example('node $0 -n "Misaka Mikoto"', 'Annotates template.svg (default template SVG) with "Misaka Mikoto"')
// Name(s)
.default('n', 'names.txt')
.alias('n', 'name')
.nargs('n', 1)
.describe('n', 'Load a name or a file')
// Template SVG
.default('t', 'template.svg')
.alias('t', 'template')
.nargs('t', 1)
.describe('t', 'template SVG file to be annotated')
// Font
.default('font-style', 'cmmi10')
.nargs('font-style', 1)
.describe('font-style', 'Font style for the name(s), need to make sure inkscape recognize this')
.default('font-size', 14)
.nargs('font-size', 1)
.describe('font-size', 'Font size for the name(s)')
.default('font-scale', 0.92)
.nargs('font-scale', 1)
.describe('font-scale', 'Font scale for the name(s)')
.hide('font-scale')
// debug
.boolean('debug')
.alias('d', 'debug')
// help
.help('h')
.alias('h', 'help')
.argv;
debug = !!argv.debug;
const template = argv.template;
const name = argv.name;
if (path.extname(name) && !fs.existsSync(name)) {
console.log(`${name} does not exist!`);
process.exit(2);
}
const names = fs.existsSync(name) ?
fs.readFileSync('names.txt', 'utf-8').replace('\r', '\n').replace('\n\n', '\n').split('\n') :
[name];
debug_log(pacman);
debug_log(argv);
// Install dependencies
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const annotate = (name, data) => {
// Convert file to DOM and get elements for modifications
const dom = new JSDOM(data, { contentType: 'image/svg+xml' });
// Get svg selector
let svg = dom.window.document.querySelector('svg');
svg = dom.window.document.getElementById(svg.id);
const width = svg.getAttribute('width');
const height = svg.getAttribute('height');
// Create text selector
// https://stackoverflow.com/a/13229110/19336104
const text = dom.window.document.createElement('text', '');
// Populate text selector
let id = 'text';
{
let i = 0;
do {
i++;
id = `text${i}`;
} while (!!dom.window.document.getElementById(`${id}`));
text.setAttribute('id', id);
text.setAttribute('xml:space', 'preserve');
text.setAttribute('style', `font-style:normal;font-weight:normal;font-size:${argv.fontSize}px;line-height:1.25;font-family:${argv.fontStyle};letter-spacing:0px;word-spacing:4px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.75094575`);
text.setAttribute('y', '214.18192');
// Lastly approximate x position
const x = (Number(width) - (name.length * argv.fontSize * argv.fontScale)) / 2;
text.setAttribute('x', `${x}`);
}
// Update Text Content
text.textContent = name;
svg.appendChild(text);
// Modify filename
const file = name.replace(/\s/g, '').replace('\&', '');
const svg_file = file + '.svg';
svg.setAttribute('sodipodi:docname', svg_file);
// Write to file
// Strangely, xmlns attribute will be automatically added after appendChild, remove it before writing to file.
console.log(`Anotating "${name}" to ${template}`);
fs.writeFileSync(path.join(OUT, svg_file), dom.serialize().replace(' xmlns=""', ''), 'utf8');
// Use inkscape to convert to PNG
if (cmdExist(INKSCAPE)) {
///@todo need to check for inkscape version, older versions does not have --actions
// or if argument options exists in --help
// inkscape action guide
// https://graphicdesign.stackexchange.com/a/161009
// https://inkscape.org/forums/beyond/inkscape-12-actions-list/
termSync(`${INKSCAPE} --actions="select-by-id:${id};object-align:hcenter page;export-filename:${path.join(OUT, svg_file)};export-do" ${path.join(OUT, svg_file)}`);
// export to PNG
termSync(`${INKSCAPE} ${path.join(OUT, svg_file)} --export-type=png --export-filename=${path.join(OUT, file + '.png')}`);
}
};
// Make output directory
mkdirSync(OUT);
// Anotate names into SVG
const data = fs.readFileSync(template, 'utf8');
names.forEach((name) => {
if (!name.startsWith('#')) {
annotate(name, data);
}
});
popd();
console.timeEnd(timed);
process.exit(0);