android-mcp-toolkit 1.1.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 +125 -0
- package/dist/index.js +1489 -0
- package/package.json +37 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1489 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
4
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// vendor/svg2vectordrawable/svg-to-vectordrawable.js
|
|
8
|
+
var require_svg_to_vectordrawable = __commonJS({
|
|
9
|
+
"vendor/svg2vectordrawable/svg-to-vectordrawable.js"(exports2, module2) {
|
|
10
|
+
var { parseSvg } = require("svgo/lib/parser");
|
|
11
|
+
var JSAPI = require("svgo/lib/svgo/jsAPI");
|
|
12
|
+
var pathBounds = require("svg-path-bounds");
|
|
13
|
+
var svgpath = require("svgpath");
|
|
14
|
+
var JS2XML = function() {
|
|
15
|
+
this.width = 24;
|
|
16
|
+
this.height = 24;
|
|
17
|
+
this.viewportWidth = 24;
|
|
18
|
+
this.viewportHeight = 24;
|
|
19
|
+
this.indent = 4;
|
|
20
|
+
this.indentLevel = 0;
|
|
21
|
+
this.vectordrawableTags = [
|
|
22
|
+
// Android vs SVG
|
|
23
|
+
"vector",
|
|
24
|
+
// svg
|
|
25
|
+
"group",
|
|
26
|
+
// g
|
|
27
|
+
"path",
|
|
28
|
+
// path, rect, circle, polygon, ellipse, polyline, line
|
|
29
|
+
"clip-path",
|
|
30
|
+
// mask
|
|
31
|
+
"aapt:attr",
|
|
32
|
+
"gradient",
|
|
33
|
+
// linearGradient, radialGradient
|
|
34
|
+
"item"
|
|
35
|
+
// stop
|
|
36
|
+
];
|
|
37
|
+
this.vectordrawableAttrs = [
|
|
38
|
+
// Android vs SVG
|
|
39
|
+
"android:name",
|
|
40
|
+
// id
|
|
41
|
+
// <Vector>
|
|
42
|
+
"xmlns:aapt",
|
|
43
|
+
"android:width",
|
|
44
|
+
// width
|
|
45
|
+
"android:height",
|
|
46
|
+
// height
|
|
47
|
+
"android:viewportWidth",
|
|
48
|
+
"android:viewportHeight",
|
|
49
|
+
// viewBox
|
|
50
|
+
"android:tint",
|
|
51
|
+
"android:tintMode",
|
|
52
|
+
"android:autoMirrored",
|
|
53
|
+
"android:alpha",
|
|
54
|
+
// <group>
|
|
55
|
+
"android:rotation",
|
|
56
|
+
// transform="rotate(<a> [<x> <y>])"
|
|
57
|
+
"android:pivotX",
|
|
58
|
+
"android:pivotY",
|
|
59
|
+
"android:scaleX",
|
|
60
|
+
// transform="scale(<x> [<y>])"
|
|
61
|
+
"android:scaleY",
|
|
62
|
+
"android:translateX",
|
|
63
|
+
// transform="translate(<x> [<y>])"
|
|
64
|
+
"android:translateY",
|
|
65
|
+
// <path>
|
|
66
|
+
"android:pathData",
|
|
67
|
+
// d
|
|
68
|
+
"android:fillColor",
|
|
69
|
+
// fill
|
|
70
|
+
"android:fillAlpha",
|
|
71
|
+
// fill-opacity 0..1
|
|
72
|
+
"android:strokeColor",
|
|
73
|
+
// stroke
|
|
74
|
+
"android:strokeWidth",
|
|
75
|
+
// stroke-width
|
|
76
|
+
"android:strokeAlpha",
|
|
77
|
+
// stroke-opacity
|
|
78
|
+
"android:trimPathStart",
|
|
79
|
+
"android:trimPathEnd",
|
|
80
|
+
"android:trimPathOffset",
|
|
81
|
+
"android:strokeLineCap",
|
|
82
|
+
// stroke-linecap butt, round, square. Default is butt.
|
|
83
|
+
"android:strokeLineJoin",
|
|
84
|
+
// stroke-linejoin miter, round, bevel. Default is miter.
|
|
85
|
+
"android:strokeMiterLimit",
|
|
86
|
+
// stroke-miterlimit Default is 4.
|
|
87
|
+
"android:fillType",
|
|
88
|
+
// fill-rule For SDK 24+, evenOdd, nonZero. Default is nonZero.
|
|
89
|
+
// aapt
|
|
90
|
+
"name",
|
|
91
|
+
"xmlns:aapt",
|
|
92
|
+
// gradient
|
|
93
|
+
"android:type",
|
|
94
|
+
// linear, radial, sweep
|
|
95
|
+
"android:startX",
|
|
96
|
+
// x1
|
|
97
|
+
"android:startY",
|
|
98
|
+
// y1
|
|
99
|
+
"android:endX",
|
|
100
|
+
// x2
|
|
101
|
+
"android:endY",
|
|
102
|
+
// y2
|
|
103
|
+
"android:centerX",
|
|
104
|
+
// cx
|
|
105
|
+
"android:centerY",
|
|
106
|
+
// cy
|
|
107
|
+
"android:gradientRadius",
|
|
108
|
+
// r
|
|
109
|
+
"android:startColor",
|
|
110
|
+
// not support
|
|
111
|
+
"android:centerColor",
|
|
112
|
+
// not support
|
|
113
|
+
"android:endColor",
|
|
114
|
+
// not support
|
|
115
|
+
"android:tileMode",
|
|
116
|
+
// not support, disabled, clamp, repeat, mirror
|
|
117
|
+
// gradient stop
|
|
118
|
+
"android:color",
|
|
119
|
+
// stop-color
|
|
120
|
+
"android:offset"
|
|
121
|
+
// offset
|
|
122
|
+
];
|
|
123
|
+
};
|
|
124
|
+
JS2XML.prototype.refactorData = function(data, floatPrecision, fillBlack, tint) {
|
|
125
|
+
let elemUses = data.querySelectorAll("use");
|
|
126
|
+
if (elemUses) {
|
|
127
|
+
elemUses.forEach((elem) => {
|
|
128
|
+
if (elem.hasAttr("xlink:href") || elem.hasAttr("href")) {
|
|
129
|
+
let attr = "";
|
|
130
|
+
if (elem.hasAttr("xlink:href")) {
|
|
131
|
+
attr = "xlink:href";
|
|
132
|
+
}
|
|
133
|
+
if (elem.hasAttr("href")) {
|
|
134
|
+
attr = "href";
|
|
135
|
+
}
|
|
136
|
+
let originalElem = data.querySelector(elem.attr(attr).value);
|
|
137
|
+
let newElem = new JSAPI({
|
|
138
|
+
type: originalElem.type,
|
|
139
|
+
name: originalElem.name
|
|
140
|
+
});
|
|
141
|
+
originalElem.eachAttr((attr2) => {
|
|
142
|
+
if (attr2.name !== attr2 && attr2.name !== "id") {
|
|
143
|
+
newElem.addAttr(attr2);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
elem.eachAttr((attr2) => {
|
|
147
|
+
if (attr2.name !== attr2 && attr2.name !== "id" && attr2.name !== "class") {
|
|
148
|
+
newElem.addAttr(attr2);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
if ((newElem.attr("x") || newElem.attr("y")) && newElem.attr("d")) {
|
|
152
|
+
let x = newElem.attr("x") ? parseFloat(newElem.attr("x").value) : 0;
|
|
153
|
+
let y = newElem.attr("y") ? parseFloat(newElem.attr("y").value) : 0;
|
|
154
|
+
let pathData = newElem.attr("d").value;
|
|
155
|
+
if (x !== 0 || y !== 0) {
|
|
156
|
+
pathData = svgpath(pathData).translate(x, y).rel().round(floatPrecision).toString();
|
|
157
|
+
}
|
|
158
|
+
newElem.removeAttr("d");
|
|
159
|
+
newElem.addAttr({ name: "d", value: pathData, prefix: "", local: "d" });
|
|
160
|
+
}
|
|
161
|
+
elem.parentNode.spliceContent(elem.parentNode.children.indexOf(elem), 0, newElem);
|
|
162
|
+
elem.parentNode.spliceContent(elem.parentNode.children.indexOf(elem), 1, []);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
let elemHaveIds = data.querySelectorAll("path, g, circle, ellipse, line, polygon, polyline, rect");
|
|
167
|
+
if (elemHaveIds) {
|
|
168
|
+
elemHaveIds.forEach((elem) => {
|
|
169
|
+
elem.removeAttr("id");
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
let elemRects = data.querySelectorAll("rect");
|
|
173
|
+
if (elemRects) {
|
|
174
|
+
elemRects.forEach((elem) => {
|
|
175
|
+
elem.renameElem("path");
|
|
176
|
+
let x = elem.hasAttr("x") ? parseFloat(elem.attr("x").value) : 0;
|
|
177
|
+
let y = elem.hasAttr("y") ? parseFloat(elem.attr("y").value) : 0;
|
|
178
|
+
let width = elem.hasAttr("width") ? parseFloat(elem.attr("width").value) : 0;
|
|
179
|
+
let height = elem.hasAttr("height") ? parseFloat(elem.attr("height").value) : 0;
|
|
180
|
+
let rx = 0;
|
|
181
|
+
let ry = 0;
|
|
182
|
+
if (elem.hasAttr("rx") && elem.hasAttr("ry")) {
|
|
183
|
+
rx = parseFloat(elem.attr("rx").value);
|
|
184
|
+
ry = parseFloat(elem.attr("ry").value);
|
|
185
|
+
} else if (elem.hasAttr("rx")) {
|
|
186
|
+
rx = parseFloat(elem.attr("rx").value);
|
|
187
|
+
ry = rx;
|
|
188
|
+
} else if (elem.hasAttr("ry")) {
|
|
189
|
+
ry = parseFloat(elem.attr("ry").value);
|
|
190
|
+
rx = ry;
|
|
191
|
+
}
|
|
192
|
+
x = this.round(x, floatPrecision);
|
|
193
|
+
y = this.round(y, floatPrecision);
|
|
194
|
+
width = this.round(width, floatPrecision);
|
|
195
|
+
height = this.round(height, floatPrecision);
|
|
196
|
+
rx = this.round(rx, floatPrecision);
|
|
197
|
+
ry = this.round(ry, floatPrecision);
|
|
198
|
+
let pathData = this.rectToPathData(x, y, width, height, rx, ry);
|
|
199
|
+
if (rx !== 0 || ry !== 0) {
|
|
200
|
+
pathData = svgpath(pathData).unarc().rel().round(floatPrecision).toString();
|
|
201
|
+
}
|
|
202
|
+
if (elem.hasAttr("transform")) {
|
|
203
|
+
let svgTransform = elem.attr("transform").value;
|
|
204
|
+
let number = "((?:-)?\\d+(?:\\.\\d+)?)";
|
|
205
|
+
let separator = "(?:(?:\\s+)|(?:\\s*,\\s*))";
|
|
206
|
+
let translateRegExp = new RegExp(`translate\\(${number}${separator}?${number}?\\)`);
|
|
207
|
+
let scaleRegExp = new RegExp(`scale\\(${number}${separator}?${number}?\\)`);
|
|
208
|
+
let rotateRegExp = new RegExp(`rotate\\(${number}${separator}?${number}?${separator}?${number}?\\)`);
|
|
209
|
+
let translateMatch = translateRegExp.exec(svgTransform);
|
|
210
|
+
let scaleMatch = scaleRegExp.exec(svgTransform);
|
|
211
|
+
let rotateMatch = rotateRegExp.exec(svgTransform);
|
|
212
|
+
if (rotateMatch) {
|
|
213
|
+
let angle = parseFloat(rotateMatch[1]);
|
|
214
|
+
let rx2 = parseFloat(rotateMatch[2]) || 0;
|
|
215
|
+
let ry2 = parseFloat(rotateMatch[3]) || 0;
|
|
216
|
+
pathData = svgpath(pathData).rotate(angle, rx2, ry2).rel().round(floatPrecision).toString();
|
|
217
|
+
}
|
|
218
|
+
if (scaleMatch) {
|
|
219
|
+
let sx = parseFloat(scaleMatch[1]);
|
|
220
|
+
let sy = parseFloat(scaleMatch[2]) || sx;
|
|
221
|
+
pathData = svgpath(pathData).scale(sx, sy).rel().round(floatPrecision).toString();
|
|
222
|
+
}
|
|
223
|
+
if (translateMatch) {
|
|
224
|
+
let x2 = parseFloat(translateMatch[1]);
|
|
225
|
+
let y2 = parseFloat(translateMatch[2]) || 0;
|
|
226
|
+
pathData = svgpath(pathData).translate(x2, y2).rel().round(floatPrecision).toString();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
elem.addAttr({ name: "d", value: pathData, prefix: "", local: "d" });
|
|
230
|
+
elem.removeAttr("x");
|
|
231
|
+
elem.removeAttr("y");
|
|
232
|
+
elem.removeAttr("width");
|
|
233
|
+
elem.removeAttr("height");
|
|
234
|
+
elem.removeAttr("rx");
|
|
235
|
+
elem.removeAttr("ry");
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
let elemGroups = data.querySelectorAll("g");
|
|
239
|
+
if (elemGroups) {
|
|
240
|
+
elemGroups.forEach((elem) => {
|
|
241
|
+
let childPaths = elem.querySelectorAll("path");
|
|
242
|
+
if (childPaths) {
|
|
243
|
+
childPaths.forEach((item) => {
|
|
244
|
+
if (elem.hasAttr("fill") && !elem.hasAttr("fill", "none") && !item.hasAttr("fill") && !item.hasAttr("fill", "none")) {
|
|
245
|
+
item.addAttr({ name: "fill", value: elem.attr("fill").value, prefix: "", local: "fill" });
|
|
246
|
+
}
|
|
247
|
+
if (elem.hasAttr("fill-opacity") && !item.hasAttr("fill-opacity")) {
|
|
248
|
+
item.addAttr({ name: "fill-opacity", value: elem.attr("fill-opacity").value, prefix: "", local: "fill-opacity" });
|
|
249
|
+
}
|
|
250
|
+
if (elem.hasAttr("stroke") && !item.hasAttr("stroke")) {
|
|
251
|
+
item.addAttr({ name: "stroke", value: elem.attr("stroke").value, prefix: "", local: "stroke" });
|
|
252
|
+
}
|
|
253
|
+
if (elem.hasAttr("stroke-width") && !item.hasAttr("stroke-width")) {
|
|
254
|
+
item.addAttr({ name: "stroke-width", value: elem.attr("stroke-width").value, prefix: "", local: "stroke-width" });
|
|
255
|
+
}
|
|
256
|
+
if (elem.hasAttr("stroke-opacity") && !item.hasAttr("stroke-opacity")) {
|
|
257
|
+
item.addAttr({ name: "stroke-opacity", value: elem.attr("stroke-opacity").value, prefix: "", local: "stroke-opacity" });
|
|
258
|
+
}
|
|
259
|
+
if (elem.hasAttr("stroke-linecap") && !item.hasAttr("stroke-linecap")) {
|
|
260
|
+
item.addAttr({ name: "stroke-linecap", value: elem.attr("stroke-linecap").value, prefix: "", local: "stroke-linecap" });
|
|
261
|
+
}
|
|
262
|
+
if (elem.hasAttr("stroke-linejoin") && !item.hasAttr("stroke-linejoin")) {
|
|
263
|
+
item.addAttr({ name: "stroke-linejoin", value: elem.attr("stroke-linejoin").value, prefix: "", local: "stroke-linejoin" });
|
|
264
|
+
}
|
|
265
|
+
if (elem.hasAttr("opacity")) {
|
|
266
|
+
let opacity = elem.attr("opacity").value;
|
|
267
|
+
if (item.hasAttr("opacity")) {
|
|
268
|
+
opacity = this.round(elem.attr("opacity").value * item.attr("opacity").value, floatPrecision);
|
|
269
|
+
}
|
|
270
|
+
item.addAttr({ name: "opacity", value: opacity, prefix: "", local: "opacity" });
|
|
271
|
+
}
|
|
272
|
+
if (elem.hasAttr("fill-rule", "evenodd")) {
|
|
273
|
+
if (!item.hasAttr("fill-rule", "nonzero") && item.hasAttr("fill")) {
|
|
274
|
+
item.addAttr({ name: "android:fillType", value: "evenOdd", prefix: "android", local: "fillType" });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
elem.removeAttr("fill");
|
|
279
|
+
elem.removeAttr("fill-opacity");
|
|
280
|
+
elem.removeAttr("stroke");
|
|
281
|
+
elem.removeAttr("stroke-width");
|
|
282
|
+
elem.removeAttr("stroke-opacity");
|
|
283
|
+
elem.removeAttr("stroke-linecap");
|
|
284
|
+
elem.removeAttr("stroke-linejoin");
|
|
285
|
+
elem.removeAttr("opacity");
|
|
286
|
+
elem.removeAttr("fill-rule");
|
|
287
|
+
}
|
|
288
|
+
if (elem.hasAttr("transform")) {
|
|
289
|
+
let svgTransform = elem.attr("transform").value;
|
|
290
|
+
let number = "((?:-)?\\d+(?:\\.\\d+)?)";
|
|
291
|
+
let separator = "(?:(?:\\s+)|(?:\\s*,\\s*))";
|
|
292
|
+
let attrs = { rotation: "", pivotX: "", pivotY: "", scaleX: "", scaleY: "", translateX: "", translateY: "" };
|
|
293
|
+
let translateRegExp = new RegExp(`translate\\(${number}${separator}?${number}?\\)`, "g");
|
|
294
|
+
let translateX = 0;
|
|
295
|
+
let translateY = 0;
|
|
296
|
+
let translateMatch;
|
|
297
|
+
while (translateMatch = translateRegExp.exec(svgTransform)) {
|
|
298
|
+
translateX += Number(translateMatch[1]);
|
|
299
|
+
translateY += Number(translateMatch[2]);
|
|
300
|
+
}
|
|
301
|
+
if (translateX !== 0 && !isNaN(translateX)) {
|
|
302
|
+
attrs.translateX = translateX;
|
|
303
|
+
}
|
|
304
|
+
if (translateY !== 0 && !isNaN(translateY)) {
|
|
305
|
+
attrs.translateY = translateY;
|
|
306
|
+
}
|
|
307
|
+
let scaleRegExp = new RegExp(`scale\\(${number}${separator}?${number}?\\)`, "g");
|
|
308
|
+
let scaleX = 1;
|
|
309
|
+
let scaleY = 1;
|
|
310
|
+
let scaleMatch;
|
|
311
|
+
while (scaleMatch = scaleRegExp.exec(svgTransform)) {
|
|
312
|
+
scaleX *= Number(scaleMatch[1]);
|
|
313
|
+
scaleY *= Number(scaleMatch[2]) || Number(scaleMatch[1]);
|
|
314
|
+
}
|
|
315
|
+
if (scaleX !== 1 && !isNaN(scaleX)) {
|
|
316
|
+
attrs.scaleX = scaleX;
|
|
317
|
+
}
|
|
318
|
+
if (scaleY !== 1 && !isNaN(scaleY)) {
|
|
319
|
+
attrs.scaleY = scaleY;
|
|
320
|
+
}
|
|
321
|
+
let rotateRegExp = new RegExp(`rotate\\(${number}${separator}?${number}?${separator}?${number}?\\)`);
|
|
322
|
+
let rotateMatch = rotateRegExp.exec(svgTransform);
|
|
323
|
+
if (rotateMatch) {
|
|
324
|
+
attrs.rotation = rotateMatch[1];
|
|
325
|
+
attrs.pivotX = rotateMatch[2] || "";
|
|
326
|
+
attrs.pivotY = rotateMatch[3] || "";
|
|
327
|
+
}
|
|
328
|
+
let skewRegExp = new RegExp(`skew([XY])\\(${number}\\)`);
|
|
329
|
+
let matrixRegExp = new RegExp("matrix\\((.*)\\)");
|
|
330
|
+
let skewMatch = skewRegExp.exec(svgTransform);
|
|
331
|
+
let matrixMatch = matrixRegExp.exec(svgTransform);
|
|
332
|
+
if (skewMatch || matrixMatch) {
|
|
333
|
+
let paths = elem.querySelectorAll("path");
|
|
334
|
+
if (paths) {
|
|
335
|
+
paths.forEach((path) => {
|
|
336
|
+
let pathData = path.attr("d").value;
|
|
337
|
+
if (skewMatch) {
|
|
338
|
+
let skewX = skewMatch[1] === "X" ? parseFloat(skewMatch[2]) : 0;
|
|
339
|
+
let skewY = skewMatch[1] === "Y" ? parseFloat(skewMatch[2]) : 0;
|
|
340
|
+
pathData = svgpath(pathData).skewX(skewX).skewY(skewY).rel().round(floatPrecision).toString();
|
|
341
|
+
}
|
|
342
|
+
if (matrixMatch) {
|
|
343
|
+
let matrix = matrixMatch[1].split(" ").map((item) => parseFloat(item));
|
|
344
|
+
pathData = svgpath(pathData).matrix(matrix).rel().round(floatPrecision).toString();
|
|
345
|
+
}
|
|
346
|
+
path.removeAttr("d");
|
|
347
|
+
path.addAttr({ name: "d", value: pathData, prefix: "", local: "d" });
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
Object.keys(attrs).forEach((key) => {
|
|
352
|
+
if (attrs[key] !== "" && (attrs[key] !== "0" && (key !== "scaleX" || key !== "scaleY"))) {
|
|
353
|
+
elem.addAttr({ name: `android:${key}`, value: this.round(attrs[key], floatPrecision), prefix: "android", local: key });
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
elem.removeAttr("transform");
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
elemGroups = data.querySelectorAll("g");
|
|
361
|
+
if (elemGroups) {
|
|
362
|
+
elemGroups.forEach((elem) => {
|
|
363
|
+
if (Object.keys(elem.attrs).length === 0) {
|
|
364
|
+
elem.parentNode.spliceContent(elem.parentNode.children.indexOf(elem), 0, elem.children);
|
|
365
|
+
elem.parentNode.spliceContent(elem.parentNode.children.indexOf(elem), 1, []);
|
|
366
|
+
} else {
|
|
367
|
+
elem.renameElem("group");
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
let elemSVG = data.querySelector("svg");
|
|
372
|
+
if (elemSVG) {
|
|
373
|
+
elemSVG.renameElem("vector");
|
|
374
|
+
if (elemSVG.hasAttr("width") && elemSVG.hasAttr("height")) {
|
|
375
|
+
this.width = parseInt(elemSVG.attr("width").value);
|
|
376
|
+
this.height = parseInt(elemSVG.attr("height").value);
|
|
377
|
+
}
|
|
378
|
+
if (elemSVG.hasAttr("viewBox")) {
|
|
379
|
+
let [x, y, w, h] = elemSVG.attr("viewBox").value.split(/\s+/);
|
|
380
|
+
this.viewportWidth = w;
|
|
381
|
+
this.viewportHeight = h;
|
|
382
|
+
if (!elemSVG.hasAttr("width") && !elemSVG.hasAttr("height")) {
|
|
383
|
+
this.width = w;
|
|
384
|
+
this.height = h;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
elemSVG.attrs = {};
|
|
388
|
+
if (data.querySelector("linearGradient, radialGradient, sweepGradient, aapt\\:attr")) {
|
|
389
|
+
elemSVG.addAttr({ name: "xmlns:aapt", value: "http://schemas.android.com/aapt", prefix: "xmlns", local: "aapt" });
|
|
390
|
+
}
|
|
391
|
+
elemSVG.addAttr({ name: "android:width", value: this.width + "dp", prefix: "android", local: "width" });
|
|
392
|
+
elemSVG.addAttr({ name: "android:height", value: this.height + "dp", prefix: "android", local: "height" });
|
|
393
|
+
elemSVG.addAttr({ name: "android:viewportWidth", value: this.viewportWidth, prefix: "android", local: "viewportWidth" });
|
|
394
|
+
elemSVG.addAttr({ name: "android:viewportHeight", value: this.viewportHeight, prefix: "android", local: "viewportHeight" });
|
|
395
|
+
if (tint) {
|
|
396
|
+
if (/^#[A-F0-9]{1,8}$/i.test(tint)) {
|
|
397
|
+
tint = tint.toUpperCase();
|
|
398
|
+
}
|
|
399
|
+
elemSVG.addAttr({ name: "android:tint", value: tint, prefix: "android", local: "tint" });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
let elemGradients = data.querySelectorAll("linearGradient, radialGradient, sweepGradient");
|
|
403
|
+
if (elemGradients) {
|
|
404
|
+
elemGradients.forEach((gradient) => {
|
|
405
|
+
if (gradient.hasAttr("id")) {
|
|
406
|
+
let gradientId = gradient.attr("id").value;
|
|
407
|
+
let gradientPaths = data.querySelectorAll(`path[fill="url(#${gradientId})"], path[stroke="url(#${gradientId})"]`);
|
|
408
|
+
if (gradientPaths) {
|
|
409
|
+
gradientPaths.forEach((path) => {
|
|
410
|
+
this.addGradientToElement(gradient, path, floatPrecision);
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
gradient.parentNode.spliceContent(gradient.parentNode.children.indexOf(gradient), 1, []);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
let elemMasks = data.querySelectorAll("mask");
|
|
418
|
+
if (elemMasks) {
|
|
419
|
+
elemMasks.forEach((elem) => {
|
|
420
|
+
if (elem.hasAttr("id")) {
|
|
421
|
+
let maskId = elem.attr("id").value;
|
|
422
|
+
let clipMaskElem = elem.children[0];
|
|
423
|
+
let pathData = svgpath(clipMaskElem.attr("d").value).round(floatPrecision).toString();
|
|
424
|
+
let maskGroup = new JSAPI({
|
|
425
|
+
type: "element",
|
|
426
|
+
name: "group",
|
|
427
|
+
attrs: {},
|
|
428
|
+
children: []
|
|
429
|
+
});
|
|
430
|
+
clipMaskElem.renameElem("clip-path");
|
|
431
|
+
clipMaskElem.attrs = {};
|
|
432
|
+
clipMaskElem.addAttr({ name: "android:pathData", value: pathData, prefix: "android", local: "pathData" });
|
|
433
|
+
maskGroup.children.push(clipMaskElem);
|
|
434
|
+
let maskedElems = data.querySelectorAll(`*[mask="url(#${maskId})"]`);
|
|
435
|
+
if (maskedElems) {
|
|
436
|
+
maskedElems.forEach((item) => {
|
|
437
|
+
item.removeAttr("mask");
|
|
438
|
+
maskGroup.children.push(item);
|
|
439
|
+
});
|
|
440
|
+
elem.parentNode.spliceContent(elem.parentNode.children.indexOf(maskedElems[0]), maskedElems.length, maskGroup);
|
|
441
|
+
} else {
|
|
442
|
+
elem.parentNode.spliceContent(elem.parentNode.children.indexOf(elem), 0, maskGroup);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
elem.parentNode.spliceContent(elem.parentNode.children.indexOf(elem), 1, []);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
let elemPaths = data.querySelectorAll("path");
|
|
449
|
+
if (elemPaths) {
|
|
450
|
+
elemPaths.forEach((elem) => {
|
|
451
|
+
if (elem.hasAttr("fill")) {
|
|
452
|
+
if (elem.attr("fill").value === "none") {
|
|
453
|
+
elem.removeAttr("fill");
|
|
454
|
+
} else if (!/^url\(#.*\)$/.test(elem.attr("fill").value)) {
|
|
455
|
+
let color = this.svgHexToAndroid(elem.attr("fill").value);
|
|
456
|
+
let fillAttr = { name: "android:fillColor", value: color, prefix: "android", local: "fillColor" };
|
|
457
|
+
if (elem.hasAttr("fill-opacity")) {
|
|
458
|
+
fillAttr.value = this.mergeColorAndOpacity(fillAttr.value, elem.attr("fill-opacity").value);
|
|
459
|
+
elem.removeAttr("fill-opacity");
|
|
460
|
+
}
|
|
461
|
+
elem.addAttr(fillAttr);
|
|
462
|
+
elem.removeAttr("fill");
|
|
463
|
+
}
|
|
464
|
+
} else if (fillBlack) {
|
|
465
|
+
let fillAttr = { name: "android:fillColor", value: "#FF000000", prefix: "android", local: "fillColor" };
|
|
466
|
+
elem.addAttr(fillAttr);
|
|
467
|
+
}
|
|
468
|
+
if (elem.hasAttr("opacity")) {
|
|
469
|
+
elem.addAttr({ name: "android:fillAlpha", value: elem.attr("opacity").value, prefix: "android", local: "fillAlpha" });
|
|
470
|
+
}
|
|
471
|
+
if (elem.hasAttr("stroke")) {
|
|
472
|
+
if (!/^url\(#.*\)$/.test(elem.attr("stroke").value) && elem.attr("stroke").value !== "none") {
|
|
473
|
+
let color = this.svgHexToAndroid(elem.attr("stroke").value);
|
|
474
|
+
let strokeAttr = { name: "android:strokeColor", value: color, prefix: "android", local: "strokeColor" };
|
|
475
|
+
if (elem.hasAttr("stroke-opacity")) {
|
|
476
|
+
strokeAttr.value = this.mergeColorAndOpacity(strokeAttr.value, elem.attr("stroke-opacity").value);
|
|
477
|
+
elem.removeAttr("stroke-opacity");
|
|
478
|
+
}
|
|
479
|
+
elem.addAttr(strokeAttr);
|
|
480
|
+
elem.removeAttr("stroke");
|
|
481
|
+
}
|
|
482
|
+
if (elem.hasAttr("opacity")) {
|
|
483
|
+
elem.addAttr({ name: "android:strokeAlpha", value: elem.attr("opacity").value, prefix: "android", local: "strokeAlpha" });
|
|
484
|
+
}
|
|
485
|
+
let strokeWidthAttr = { name: "android:strokeWidth", value: 0, prefix: "android", local: "strokeWidth" };
|
|
486
|
+
if (!elem.hasAttr("stroke-width")) {
|
|
487
|
+
strokeWidthAttr.value = 1;
|
|
488
|
+
} else {
|
|
489
|
+
strokeWidthAttr.value = elem.attr("stroke-width").value;
|
|
490
|
+
elem.removeAttr("stroke-width");
|
|
491
|
+
}
|
|
492
|
+
elem.addAttr(strokeWidthAttr);
|
|
493
|
+
let strokeExtraAttrs = ["stroke-linecap", "stroke-linejoin", "stroke-miterlimit"];
|
|
494
|
+
let strokeExtraAttrsAndroid = ["strokeLineCap", "strokeLineJoin", "strokeMiterLimit"];
|
|
495
|
+
strokeExtraAttrs.forEach((attr, index) => {
|
|
496
|
+
if (elem.hasAttr(attr)) {
|
|
497
|
+
let localName = strokeExtraAttrsAndroid[index];
|
|
498
|
+
elem.addAttr({ name: "android:" + localName, value: elem.attr(attr).value, prefix: "android", local: localName });
|
|
499
|
+
elem.removeAttr("attr");
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
if (elem.hasAttr("opacity")) {
|
|
504
|
+
elem.removeAttr("opacity");
|
|
505
|
+
}
|
|
506
|
+
elem.removeAttr("fill-rule", "nonzero");
|
|
507
|
+
if (elem.hasAttr("fill-rule", "evenodd")) {
|
|
508
|
+
elem.addAttr({ name: "android:fillType", value: "evenOdd", prefix: "android", local: "fillType" });
|
|
509
|
+
elem.removeAttr("fill-rule", "evenodd");
|
|
510
|
+
}
|
|
511
|
+
if (elem.hasAttr("d")) {
|
|
512
|
+
let pathData = svgpath(elem.attr("d").value).round(floatPrecision).toString();
|
|
513
|
+
elem.addAttr({ name: "android:pathData", value: pathData, prefix: "android", local: "pathData" });
|
|
514
|
+
elem.removeAttr("d");
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
JS2XML.prototype.addGradientToElement = function(gradient, elem, floatPrecision) {
|
|
520
|
+
let vectorDrawableGradient = new JSAPI({
|
|
521
|
+
type: "element",
|
|
522
|
+
name: "gradient",
|
|
523
|
+
children: []
|
|
524
|
+
});
|
|
525
|
+
let vectorDrawableAapt = new JSAPI({
|
|
526
|
+
type: "element",
|
|
527
|
+
name: "aapt:attr",
|
|
528
|
+
children: [vectorDrawableGradient]
|
|
529
|
+
});
|
|
530
|
+
let gradientId = gradient.attr("id").value;
|
|
531
|
+
if (elem.hasAttr("fill", `url(#${gradientId})`)) {
|
|
532
|
+
vectorDrawableAapt.addAttr({ name: "name", value: "android:fillColor", prefix: "", local: "name" });
|
|
533
|
+
elem.removeAttr("fill");
|
|
534
|
+
}
|
|
535
|
+
if (elem.hasAttr("stroke", `url(#${gradientId})`)) {
|
|
536
|
+
vectorDrawableAapt.addAttr({ name: "name", value: "android:strokeColor", prefix: "", local: "name" });
|
|
537
|
+
elem.removeAttr("stroke");
|
|
538
|
+
}
|
|
539
|
+
this.adjustGradientCoordinate(gradient, elem, floatPrecision);
|
|
540
|
+
if (gradient.name === "linearGradient") {
|
|
541
|
+
vectorDrawableGradient.addAttr({ name: "android:type", value: "linear", prefix: "android", local: "type" });
|
|
542
|
+
let startX = gradient.hasAttr("x1") ? gradient.attr("x1").value : "0";
|
|
543
|
+
let startY = gradient.hasAttr("y1") ? gradient.attr("y1").value : "0";
|
|
544
|
+
let endX = gradient.hasAttr("x2") ? gradient.attr("x2").value : this.viewportWidth;
|
|
545
|
+
let endY = gradient.hasAttr("y2") ? gradient.attr("y2").value : "0";
|
|
546
|
+
vectorDrawableGradient.addAttr({ name: "android:startX", value: startX, prefix: "android", local: "startX" });
|
|
547
|
+
vectorDrawableGradient.addAttr({ name: "android:startY", value: startY, prefix: "android", local: "startY" });
|
|
548
|
+
vectorDrawableGradient.addAttr({ name: "android:endX", value: endX, prefix: "android", local: "endX" });
|
|
549
|
+
vectorDrawableGradient.addAttr({ name: "android:endY", value: endY, prefix: "android", local: "endY" });
|
|
550
|
+
}
|
|
551
|
+
if (gradient.name === "radialGradient") {
|
|
552
|
+
vectorDrawableGradient.addAttr({ name: "android:type", value: "radial", prefix: "android", local: "type" });
|
|
553
|
+
let centerX = gradient.hasAttr("cx") ? gradient.attr("cx").value : this.viewportWidth / 2;
|
|
554
|
+
let centerY = gradient.hasAttr("cy") ? gradient.attr("cy").value : this.viewportHeight / 2;
|
|
555
|
+
if (gradient.hasAttr("rx")) centerX = gradient.attr("rx").value;
|
|
556
|
+
if (gradient.hasAttr("ry")) centerY = gradient.attr("ry").value;
|
|
557
|
+
let gradientRadius = gradient.hasAttr("r") ? gradient.attr("r").value : Math.max(this.viewportWidth, this.viewportHeight) / 2;
|
|
558
|
+
vectorDrawableGradient.addAttr({ name: "android:centerX", value: centerX, prefix: "android", local: "centerX" });
|
|
559
|
+
vectorDrawableGradient.addAttr({ name: "android:centerY", value: centerY, prefix: "android", local: "centerY" });
|
|
560
|
+
vectorDrawableGradient.addAttr({ name: "android:gradientRadius", value: gradientRadius, prefix: "android", local: "gradientRadius" });
|
|
561
|
+
}
|
|
562
|
+
if (gradient.name === "sweepGradient") {
|
|
563
|
+
vectorDrawableGradient.addAttr({ name: "android:type", value: "sweep", prefix: "android", local: "type" });
|
|
564
|
+
let centerX = gradient.hasAttr("cx") ? gradient.attr("cx").value : this.viewportWidth / 2;
|
|
565
|
+
let centerY = gradient.hasAttr("cy") ? gradient.attr("cy").value : this.viewportHeight / 2;
|
|
566
|
+
vectorDrawableGradient.addAttr({ name: "android:centerX", value: centerX, prefix: "android", local: "centerX" });
|
|
567
|
+
vectorDrawableGradient.addAttr({ name: "android:centerY", value: centerY, prefix: "android", local: "centerY" });
|
|
568
|
+
}
|
|
569
|
+
gradient.children.forEach((item) => {
|
|
570
|
+
let colorStop = new JSAPI({
|
|
571
|
+
type: "element",
|
|
572
|
+
name: "item"
|
|
573
|
+
});
|
|
574
|
+
const stopColorAttr = item.attr("stop-color");
|
|
575
|
+
let color = this.svgHexToAndroid(stopColorAttr == null ? "#000000FF" : stopColorAttr.value);
|
|
576
|
+
const offsetAttr = item.attr("offset");
|
|
577
|
+
let offset = offsetAttr == null ? 0 : offsetAttr.value;
|
|
578
|
+
if (this.isPercent(offset)) {
|
|
579
|
+
offset = Math.round(parseFloat(offset)) / 100;
|
|
580
|
+
}
|
|
581
|
+
if (item.hasAttr("stop-opacity")) {
|
|
582
|
+
color = this.mergeColorAndOpacity(color, item.attr("stop-opacity").value);
|
|
583
|
+
}
|
|
584
|
+
colorStop.addAttr({ name: "android:color", value: color, prefix: "android", local: "color" });
|
|
585
|
+
colorStop.addAttr({ name: "android:offset", value: offset, prefix: "android", local: "offset" });
|
|
586
|
+
vectorDrawableGradient.children.push(colorStop);
|
|
587
|
+
});
|
|
588
|
+
if (!elem.children) elem.children = [];
|
|
589
|
+
elem.children.push(vectorDrawableAapt);
|
|
590
|
+
};
|
|
591
|
+
JS2XML.prototype.adjustGradientCoordinate = function(gradient, elem, floatPrecision) {
|
|
592
|
+
if (gradient.elem === "linearGradient") {
|
|
593
|
+
if (!gradient.hasAttr("x1")) {
|
|
594
|
+
gradient.addAttr({ name: "x1", value: "0", prefix: "", local: "x1" });
|
|
595
|
+
}
|
|
596
|
+
if (!gradient.hasAttr("y1")) {
|
|
597
|
+
gradient.addAttr({ name: "y1", value: "0", prefix: "", local: "y1" });
|
|
598
|
+
}
|
|
599
|
+
if (!gradient.hasAttr("x2")) {
|
|
600
|
+
gradient.addAttr({ name: "x2", value: "100%", prefix: "", local: "x2" });
|
|
601
|
+
}
|
|
602
|
+
if (!gradient.hasAttr("y2")) {
|
|
603
|
+
gradient.addAttr({ name: "y2", value: "100%", prefix: "", local: "y2" });
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (gradient.elem === "radialGradient") {
|
|
607
|
+
if (!gradient.hasAttr("cx")) {
|
|
608
|
+
gradient.addAttr({ name: "cx", value: "50%", prefix: "", local: "cx" });
|
|
609
|
+
}
|
|
610
|
+
if (!gradient.hasAttr("cy")) {
|
|
611
|
+
gradient.addAttr({ name: "cy", value: "50%", prefix: "", local: "cy" });
|
|
612
|
+
}
|
|
613
|
+
if (!gradient.hasAttr("r")) {
|
|
614
|
+
gradient.addAttr({ name: "r", value: "50%", prefix: "", local: "r" });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (gradient.elem === "sweepGradient") {
|
|
618
|
+
if (!gradient.hasAttr("cx")) {
|
|
619
|
+
gradient.addAttr({ name: "cx", value: "50%", prefix: "", local: "cx" });
|
|
620
|
+
}
|
|
621
|
+
if (!gradient.hasAttr("cy")) {
|
|
622
|
+
gradient.addAttr({ name: "cy", value: "50%", prefix: "", local: "cy" });
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
gradient.eachAttr((attr) => {
|
|
626
|
+
let positionAttrs = [
|
|
627
|
+
// SVG linearGradient
|
|
628
|
+
"x1",
|
|
629
|
+
"y1",
|
|
630
|
+
"x2",
|
|
631
|
+
"y2",
|
|
632
|
+
// SVG radialGradient, Android VectorDrawable not support 'fx' and 'fy'.
|
|
633
|
+
"cx",
|
|
634
|
+
"cy",
|
|
635
|
+
"r",
|
|
636
|
+
"fx",
|
|
637
|
+
"fy"
|
|
638
|
+
];
|
|
639
|
+
if (positionAttrs.indexOf(attr.name) >= 0) {
|
|
640
|
+
if (!gradient.hasAttr("gradientUnits", "userSpaceOnUse")) {
|
|
641
|
+
let [x1, y1, x2, y2] = pathBounds(elem.attr("d").value);
|
|
642
|
+
if (this.isPercent(attr.value)) {
|
|
643
|
+
let valueFloat = parseFloat(attr.value) / 100;
|
|
644
|
+
if (attr.name === "x1" || attr.name === "x2" || attr.name === "cx" || attr.name === "fx") {
|
|
645
|
+
attr.value = x1 + (x2 - x1) * valueFloat;
|
|
646
|
+
}
|
|
647
|
+
if (attr.name === "y1" || attr.name === "y2" || attr.name === "cy" || attr.name === "fy") {
|
|
648
|
+
attr.value = y1 + (y2 - y1) * valueFloat;
|
|
649
|
+
}
|
|
650
|
+
if (attr.name === "r") {
|
|
651
|
+
attr.value = Math.max(x2 - x1, y2 - y1) * valueFloat;
|
|
652
|
+
}
|
|
653
|
+
} else {
|
|
654
|
+
if (attr.name === "x1" || attr.name === "x2" || attr.name === "cx" || attr.name === "fx") {
|
|
655
|
+
attr.value = x1 + attr.value;
|
|
656
|
+
}
|
|
657
|
+
if (attr.name === "y1" || attr.name === "y2" || attr.name === "cy" || attr.name === "fy") {
|
|
658
|
+
attr.value = y1 + attr.value;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
} else {
|
|
662
|
+
if (this.isPercent(attr.value)) {
|
|
663
|
+
let valueFloat = parseFloat(attr.value) / 100;
|
|
664
|
+
if (attr.name === "x1" || attr.name === "x2" || attr.name === "cx" || attr.name === "fx") {
|
|
665
|
+
attr.value = this.viewportWidth * valueFloat;
|
|
666
|
+
}
|
|
667
|
+
if (attr.name === "y1" || attr.name === "y2" || attr.name === "cy" || attr.name === "fy") {
|
|
668
|
+
attr.value = this.viewportHeight * valueFloat;
|
|
669
|
+
}
|
|
670
|
+
if (attr.name === "r") {
|
|
671
|
+
attr.value = Math.max(this.viewportWidth, this.viewportHeight) * valueFloat;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
attr.value = this.round(attr.value, floatPrecision);
|
|
676
|
+
}
|
|
677
|
+
}, this);
|
|
678
|
+
};
|
|
679
|
+
JS2XML.prototype.mergeColorAndOpacity = function(androidColorHex, opacity) {
|
|
680
|
+
let opacityFromAndroidColor = parseInt(androidColorHex.substr(1, 2), 16) / 255;
|
|
681
|
+
let opacityHex = Number(Math.round(opacity * opacityFromAndroidColor * 255)).toString(16).toUpperCase();
|
|
682
|
+
if (opacityHex.length === 1) {
|
|
683
|
+
opacityHex = "0" + opacityHex;
|
|
684
|
+
}
|
|
685
|
+
return "#" + opacityHex + androidColorHex.substr(-6);
|
|
686
|
+
};
|
|
687
|
+
JS2XML.prototype.isPercent = function(value) {
|
|
688
|
+
return /(-)?\d+(\.d+)?%$/.test(String(value));
|
|
689
|
+
};
|
|
690
|
+
JS2XML.prototype.round = function(value, floatPrecision) {
|
|
691
|
+
return Math.round(value * Math.pow(10, floatPrecision)) / Math.pow(10, floatPrecision);
|
|
692
|
+
};
|
|
693
|
+
JS2XML.prototype.convert = function(data, floatPrecision, strict, fillBlack, tint) {
|
|
694
|
+
this.refactorData(data, floatPrecision, fillBlack, tint);
|
|
695
|
+
return this.travelConvert(data, strict);
|
|
696
|
+
};
|
|
697
|
+
JS2XML.prototype.travelConvert = function(data, strict) {
|
|
698
|
+
let xml = "";
|
|
699
|
+
this.indentLevel++;
|
|
700
|
+
if (data.children) {
|
|
701
|
+
data.children.forEach((item) => {
|
|
702
|
+
if (this.vectordrawableTags.indexOf(item.name) >= 0) {
|
|
703
|
+
xml += this.createElement(item, strict);
|
|
704
|
+
} else if (strict) {
|
|
705
|
+
throw new Error("Unsupported element " + item.name);
|
|
706
|
+
}
|
|
707
|
+
}, this);
|
|
708
|
+
}
|
|
709
|
+
this.indentLevel--;
|
|
710
|
+
return xml;
|
|
711
|
+
};
|
|
712
|
+
JS2XML.prototype.createElement = function(data, strict) {
|
|
713
|
+
if (data.isEmpty()) {
|
|
714
|
+
return this.createIndent() + "<" + data.name + this.createAttrs(data, strict) + "/>\n";
|
|
715
|
+
} else {
|
|
716
|
+
let processedData = "";
|
|
717
|
+
processedData += this.travelConvert(data, strict);
|
|
718
|
+
return this.createIndent() + "<" + data.name + this.createAttrs(data, strict) + ">\n" + processedData + this.createIndent() + "</" + data.name + ">\n";
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
JS2XML.prototype.createAttrs = function(elem, strict) {
|
|
722
|
+
let attrs = "";
|
|
723
|
+
if (elem.name === "vector") {
|
|
724
|
+
attrs += ' xmlns:android="http://schemas.android.com/apk/res/android"';
|
|
725
|
+
}
|
|
726
|
+
elem.eachAttr(function(attr) {
|
|
727
|
+
if (attr.value !== void 0) {
|
|
728
|
+
if (this.vectordrawableAttrs.indexOf(attr.name) >= 0) {
|
|
729
|
+
if (["fillColor", "strokeColor", "color"].indexOf(attr.local) >= 0) {
|
|
730
|
+
attr.value = this.simplifyAndroidHexCode(attr.value);
|
|
731
|
+
}
|
|
732
|
+
if (elem.name === "aapt:attr" && attr.name === "name") {
|
|
733
|
+
attrs += " " + attr.name + '="' + attr.value + '"';
|
|
734
|
+
} else {
|
|
735
|
+
attrs += "\n" + this.createIndent() + " ".repeat(this.indent) + attr.name + '="' + attr.value + '"';
|
|
736
|
+
}
|
|
737
|
+
} else if (strict) {
|
|
738
|
+
throw new Error("Unsupported attribute " + attr.name);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}, this);
|
|
742
|
+
return attrs;
|
|
743
|
+
};
|
|
744
|
+
JS2XML.prototype.svgHexToAndroid = function(hexColor) {
|
|
745
|
+
const toByte = (value) => {
|
|
746
|
+
let numeric = value;
|
|
747
|
+
if (typeof numeric === "string" && numeric.trim().endsWith("%")) {
|
|
748
|
+
numeric = Math.round(parseFloat(numeric) * 2.55);
|
|
749
|
+
}
|
|
750
|
+
numeric = Math.min(255, Math.max(0, Math.round(Number(numeric))));
|
|
751
|
+
return numeric;
|
|
752
|
+
};
|
|
753
|
+
const rgbaToHex = (r, g, b, a = 1) => {
|
|
754
|
+
const alpha = Math.min(1, Math.max(0, Number(a)));
|
|
755
|
+
const rHex = toByte(r).toString(16).padStart(2, "0");
|
|
756
|
+
const gHex = toByte(g).toString(16).padStart(2, "0");
|
|
757
|
+
const bHex = toByte(b).toString(16).padStart(2, "0");
|
|
758
|
+
const aHex = Math.round(alpha * 255).toString(16).padStart(2, "0");
|
|
759
|
+
return `#${rHex}${gHex}${bHex}${aHex}`;
|
|
760
|
+
};
|
|
761
|
+
const hslToRgb = (h, s, l) => {
|
|
762
|
+
const hue = (Number(h) % 360 + 360) % 360 / 360;
|
|
763
|
+
const sat = Math.min(1, Math.max(0, s));
|
|
764
|
+
const light = Math.min(1, Math.max(0, l));
|
|
765
|
+
if (sat === 0) {
|
|
766
|
+
const grey = Math.round(light * 255);
|
|
767
|
+
return [grey, grey, grey];
|
|
768
|
+
}
|
|
769
|
+
const q = light < 0.5 ? light * (1 + sat) : light + sat - light * sat;
|
|
770
|
+
const p = 2 * light - q;
|
|
771
|
+
const hueToChannel = (t) => {
|
|
772
|
+
let temp = t;
|
|
773
|
+
if (temp < 0) temp += 1;
|
|
774
|
+
if (temp > 1) temp -= 1;
|
|
775
|
+
if (temp < 1 / 6) return p + (q - p) * 6 * temp;
|
|
776
|
+
if (temp < 1 / 2) return q;
|
|
777
|
+
if (temp < 2 / 3) return p + (q - p) * (2 / 3 - temp) * 6;
|
|
778
|
+
return p;
|
|
779
|
+
};
|
|
780
|
+
return [
|
|
781
|
+
Math.round(hueToChannel(hue + 1 / 3) * 255),
|
|
782
|
+
Math.round(hueToChannel(hue) * 255),
|
|
783
|
+
Math.round(hueToChannel(hue - 1 / 3) * 255)
|
|
784
|
+
];
|
|
785
|
+
};
|
|
786
|
+
const keywordColors = {
|
|
787
|
+
black: "#000000FF",
|
|
788
|
+
white: "#FFFFFFFF",
|
|
789
|
+
red: "#FF0000FF",
|
|
790
|
+
green: "#008000FF",
|
|
791
|
+
blue: "#0000FFFF",
|
|
792
|
+
yellow: "#FFFF00FF",
|
|
793
|
+
cyan: "#00FFFFFF",
|
|
794
|
+
magenta: "#FF00FFFF",
|
|
795
|
+
gray: "#808080FF",
|
|
796
|
+
grey: "#808080FF",
|
|
797
|
+
orange: "#FFA500FF",
|
|
798
|
+
purple: "#800080FF",
|
|
799
|
+
pink: "#FFC0CBFF",
|
|
800
|
+
brown: "#A52A2AFF",
|
|
801
|
+
transparent: "#00000000"
|
|
802
|
+
};
|
|
803
|
+
const normalized = hexColor.trim();
|
|
804
|
+
if (/^#[0-9a-f]{8}$/i.test(normalized)) {
|
|
805
|
+
hexColor = "#" + normalized.substr(7, 2) + normalized.substr(1, 6);
|
|
806
|
+
} else if (/^#[0-9a-f]{4}$/i.test(normalized)) {
|
|
807
|
+
hexColor = "#" + normalized[4].repeat(2) + normalized[1].repeat(2) + normalized[2].repeat(2) + normalized[3].repeat(2);
|
|
808
|
+
} else if (/^#[0-9a-f]{6}$/i.test(normalized)) {
|
|
809
|
+
hexColor = "#FF" + normalized.substr(1, 6);
|
|
810
|
+
} else if (/^#[0-9a-f]{3}$/i.test(normalized)) {
|
|
811
|
+
hexColor = "#FF" + normalized[1].repeat(2) + normalized[2].repeat(2) + normalized[3].repeat(2);
|
|
812
|
+
} else if (/^rgba?\(/i.test(normalized)) {
|
|
813
|
+
const rgba = normalized.replace(/^rgba?\(/i, "").replace(/\)$/, "").split(",");
|
|
814
|
+
if (rgba.length >= 3) {
|
|
815
|
+
const alpha = rgba.length === 4 ? rgba[3] : 1;
|
|
816
|
+
hexColor = rgbaToHex(rgba[0], rgba[1], rgba[2], alpha);
|
|
817
|
+
} else {
|
|
818
|
+
hexColor = "#FF000000";
|
|
819
|
+
}
|
|
820
|
+
} else if (/^hsla?\(/i.test(normalized)) {
|
|
821
|
+
const hsla = normalized.replace(/^hsla?\(/i, "").replace(/\)$/, "").split(",");
|
|
822
|
+
if (hsla.length >= 3) {
|
|
823
|
+
const hue = parseFloat(hsla[0]);
|
|
824
|
+
const sat = hsla[1].trim().endsWith("%") ? parseFloat(hsla[1]) / 100 : parseFloat(hsla[1]);
|
|
825
|
+
const light = hsla[2].trim().endsWith("%") ? parseFloat(hsla[2]) / 100 : parseFloat(hsla[2]);
|
|
826
|
+
const alpha = hsla.length === 4 ? hsla[3] : 1;
|
|
827
|
+
const [r, g, b] = hslToRgb(hue, sat, light);
|
|
828
|
+
hexColor = rgbaToHex(r, g, b, alpha);
|
|
829
|
+
} else {
|
|
830
|
+
hexColor = "#FF000000";
|
|
831
|
+
}
|
|
832
|
+
} else if (keywordColors[normalized.toLowerCase()]) {
|
|
833
|
+
hexColor = keywordColors[normalized.toLowerCase()];
|
|
834
|
+
} else {
|
|
835
|
+
hexColor = "#FF000000";
|
|
836
|
+
}
|
|
837
|
+
return hexColor.toUpperCase();
|
|
838
|
+
};
|
|
839
|
+
JS2XML.prototype.simplifyAndroidHexCode = function(androidColorHex) {
|
|
840
|
+
if (/#FF[A-F0-9]{6}/.test(androidColorHex)) {
|
|
841
|
+
androidColorHex = "#" + androidColorHex.substr(-6);
|
|
842
|
+
}
|
|
843
|
+
let partOdd = androidColorHex.substr(1).split("").filter((item, index) => {
|
|
844
|
+
return index % 2 === 0;
|
|
845
|
+
}).join("");
|
|
846
|
+
let partEven = androidColorHex.substr(1).split("").filter((item, index) => {
|
|
847
|
+
return index % 2 === 1;
|
|
848
|
+
}).join("");
|
|
849
|
+
if (partOdd === partEven) {
|
|
850
|
+
androidColorHex = "#" + partOdd;
|
|
851
|
+
}
|
|
852
|
+
return androidColorHex;
|
|
853
|
+
};
|
|
854
|
+
JS2XML.prototype.createIndent = function() {
|
|
855
|
+
let indent = " ".repeat(this.indent);
|
|
856
|
+
indent = indent.repeat(this.indentLevel - 1);
|
|
857
|
+
return indent;
|
|
858
|
+
};
|
|
859
|
+
JS2XML.prototype.rectToPathData = function(x, y, width, height, rx, ry) {
|
|
860
|
+
let d = "";
|
|
861
|
+
if (rx === 0 && ry === 0) {
|
|
862
|
+
d = "M" + x + "," + y + "h" + width + "v" + height + "H" + x + "z";
|
|
863
|
+
} else {
|
|
864
|
+
d = "M" + x + "," + (y + ry) + "a" + rx + " -" + ry + " 0 0 1 " + rx + " -" + ry + "h" + (width - rx * 2) + "a" + rx + " " + ry + " 0 0 1 " + rx + " " + ry + "v" + (height - ry * 2) + "a-" + rx + " " + ry + " 0 0 1 -" + rx + " " + ry + "h-" + (width - rx * 2) + "a-" + rx + " -" + ry + " 0 0 1 -" + rx + " -" + ry + "z";
|
|
865
|
+
}
|
|
866
|
+
return d;
|
|
867
|
+
};
|
|
868
|
+
module2.exports = function(svgCode, options) {
|
|
869
|
+
let floatPrecision = 2;
|
|
870
|
+
let strict = false;
|
|
871
|
+
let fillBlack = false;
|
|
872
|
+
let xmlTag = false;
|
|
873
|
+
let tint;
|
|
874
|
+
if (options) {
|
|
875
|
+
if (options.floatPrecision) {
|
|
876
|
+
floatPrecision = options.floatPrecision;
|
|
877
|
+
}
|
|
878
|
+
if (options.strict) {
|
|
879
|
+
strict = options.strict;
|
|
880
|
+
}
|
|
881
|
+
if (options.fillBlack) {
|
|
882
|
+
fillBlack = options.fillBlack;
|
|
883
|
+
}
|
|
884
|
+
if (options.xmlTag) {
|
|
885
|
+
xmlTag = options.xmlTag;
|
|
886
|
+
}
|
|
887
|
+
if (options.tint) {
|
|
888
|
+
tint = options.tint;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return new Promise((resolve, reject) => {
|
|
892
|
+
let data = parseSvg(svgCode);
|
|
893
|
+
if (data.error) {
|
|
894
|
+
reject(data.error);
|
|
895
|
+
} else {
|
|
896
|
+
let xml = new JS2XML().convert(data, floatPrecision, strict, fillBlack, tint);
|
|
897
|
+
if (xmlTag) {
|
|
898
|
+
xml = '<?xml version="1.0" encoding="utf-8"?>\n' + xml;
|
|
899
|
+
}
|
|
900
|
+
resolve(xml);
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
// vendor/svg2vectordrawable/svgo-config.js
|
|
908
|
+
var require_svgo_config = __commonJS({
|
|
909
|
+
"vendor/svg2vectordrawable/svgo-config.js"(exports2, module2) {
|
|
910
|
+
module2.exports = function(floatPrecision = 2) {
|
|
911
|
+
const svgoConfig = {
|
|
912
|
+
info: {
|
|
913
|
+
input: "string"
|
|
914
|
+
},
|
|
915
|
+
plugins: [
|
|
916
|
+
{
|
|
917
|
+
name: "removeDoctype"
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
name: "removeXMLProcInst"
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
name: "removeComments"
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
name: "removeMetadata"
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
name: "removeEditorsNSData"
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
name: "cleanupAttrs"
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
name: "mergeStyles"
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
name: "inlineStyles",
|
|
939
|
+
params: { onlyMatchedOnce: false }
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
name: "minifyStyles"
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
name: "cleanupIDs",
|
|
946
|
+
active: false
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
name: "removeUselessDefs"
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
name: "cleanupNumericValues",
|
|
953
|
+
params: { floatPrecision, leadingZero: false }
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
name: "convertColors",
|
|
957
|
+
params: { shorthex: false, shortname: false }
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
name: "removeUnknownsAndDefaults",
|
|
961
|
+
params: { unknownContent: false, unknownAttrs: false }
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
name: "removeNonInheritableGroupAttrs"
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
name: "removeUselessStrokeAndFill"
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
name: "removeViewBox",
|
|
971
|
+
active: false
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
name: "cleanupEnableBackground"
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
name: "removeHiddenElems"
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
name: "removeEmptyText"
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
name: "convertShapeToPath",
|
|
984
|
+
params: { convertArcs: true, floatPrecision }
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
name: "convertEllipseToCircle"
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
name: "moveElemsAttrsToGroup",
|
|
991
|
+
active: false
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
name: "moveGroupAttrsToElems"
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
name: "collapseGroups"
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
name: "convertPathData",
|
|
1001
|
+
params: { floatPrecision, transformPrecision: floatPrecision, leadingZero: false, makeArcs: false, noSpaceAfterFlags: false, collapseRepeated: false }
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
name: "convertTransform"
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
name: "removeEmptyAttrs"
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
name: "removeEmptyContainers"
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
name: "mergePaths",
|
|
1014
|
+
active: false
|
|
1015
|
+
},
|
|
1016
|
+
{
|
|
1017
|
+
name: "removeUnusedNS"
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
name: "sortDefsChildren"
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
name: "removeTitle"
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
name: "removeDesc"
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
name: "removeXMLNS",
|
|
1030
|
+
active: false
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
name: "removeRasterImages"
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
name: "cleanupListOfValues",
|
|
1037
|
+
params: { floatPrecision, leadingZero: false }
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
name: "sortAttrs",
|
|
1041
|
+
active: false
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
name: "convertStyleToAttrs",
|
|
1045
|
+
active: false
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
name: "prefixIds",
|
|
1049
|
+
active: false
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
name: "removeDimensions",
|
|
1053
|
+
active: false
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
name: "removeAttrs",
|
|
1057
|
+
active: false
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
name: "removeAttributesBySelector",
|
|
1061
|
+
active: false
|
|
1062
|
+
},
|
|
1063
|
+
{
|
|
1064
|
+
name: "removeElementsByAttr",
|
|
1065
|
+
active: false
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
name: "addClassesToSVGElement",
|
|
1069
|
+
active: false
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
name: "removeStyleElement",
|
|
1073
|
+
active: false
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
name: "removeScriptElement",
|
|
1077
|
+
active: false
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
name: "addAttributesToSVGElement",
|
|
1081
|
+
active: false
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
name: "removeOffCanvasPaths",
|
|
1085
|
+
active: false
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
name: "reusePaths",
|
|
1089
|
+
active: false
|
|
1090
|
+
}
|
|
1091
|
+
]
|
|
1092
|
+
};
|
|
1093
|
+
return svgoConfig;
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
// vendor/svg2vectordrawable/main.node.js
|
|
1099
|
+
var require_main_node = __commonJS({
|
|
1100
|
+
"vendor/svg2vectordrawable/main.node.js"(exports2, module2) {
|
|
1101
|
+
var { optimize } = require("svgo");
|
|
1102
|
+
var svg2vectordrawable = require_svg_to_vectordrawable();
|
|
1103
|
+
var svgoConfig = require_svgo_config();
|
|
1104
|
+
module2.exports = function(svgCode, options) {
|
|
1105
|
+
let floatPrecision = options ? options.floatPrecision : 2;
|
|
1106
|
+
const result = optimize(svgCode, svgoConfig(floatPrecision));
|
|
1107
|
+
svgCode = result.data;
|
|
1108
|
+
return svg2vectordrawable(svgCode, options);
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
// vendor/svg2vectordrawable/index.js
|
|
1114
|
+
var require_svg2vectordrawable = __commonJS({
|
|
1115
|
+
"vendor/svg2vectordrawable/index.js"(exports2, module2) {
|
|
1116
|
+
module2.exports = require_main_node();
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
// src/tools/svgTool.js
|
|
1121
|
+
var require_svgTool = __commonJS({
|
|
1122
|
+
"src/tools/svgTool.js"(exports2, module2) {
|
|
1123
|
+
var fs = require("fs/promises");
|
|
1124
|
+
var path = require("path");
|
|
1125
|
+
var { createHash } = require("crypto");
|
|
1126
|
+
var z = require("zod/v4");
|
|
1127
|
+
var svg2vectordrawable = require_svg2vectordrawable();
|
|
1128
|
+
var svgToolInstructions2 = [
|
|
1129
|
+
"Use this server to convert SVG into Android VectorDrawable XML (fast, cached).",
|
|
1130
|
+
"Call tool convert-svg-to-android-drawable for any SVG\u2192VectorDrawable conversion or option tuning.",
|
|
1131
|
+
"Prefer inline SVG via svg; if using svgPath, pass absolute or caller-provided paths only\u2014do not invent paths.",
|
|
1132
|
+
"Set outputPath only if a file should be written; otherwise XML is returned inline.",
|
|
1133
|
+
"Defaults: floatPrecision=2, fillBlack=false, xmlTag=false, cache=true.",
|
|
1134
|
+
"Use fillBlack=true only when the SVG lacks fill and black is desired; set tint only if the caller explicitly requests a tint color.",
|
|
1135
|
+
"Do not alter caller SVG content beyond conversion; keep inputs as provided."
|
|
1136
|
+
].join("\n");
|
|
1137
|
+
var convertInputSchema = z.object({
|
|
1138
|
+
svg: z.string().min(1).describe("Inline SVG markup to convert").optional(),
|
|
1139
|
+
svgPath: z.string().min(1).describe("Path to an SVG file to read").optional(),
|
|
1140
|
+
outputPath: z.string().min(1).describe("Optional output path for generated VectorDrawable XML").optional(),
|
|
1141
|
+
floatPrecision: z.number().int().min(0).max(6).default(2).describe("Decimal precision when serializing coordinates"),
|
|
1142
|
+
fillBlack: z.boolean().default(false).describe("Force fill color black when missing"),
|
|
1143
|
+
xmlTag: z.boolean().default(false).describe("Include XML declaration"),
|
|
1144
|
+
tint: z.string().min(1).optional().describe("Android tint color (e.g. #FF000000)"),
|
|
1145
|
+
cache: z.boolean().default(true).describe("Reuse cached result for identical inputs within this process")
|
|
1146
|
+
}).refine((data) => data.svg || data.svgPath, { message: "Provide either svg or svgPath" });
|
|
1147
|
+
var conversionCache = /* @__PURE__ */ new Map();
|
|
1148
|
+
var MAX_CACHE_SIZE = 32;
|
|
1149
|
+
function makeCacheKey(svg, options) {
|
|
1150
|
+
const hash = createHash("sha256");
|
|
1151
|
+
hash.update(svg);
|
|
1152
|
+
hash.update(JSON.stringify(options));
|
|
1153
|
+
return hash.digest("hex");
|
|
1154
|
+
}
|
|
1155
|
+
function getCached(key) {
|
|
1156
|
+
const existing = conversionCache.get(key);
|
|
1157
|
+
if (!existing) return null;
|
|
1158
|
+
conversionCache.delete(key);
|
|
1159
|
+
conversionCache.set(key, existing);
|
|
1160
|
+
return existing;
|
|
1161
|
+
}
|
|
1162
|
+
function setCache(key, value) {
|
|
1163
|
+
if (conversionCache.size >= MAX_CACHE_SIZE) {
|
|
1164
|
+
const oldestKey = conversionCache.keys().next().value;
|
|
1165
|
+
if (oldestKey) {
|
|
1166
|
+
conversionCache.delete(oldestKey);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
conversionCache.set(key, value);
|
|
1170
|
+
}
|
|
1171
|
+
async function loadSvg(params) {
|
|
1172
|
+
if (params.svg) return params.svg;
|
|
1173
|
+
const resolvedPath = path.resolve(params.svgPath);
|
|
1174
|
+
return fs.readFile(resolvedPath, "utf8");
|
|
1175
|
+
}
|
|
1176
|
+
async function maybeWriteOutput(outputPath, xml) {
|
|
1177
|
+
if (!outputPath) return null;
|
|
1178
|
+
const resolvedPath = path.resolve(outputPath);
|
|
1179
|
+
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
1180
|
+
await fs.writeFile(resolvedPath, xml, "utf8");
|
|
1181
|
+
return resolvedPath;
|
|
1182
|
+
}
|
|
1183
|
+
function registerSvgTool2(server2) {
|
|
1184
|
+
server2.registerTool(
|
|
1185
|
+
"convert-svg-to-android-drawable",
|
|
1186
|
+
{
|
|
1187
|
+
title: "SVG to VectorDrawable",
|
|
1188
|
+
description: "Convert SVG markup or files into Android VectorDrawable XML quickly, optionally writing to disk.",
|
|
1189
|
+
inputSchema: convertInputSchema
|
|
1190
|
+
},
|
|
1191
|
+
async (params, extra) => {
|
|
1192
|
+
const svgCode = await loadSvg(params);
|
|
1193
|
+
const options = {
|
|
1194
|
+
floatPrecision: params.floatPrecision,
|
|
1195
|
+
fillBlack: params.fillBlack,
|
|
1196
|
+
xmlTag: params.xmlTag,
|
|
1197
|
+
tint: params.tint
|
|
1198
|
+
};
|
|
1199
|
+
const cacheKey = makeCacheKey(svgCode, options);
|
|
1200
|
+
const startTime = process.hrtime.bigint();
|
|
1201
|
+
let xml = null;
|
|
1202
|
+
if (params.cache) {
|
|
1203
|
+
xml = getCached(cacheKey);
|
|
1204
|
+
}
|
|
1205
|
+
if (!xml) {
|
|
1206
|
+
xml = await svg2vectordrawable(svgCode, options);
|
|
1207
|
+
if (!xml || typeof xml !== "string") {
|
|
1208
|
+
throw new Error("Conversion did not produce XML");
|
|
1209
|
+
}
|
|
1210
|
+
setCache(cacheKey, xml);
|
|
1211
|
+
}
|
|
1212
|
+
const savedPath = await maybeWriteOutput(params.outputPath, xml);
|
|
1213
|
+
const elapsedMs = Number(process.hrtime.bigint() - startTime) / 1e6;
|
|
1214
|
+
if (extra && typeof extra.sessionId === "string") {
|
|
1215
|
+
server2.sendLoggingMessage(
|
|
1216
|
+
{
|
|
1217
|
+
level: "info",
|
|
1218
|
+
data: `Converted SVG in ${elapsedMs.toFixed(2)}ms` + (savedPath ? ` (saved to ${savedPath})` : "")
|
|
1219
|
+
},
|
|
1220
|
+
extra.sessionId
|
|
1221
|
+
).catch(() => {
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
const content = [];
|
|
1225
|
+
if (savedPath) {
|
|
1226
|
+
content.push({ type: "text", text: `Saved VectorDrawable to ${savedPath}` });
|
|
1227
|
+
}
|
|
1228
|
+
content.push({ type: "text", text: xml });
|
|
1229
|
+
return { content };
|
|
1230
|
+
}
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
module2.exports = {
|
|
1234
|
+
registerSvgTool: registerSvgTool2,
|
|
1235
|
+
svgToolInstructions: svgToolInstructions2
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
// src/tools/logcatTool.js
|
|
1241
|
+
var require_logcatTool = __commonJS({
|
|
1242
|
+
"src/tools/logcatTool.js"(exports2, module2) {
|
|
1243
|
+
var { execFile } = require("child_process");
|
|
1244
|
+
var { promisify } = require("util");
|
|
1245
|
+
var z = require("zod/v4");
|
|
1246
|
+
var execFileAsync = promisify(execFile);
|
|
1247
|
+
var logcatToolInstructions2 = [
|
|
1248
|
+
"Use read-adb-logcat to tail device logs for a package, pid, or tag; default tail=200 lines.",
|
|
1249
|
+
"Use get-pid-by-package to resolve pid quickly via adb shell pidof -s.",
|
|
1250
|
+
"Use get-current-activity to inspect current focus (Activity/Window) via dumpsys window.",
|
|
1251
|
+
"Use fetch-crash-stacktrace to pull the latest crash buffer (-b crash) optionally filtered by pid.",
|
|
1252
|
+
"Use check-anr-state to inspect ActivityManager ANR logs and /data/anr/traces.txt (best-effort).",
|
|
1253
|
+
"Use clear-logcat-buffer to reset logcat (-c) before running new scenarios."
|
|
1254
|
+
].join("\n");
|
|
1255
|
+
var logcatInputSchema = z.object({
|
|
1256
|
+
packageName: z.string().min(1).describe("Android package name; resolves pid via adb shell pidof").optional(),
|
|
1257
|
+
pid: z.string().min(1).describe("Explicit process id for logcat --pid").optional(),
|
|
1258
|
+
tag: z.string().min(1).describe("Logcat tag to include (uses -s tag)").optional(),
|
|
1259
|
+
priority: z.enum(["V", "D", "I", "W", "E", "F", "S"]).default("V").describe("Minimum priority when tag is provided (e.g., D for debug)"),
|
|
1260
|
+
maxLines: z.number().int().min(1).max(2e3).default(200).describe("Tail line count via logcat -t"),
|
|
1261
|
+
timeoutMs: z.number().int().min(1e3).max(15e3).default(5e3).describe("Timeout per adb call in milliseconds")
|
|
1262
|
+
}).refine((data) => data.packageName || data.pid || data.tag, {
|
|
1263
|
+
message: "Provide packageName, pid, or tag to avoid unfiltered logs"
|
|
1264
|
+
});
|
|
1265
|
+
var pidInputSchema = z.object({
|
|
1266
|
+
packageName: z.string().min(1).describe("Android package name to resolve pid via adb shell pidof -s"),
|
|
1267
|
+
timeoutMs: z.number().int().min(1e3).max(15e3).default(5e3).describe("Timeout per adb call in milliseconds")
|
|
1268
|
+
});
|
|
1269
|
+
var currentActivityInputSchema = z.object({
|
|
1270
|
+
timeoutMs: z.number().int().min(1e3).max(15e3).default(5e3).describe("Timeout per adb call in milliseconds")
|
|
1271
|
+
});
|
|
1272
|
+
var crashStackInputSchema = z.object({
|
|
1273
|
+
packageName: z.string().min(1).describe("Optional package to resolve pid; filters crash buffer with --pid").optional(),
|
|
1274
|
+
maxLines: z.number().int().min(50).max(2e3).default(400).describe("Tail line count from crash buffer (-b crash -t)"),
|
|
1275
|
+
timeoutMs: z.number().int().min(1e3).max(15e3).default(5e3).describe("Timeout per adb call in milliseconds")
|
|
1276
|
+
});
|
|
1277
|
+
var anrStateInputSchema = z.object({
|
|
1278
|
+
maxLines: z.number().int().min(50).max(2e3).default(400).describe("Tail line count from ActivityManager:E"),
|
|
1279
|
+
timeoutMs: z.number().int().min(1e3).max(15e3).default(5e3).describe("Timeout per adb call in milliseconds")
|
|
1280
|
+
});
|
|
1281
|
+
var clearLogcatInputSchema = z.object({
|
|
1282
|
+
timeoutMs: z.number().int().min(1e3).max(15e3).default(5e3).describe("Timeout per adb call in milliseconds")
|
|
1283
|
+
});
|
|
1284
|
+
async function runAdbCommand(args, timeoutMs) {
|
|
1285
|
+
try {
|
|
1286
|
+
const { stdout } = await execFileAsync("adb", args, {
|
|
1287
|
+
timeout: timeoutMs,
|
|
1288
|
+
maxBuffer: 5 * 1024 * 1024
|
|
1289
|
+
});
|
|
1290
|
+
return stdout.trimEnd();
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
const stderr = error && typeof error.stderr === "string" ? error.stderr.trim() : "";
|
|
1293
|
+
const message = [`adb ${args.join(" ")} failed`, error.message].filter(Boolean).join(": ");
|
|
1294
|
+
if (stderr) {
|
|
1295
|
+
throw new Error(`${message} | stderr: ${stderr}`);
|
|
1296
|
+
}
|
|
1297
|
+
throw new Error(message);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
async function resolvePid(packageName, timeoutMs) {
|
|
1301
|
+
const output = await runAdbCommand(["shell", "pidof", "-s", packageName], timeoutMs);
|
|
1302
|
+
const pid = output.split(/\s+/).find(Boolean);
|
|
1303
|
+
if (!pid) {
|
|
1304
|
+
throw new Error(`Could not resolve pid for package ${packageName}`);
|
|
1305
|
+
}
|
|
1306
|
+
return pid;
|
|
1307
|
+
}
|
|
1308
|
+
function buildLogcatArgs(params, pid) {
|
|
1309
|
+
const args = ["logcat", "-d", "-t", String(params.maxLines)];
|
|
1310
|
+
if (pid) {
|
|
1311
|
+
args.push(`--pid=${pid}`);
|
|
1312
|
+
}
|
|
1313
|
+
if (params.tag) {
|
|
1314
|
+
const filterSpec = `${params.tag}:${params.priority}`;
|
|
1315
|
+
args.push("-s", filterSpec);
|
|
1316
|
+
}
|
|
1317
|
+
return args;
|
|
1318
|
+
}
|
|
1319
|
+
function registerLogcatTool2(server2) {
|
|
1320
|
+
server2.registerTool(
|
|
1321
|
+
"read-adb-logcat",
|
|
1322
|
+
{
|
|
1323
|
+
title: "Read adb logcat",
|
|
1324
|
+
description: "Dump recent adb logcat output scoped by package, pid, or tag with tail and timeout controls.",
|
|
1325
|
+
inputSchema: logcatInputSchema
|
|
1326
|
+
},
|
|
1327
|
+
async (params, extra) => {
|
|
1328
|
+
const timeoutMs = params.timeoutMs;
|
|
1329
|
+
const pid = params.pid || (params.packageName ? await resolvePid(params.packageName, timeoutMs) : null);
|
|
1330
|
+
const args = buildLogcatArgs(params, pid);
|
|
1331
|
+
const startTime = process.hrtime.bigint();
|
|
1332
|
+
const output = await runAdbCommand(args, timeoutMs);
|
|
1333
|
+
const elapsedMs = Number(process.hrtime.bigint() - startTime) / 1e6;
|
|
1334
|
+
if (extra && typeof extra.sessionId === "string") {
|
|
1335
|
+
server2.sendLoggingMessage(
|
|
1336
|
+
{
|
|
1337
|
+
level: "info",
|
|
1338
|
+
data: `Read logcat (${params.maxLines} lines` + (pid ? `, pid=${pid}` : "") + (params.tag ? `, tag=${params.tag}:${params.priority}` : "") + `) in ${elapsedMs.toFixed(2)}ms`
|
|
1339
|
+
},
|
|
1340
|
+
extra.sessionId
|
|
1341
|
+
).catch(() => {
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
if (!output) {
|
|
1345
|
+
return { content: [{ type: "text", text: "Logcat returned no lines." }] };
|
|
1346
|
+
}
|
|
1347
|
+
return { content: [{ type: "text", text: output }] };
|
|
1348
|
+
}
|
|
1349
|
+
);
|
|
1350
|
+
server2.registerTool(
|
|
1351
|
+
"get-pid-by-package",
|
|
1352
|
+
{
|
|
1353
|
+
title: "Get pid by package",
|
|
1354
|
+
description: "Resolve process id for a package via adb shell pidof -s.",
|
|
1355
|
+
inputSchema: pidInputSchema
|
|
1356
|
+
},
|
|
1357
|
+
async (params) => {
|
|
1358
|
+
const pid = await resolvePid(params.packageName, params.timeoutMs);
|
|
1359
|
+
return { content: [{ type: "text", text: pid }] };
|
|
1360
|
+
}
|
|
1361
|
+
);
|
|
1362
|
+
server2.registerTool(
|
|
1363
|
+
"get-current-activity",
|
|
1364
|
+
{
|
|
1365
|
+
title: "Get current activity/window focus",
|
|
1366
|
+
description: "Inspect current focused app/window via dumpsys window (mCurrentFocus/mFocusedApp). Useful even in single-activity apps to verify top window.",
|
|
1367
|
+
inputSchema: currentActivityInputSchema
|
|
1368
|
+
},
|
|
1369
|
+
async (params) => {
|
|
1370
|
+
const dump = await runAdbCommand(["shell", "dumpsys", "window"], params.timeoutMs);
|
|
1371
|
+
const lines = dump.split("\n").filter((line) => line.includes("mCurrentFocus") || line.includes("mFocusedApp"));
|
|
1372
|
+
const trimmed = lines.slice(0, 8).join("\n").trim();
|
|
1373
|
+
if (!trimmed) {
|
|
1374
|
+
return { content: [{ type: "text", text: "No focus info found in dumpsys window." }] };
|
|
1375
|
+
}
|
|
1376
|
+
return { content: [{ type: "text", text: trimmed }] };
|
|
1377
|
+
}
|
|
1378
|
+
);
|
|
1379
|
+
server2.registerTool(
|
|
1380
|
+
"fetch-crash-stacktrace",
|
|
1381
|
+
{
|
|
1382
|
+
title: "Fetch crash stacktrace (crash buffer)",
|
|
1383
|
+
description: "Pull recent crash buffer (-b crash -d -t) optionally filtered by pid resolved from package.",
|
|
1384
|
+
inputSchema: crashStackInputSchema
|
|
1385
|
+
},
|
|
1386
|
+
async (params) => {
|
|
1387
|
+
const pid = params.packageName ? await resolvePid(params.packageName, params.timeoutMs) : null;
|
|
1388
|
+
const args = ["logcat", "-b", "crash", "-d", "-t", String(params.maxLines)];
|
|
1389
|
+
if (pid) {
|
|
1390
|
+
args.push(`--pid=${pid}`);
|
|
1391
|
+
}
|
|
1392
|
+
const output = await runAdbCommand(args, params.timeoutMs);
|
|
1393
|
+
if (!output) {
|
|
1394
|
+
return { content: [{ type: "text", text: "No crash entries found." }] };
|
|
1395
|
+
}
|
|
1396
|
+
return { content: [{ type: "text", text: output }] };
|
|
1397
|
+
}
|
|
1398
|
+
);
|
|
1399
|
+
server2.registerTool(
|
|
1400
|
+
"check-anr-state",
|
|
1401
|
+
{
|
|
1402
|
+
title: "Check ANR state (ActivityManager + traces)",
|
|
1403
|
+
description: "Check recent ActivityManager ANR logs and tail /data/anr/traces.txt when accessible (best-effort, may require root/debuggable).",
|
|
1404
|
+
inputSchema: anrStateInputSchema
|
|
1405
|
+
},
|
|
1406
|
+
async (params) => {
|
|
1407
|
+
const sections = [];
|
|
1408
|
+
try {
|
|
1409
|
+
const amLogs = await runAdbCommand(
|
|
1410
|
+
["logcat", "-d", "-t", String(params.maxLines), "ActivityManager:E", "*:S"],
|
|
1411
|
+
params.timeoutMs
|
|
1412
|
+
);
|
|
1413
|
+
if (amLogs) {
|
|
1414
|
+
sections.push("ActivityManager (recent):\n" + amLogs);
|
|
1415
|
+
} else {
|
|
1416
|
+
sections.push("ActivityManager (recent): no entries.");
|
|
1417
|
+
}
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
sections.push(`ActivityManager: ${error.message}`);
|
|
1420
|
+
}
|
|
1421
|
+
try {
|
|
1422
|
+
const stat = await runAdbCommand(["shell", "ls", "-l", "/data/anr/traces.txt"], params.timeoutMs);
|
|
1423
|
+
sections.push("traces.txt stat:\n" + stat);
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
sections.push(`traces.txt stat: ${error.message}`);
|
|
1426
|
+
}
|
|
1427
|
+
try {
|
|
1428
|
+
const tail = await runAdbCommand(
|
|
1429
|
+
["shell", "tail", "-n", "200", "/data/anr/traces.txt"],
|
|
1430
|
+
params.timeoutMs
|
|
1431
|
+
);
|
|
1432
|
+
if (tail) {
|
|
1433
|
+
sections.push("traces.txt tail (200 lines):\n" + tail);
|
|
1434
|
+
} else {
|
|
1435
|
+
sections.push("traces.txt tail: empty.");
|
|
1436
|
+
}
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
sections.push(`traces.txt tail: ${error.message}`);
|
|
1439
|
+
}
|
|
1440
|
+
return { content: [{ type: "text", text: sections.join("\n\n") }] };
|
|
1441
|
+
}
|
|
1442
|
+
);
|
|
1443
|
+
server2.registerTool(
|
|
1444
|
+
"clear-logcat-buffer",
|
|
1445
|
+
{
|
|
1446
|
+
title: "Clear logcat buffer",
|
|
1447
|
+
description: "Run adb logcat -c to clear buffers before a new scenario.",
|
|
1448
|
+
inputSchema: clearLogcatInputSchema
|
|
1449
|
+
},
|
|
1450
|
+
async (params) => {
|
|
1451
|
+
await runAdbCommand(["logcat", "-c"], params.timeoutMs);
|
|
1452
|
+
return { content: [{ type: "text", text: "Cleared logcat buffers." }] };
|
|
1453
|
+
}
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
module2.exports = {
|
|
1457
|
+
registerLogcatTool: registerLogcatTool2,
|
|
1458
|
+
logcatToolInstructions: logcatToolInstructions2
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
// src/index.js
|
|
1464
|
+
var { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
1465
|
+
var { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
1466
|
+
var { registerSvgTool, svgToolInstructions } = require_svgTool();
|
|
1467
|
+
var { registerLogcatTool, logcatToolInstructions } = require_logcatTool();
|
|
1468
|
+
var serverInstructions = [svgToolInstructions, logcatToolInstructions].join("\n");
|
|
1469
|
+
var server = new McpServer(
|
|
1470
|
+
{
|
|
1471
|
+
name: "svg-to-android-drawable",
|
|
1472
|
+
version: "1.1.0"
|
|
1473
|
+
},
|
|
1474
|
+
{
|
|
1475
|
+
capabilities: { logging: {} },
|
|
1476
|
+
instructions: serverInstructions
|
|
1477
|
+
}
|
|
1478
|
+
);
|
|
1479
|
+
registerSvgTool(server);
|
|
1480
|
+
registerLogcatTool(server);
|
|
1481
|
+
async function main() {
|
|
1482
|
+
const transport = new StdioServerTransport();
|
|
1483
|
+
await server.connect(transport);
|
|
1484
|
+
process.stdin.resume();
|
|
1485
|
+
}
|
|
1486
|
+
main().catch((error) => {
|
|
1487
|
+
console.error("Failed to start MCP server:", error);
|
|
1488
|
+
process.exit(1);
|
|
1489
|
+
});
|