@artifact-kit/deckkit-pro 0.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/COMMERCIAL-LICENSE.md +5 -0
- package/README.md +50 -0
- package/dist/cli.cjs +194 -0
- package/dist/cli.js +193 -0
- package/dist/gradient-fill.cjs +44 -0
- package/dist/gradient-fill.js +44 -0
- package/dist/image.cjs +102 -0
- package/dist/image.js +102 -0
- package/dist/index.cjs +31 -0
- package/dist/index.js +33 -0
- package/dist/svg-to-custom-geometry.cjs +195 -0
- package/dist/svg-to-custom-geometry.js +195 -0
- package/dist/svg-to-png-cli.cjs +108 -0
- package/dist/svg-to-png-cli.js +107 -0
- package/dist/svg-to-png.cjs +27 -0
- package/dist/svg-to-png.js +27 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/gradient-fill.d.ts +22 -0
- package/dist/types/gradient-fill.d.ts.map +1 -0
- package/dist/types/image.d.ts +62 -0
- package/dist/types/image.d.ts.map +1 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/svg-to-custom-geometry.d.ts +40 -0
- package/dist/types/svg-to-custom-geometry.d.ts.map +1 -0
- package/dist/types/svg-to-png-cli.d.ts +3 -0
- package/dist/types/svg-to-png-cli.d.ts.map +1 -0
- package/dist/types/svg-to-png.d.ts +18 -0
- package/dist/types/svg-to-png.d.ts.map +1 -0
- package/package.json +97 -0
package/dist/image.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import sharp from "sharp";
|
|
4
|
+
async function getImageInfo(input) {
|
|
5
|
+
const metadata = await sharp(input).metadata();
|
|
6
|
+
return {
|
|
7
|
+
width: metadata.width,
|
|
8
|
+
height: metadata.height,
|
|
9
|
+
format: metadata.format,
|
|
10
|
+
channels: metadata.channels,
|
|
11
|
+
space: metadata.space,
|
|
12
|
+
hasAlpha: metadata.hasAlpha,
|
|
13
|
+
density: metadata.density,
|
|
14
|
+
size: metadata.size
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
async function cropImage(input, options) {
|
|
18
|
+
return sharp(input).extract({
|
|
19
|
+
left: options.x,
|
|
20
|
+
top: options.y,
|
|
21
|
+
width: options.width,
|
|
22
|
+
height: options.height
|
|
23
|
+
}).png().toBuffer();
|
|
24
|
+
}
|
|
25
|
+
async function resizeImage(input, options) {
|
|
26
|
+
const image = sharp(input).resize({
|
|
27
|
+
width: options.width,
|
|
28
|
+
height: options.height,
|
|
29
|
+
fit: options.fit ?? "contain",
|
|
30
|
+
background: options.background
|
|
31
|
+
});
|
|
32
|
+
return image.png().toBuffer();
|
|
33
|
+
}
|
|
34
|
+
async function sampleColor(input, x, y) {
|
|
35
|
+
const { data, info } = await sharp(input).ensureAlpha().extract({ left: x, top: y, width: 1, height: 1 }).raw().toBuffer({ resolveWithObject: true });
|
|
36
|
+
return {
|
|
37
|
+
x,
|
|
38
|
+
y,
|
|
39
|
+
r: data[0] ?? 0,
|
|
40
|
+
g: data[1] ?? 0,
|
|
41
|
+
b: data[2] ?? 0,
|
|
42
|
+
alpha: info.channels >= 4 ? data[3] : void 0
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async function compareImages(reference, generated, options = {}) {
|
|
46
|
+
const [refBuffer, genBuffer] = await Promise.all([
|
|
47
|
+
sharp(reference).png().toBuffer(),
|
|
48
|
+
sharp(generated).png().toBuffer()
|
|
49
|
+
]);
|
|
50
|
+
const [refInfo, genInfo] = await Promise.all([getImageInfo(refBuffer), getImageInfo(genBuffer)]);
|
|
51
|
+
const gap = options.gap ?? 12;
|
|
52
|
+
const width = (refInfo.width ?? 0) + (genInfo.width ?? 0) + gap;
|
|
53
|
+
const height = Math.max(refInfo.height ?? 0, genInfo.height ?? 0);
|
|
54
|
+
return sharp({
|
|
55
|
+
create: {
|
|
56
|
+
width,
|
|
57
|
+
height,
|
|
58
|
+
channels: 4,
|
|
59
|
+
background: options.background ?? { r: 255, g: 255, b: 255, alpha: 1 }
|
|
60
|
+
}
|
|
61
|
+
}).composite([
|
|
62
|
+
{ input: refBuffer, left: 0, top: 0 },
|
|
63
|
+
{ input: genBuffer, left: (refInfo.width ?? 0) + gap, top: 0 }
|
|
64
|
+
]).png().toBuffer();
|
|
65
|
+
}
|
|
66
|
+
async function overlayImages(reference, generated, options = {}) {
|
|
67
|
+
const refInfo = await getImageInfo(reference);
|
|
68
|
+
const refWidth = refInfo.width ?? 1;
|
|
69
|
+
const refHeight = refInfo.height ?? 1;
|
|
70
|
+
const generatedBuffer = await withOpacity(
|
|
71
|
+
await sharp(generated).resize({ width: refWidth, height: refHeight, fit: "contain" }).ensureAlpha().png().toBuffer(),
|
|
72
|
+
options.opacity ?? 0.5
|
|
73
|
+
);
|
|
74
|
+
return sharp(reference).resize({ width: refWidth, height: refHeight, fit: "contain", background: options.background }).composite([{ input: generatedBuffer, left: 0, top: 0 }]).png().toBuffer();
|
|
75
|
+
}
|
|
76
|
+
async function withOpacity(input, opacity) {
|
|
77
|
+
const clampedOpacity = Math.max(0, Math.min(1, opacity));
|
|
78
|
+
const { data, info } = await sharp(input).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
79
|
+
for (let index = 3; index < data.length; index += info.channels) {
|
|
80
|
+
data[index] = Math.round((data[index] ?? 0) * clampedOpacity);
|
|
81
|
+
}
|
|
82
|
+
return sharp(data, {
|
|
83
|
+
raw: {
|
|
84
|
+
width: info.width,
|
|
85
|
+
height: info.height,
|
|
86
|
+
channels: info.channels
|
|
87
|
+
}
|
|
88
|
+
}).png().toBuffer();
|
|
89
|
+
}
|
|
90
|
+
async function writeImage(buffer, output) {
|
|
91
|
+
await mkdir(dirname(output), { recursive: true });
|
|
92
|
+
await sharp(buffer).toFile(output);
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
compareImages,
|
|
96
|
+
cropImage,
|
|
97
|
+
getImageInfo,
|
|
98
|
+
overlayImages,
|
|
99
|
+
resizeImage,
|
|
100
|
+
sampleColor,
|
|
101
|
+
writeImage
|
|
102
|
+
};
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
|
|
3
|
+
const gradientFill = require("./gradient-fill.cjs");
|
|
4
|
+
const svgToCustomGeometry = require("./svg-to-custom-geometry.cjs");
|
|
5
|
+
const svgToPng = require("./svg-to-png.cjs");
|
|
6
|
+
const image = require("./image.cjs");
|
|
7
|
+
function deckkitPro() {
|
|
8
|
+
return {
|
|
9
|
+
name: "@artifact-kit/deckkit-pro",
|
|
10
|
+
setup(context) {
|
|
11
|
+
gradientFill.setupGradientFill(context);
|
|
12
|
+
svgToCustomGeometry.setupSvgCustomGeometry(context);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
exports.renderGradientFill = gradientFill.renderGradientFill;
|
|
17
|
+
exports.setupGradientFill = gradientFill.setupGradientFill;
|
|
18
|
+
exports.handleSvgCustomGeometry = svgToCustomGeometry.handleSvgCustomGeometry;
|
|
19
|
+
exports.setupSvgCustomGeometry = svgToCustomGeometry.setupSvgCustomGeometry;
|
|
20
|
+
exports.svgToCustomGeometry = svgToCustomGeometry.svgToCustomGeometry;
|
|
21
|
+
exports.renderSvgToPng = svgToPng.renderSvgToPng;
|
|
22
|
+
exports.writeSvgToPng = svgToPng.writeSvgToPng;
|
|
23
|
+
exports.compareImages = image.compareImages;
|
|
24
|
+
exports.cropImage = image.cropImage;
|
|
25
|
+
exports.getImageInfo = image.getImageInfo;
|
|
26
|
+
exports.overlayImages = image.overlayImages;
|
|
27
|
+
exports.resizeImage = image.resizeImage;
|
|
28
|
+
exports.sampleColor = image.sampleColor;
|
|
29
|
+
exports.writeImage = image.writeImage;
|
|
30
|
+
exports.deckkitPro = deckkitPro;
|
|
31
|
+
exports.default = deckkitPro;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { setupGradientFill } from "./gradient-fill.js";
|
|
2
|
+
import { renderGradientFill } from "./gradient-fill.js";
|
|
3
|
+
import { setupSvgCustomGeometry } from "./svg-to-custom-geometry.js";
|
|
4
|
+
import { handleSvgCustomGeometry, svgToCustomGeometry } from "./svg-to-custom-geometry.js";
|
|
5
|
+
import { renderSvgToPng, writeSvgToPng } from "./svg-to-png.js";
|
|
6
|
+
import { compareImages, cropImage, getImageInfo, overlayImages, resizeImage, sampleColor, writeImage } from "./image.js";
|
|
7
|
+
function deckkitPro() {
|
|
8
|
+
return {
|
|
9
|
+
name: "@artifact-kit/deckkit-pro",
|
|
10
|
+
setup(context) {
|
|
11
|
+
setupGradientFill(context);
|
|
12
|
+
setupSvgCustomGeometry(context);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
compareImages,
|
|
18
|
+
cropImage,
|
|
19
|
+
deckkitPro,
|
|
20
|
+
deckkitPro as default,
|
|
21
|
+
getImageInfo,
|
|
22
|
+
handleSvgCustomGeometry,
|
|
23
|
+
overlayImages,
|
|
24
|
+
renderGradientFill,
|
|
25
|
+
renderSvgToPng,
|
|
26
|
+
resizeImage,
|
|
27
|
+
sampleColor,
|
|
28
|
+
setupGradientFill,
|
|
29
|
+
setupSvgCustomGeometry,
|
|
30
|
+
svgToCustomGeometry,
|
|
31
|
+
writeImage,
|
|
32
|
+
writeSvgToPng
|
|
33
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const paper = require("paper-jsdom");
|
|
4
|
+
function setupSvgCustomGeometry(context) {
|
|
5
|
+
context.addShapeHandler(handleSvgCustomGeometry);
|
|
6
|
+
}
|
|
7
|
+
function handleSvgCustomGeometry({ target, shape, options }) {
|
|
8
|
+
const svg = options.svg;
|
|
9
|
+
if (String(shape) !== "custGeom" || typeof svg !== "string") return void 0;
|
|
10
|
+
const { svg: _svg, svgPrecision, ...baseOptions } = options;
|
|
11
|
+
const result = svgToCustomGeometry(svg, {
|
|
12
|
+
x: typeof options.x === "number" ? options.x : void 0,
|
|
13
|
+
y: typeof options.y === "number" ? options.y : void 0,
|
|
14
|
+
w: typeof options.w === "number" ? options.w : void 0,
|
|
15
|
+
h: typeof options.h === "number" ? options.h : void 0,
|
|
16
|
+
precision: svgPrecision
|
|
17
|
+
});
|
|
18
|
+
result.paths.forEach((path, index) => {
|
|
19
|
+
target.addShape("custGeom", {
|
|
20
|
+
...baseOptions,
|
|
21
|
+
x: options.x,
|
|
22
|
+
y: options.y,
|
|
23
|
+
w: result.width,
|
|
24
|
+
h: result.height,
|
|
25
|
+
objectName: options.objectName ? `${options.objectName} ${index + 1}` : void 0,
|
|
26
|
+
points: path.points,
|
|
27
|
+
fill: path.fill,
|
|
28
|
+
line: path.line
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
function svgToCustomGeometry(svg, options = {}) {
|
|
34
|
+
const viewBox = readViewBox(svg) ?? readSvgSize(svg) ?? [0, 0, 1, 1];
|
|
35
|
+
const [, , viewBoxWidth, viewBoxHeight] = viewBox;
|
|
36
|
+
const width = options.w ?? viewBoxWidth / 100;
|
|
37
|
+
const height = options.h ?? viewBoxHeight / 100;
|
|
38
|
+
const precision = options.precision ?? 5;
|
|
39
|
+
paper.setup(new paper.Size(viewBoxWidth || 1, viewBoxHeight || 1));
|
|
40
|
+
try {
|
|
41
|
+
const rootItem = paper.project.importSVG(svg, {
|
|
42
|
+
expandShapes: true,
|
|
43
|
+
insert: true
|
|
44
|
+
});
|
|
45
|
+
const items = rootItem.getItems({
|
|
46
|
+
match: (item) => item instanceof paper.Path || item instanceof paper.CompoundPath
|
|
47
|
+
});
|
|
48
|
+
const paths = collectUniquePathItems(items).map((pathItem) => paperPathToCustomGeometry(pathItem, viewBox, width, height, precision));
|
|
49
|
+
return {
|
|
50
|
+
width,
|
|
51
|
+
height,
|
|
52
|
+
viewBox,
|
|
53
|
+
paths
|
|
54
|
+
};
|
|
55
|
+
} finally {
|
|
56
|
+
paper.project.clear();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function collectUniquePathItems(items) {
|
|
60
|
+
const seen = /* @__PURE__ */ new Set();
|
|
61
|
+
const paths = [];
|
|
62
|
+
for (const item of items) {
|
|
63
|
+
const pathItems = paperItemToPaths(item);
|
|
64
|
+
for (const pathItem of pathItems) {
|
|
65
|
+
if (seen.has(pathItem)) continue;
|
|
66
|
+
seen.add(pathItem);
|
|
67
|
+
paths.push(pathItem);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return paths;
|
|
71
|
+
}
|
|
72
|
+
function paperItemToPaths(item) {
|
|
73
|
+
if (item instanceof paper.Path) return [item];
|
|
74
|
+
if (item instanceof paper.CompoundPath) return item.children.filter((child) => child instanceof paper.Path);
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
function paperPathToCustomGeometry(pathItem, viewBox, width, height, precision) {
|
|
78
|
+
const stroke = inheritedStroke(pathItem);
|
|
79
|
+
return {
|
|
80
|
+
points: pathToCustomGeometryPoints(pathItem, viewBox, width, height, precision),
|
|
81
|
+
fill: colorToFill(inheritedValue(pathItem, "fillColor")),
|
|
82
|
+
line: colorToLine(stroke.color, stroke.width, viewBox, width, height)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function inheritedStroke(item) {
|
|
86
|
+
let cursor = item;
|
|
87
|
+
while (cursor) {
|
|
88
|
+
if (cursor.strokeColor) {
|
|
89
|
+
return {
|
|
90
|
+
color: cursor.strokeColor,
|
|
91
|
+
width: cursor.strokeWidth
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
cursor = cursor.parent;
|
|
95
|
+
}
|
|
96
|
+
return { color: void 0, width: void 0 };
|
|
97
|
+
}
|
|
98
|
+
function inheritedValue(item, key) {
|
|
99
|
+
let cursor = item;
|
|
100
|
+
while (cursor) {
|
|
101
|
+
const value = cursor[key];
|
|
102
|
+
if (value !== void 0 && value !== null) return value;
|
|
103
|
+
cursor = cursor.parent;
|
|
104
|
+
}
|
|
105
|
+
return void 0;
|
|
106
|
+
}
|
|
107
|
+
function pathToCustomGeometryPoints(pathItem, viewBox, width, height, precision) {
|
|
108
|
+
const segments = pathItem.segments;
|
|
109
|
+
if (!segments || segments.length === 0) return [];
|
|
110
|
+
const points = [movePoint(segments[0].point, viewBox, width, height, precision)];
|
|
111
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
112
|
+
points.push(segmentPoint(segments[index - 1], segments[index], viewBox, width, height, precision));
|
|
113
|
+
}
|
|
114
|
+
if (pathItem.closed) {
|
|
115
|
+
points.push(segmentPoint(segments[segments.length - 1], segments[0], viewBox, width, height, precision));
|
|
116
|
+
points.push({ close: true });
|
|
117
|
+
}
|
|
118
|
+
return points;
|
|
119
|
+
}
|
|
120
|
+
function movePoint(point, viewBox, width, height, precision) {
|
|
121
|
+
return {
|
|
122
|
+
...scalePoint(point, viewBox, width, height, precision),
|
|
123
|
+
moveTo: true
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function segmentPoint(from, to, viewBox, width, height, precision) {
|
|
127
|
+
const end = scalePoint(to.point, viewBox, width, height, precision);
|
|
128
|
+
const fromHandle = from.handleOut;
|
|
129
|
+
const toHandle = to.handleIn;
|
|
130
|
+
if (!fromHandle.isZero() || !toHandle.isZero()) {
|
|
131
|
+
const c1 = scalePoint(from.point.add(fromHandle), viewBox, width, height, precision);
|
|
132
|
+
const c2 = scalePoint(to.point.add(toHandle), viewBox, width, height, precision);
|
|
133
|
+
return {
|
|
134
|
+
...end,
|
|
135
|
+
curve: {
|
|
136
|
+
type: "cubic",
|
|
137
|
+
x1: c1.x,
|
|
138
|
+
y1: c1.y,
|
|
139
|
+
x2: c2.x,
|
|
140
|
+
y2: c2.y
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return end;
|
|
145
|
+
}
|
|
146
|
+
function scalePoint(point, viewBox, width, height, precision) {
|
|
147
|
+
const [minX, minY, viewBoxWidth, viewBoxHeight] = viewBox;
|
|
148
|
+
return {
|
|
149
|
+
x: round((point.x - minX) / viewBoxWidth * width, precision),
|
|
150
|
+
y: round((point.y - minY) / viewBoxHeight * height, precision)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function colorToFill(color) {
|
|
154
|
+
const hex = paperColorToHex(color);
|
|
155
|
+
return hex ? { color: hex } : void 0;
|
|
156
|
+
}
|
|
157
|
+
function colorToLine(color, strokeWidth, viewBox, width, height) {
|
|
158
|
+
const hex = paperColorToHex(color);
|
|
159
|
+
if (!hex) return { type: "none" };
|
|
160
|
+
const [, , viewBoxWidth, viewBoxHeight] = viewBox;
|
|
161
|
+
const scale = (width / viewBoxWidth + height / viewBoxHeight) / 2;
|
|
162
|
+
return {
|
|
163
|
+
color: hex,
|
|
164
|
+
width: strokeWidth ? Math.max(0.1, strokeWidth * scale * 72) : 1
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function paperColorToHex(color) {
|
|
168
|
+
const css = color?.toCSS?.(true);
|
|
169
|
+
if (typeof css !== "string" || !css.startsWith("#")) return void 0;
|
|
170
|
+
return css.slice(1).toUpperCase();
|
|
171
|
+
}
|
|
172
|
+
function readViewBox(svg) {
|
|
173
|
+
const match = svg.match(/\bviewBox=["']([^"']+)["']/);
|
|
174
|
+
if (!match) return void 0;
|
|
175
|
+
const values = match[1].trim().split(/[\s,]+/).map(Number);
|
|
176
|
+
return values.length === 4 && values.every(Number.isFinite) ? values : void 0;
|
|
177
|
+
}
|
|
178
|
+
function readSvgSize(svg) {
|
|
179
|
+
const width = readNumericSvgAttr(svg, "width");
|
|
180
|
+
const height = readNumericSvgAttr(svg, "height");
|
|
181
|
+
return width && height ? [0, 0, width, height] : void 0;
|
|
182
|
+
}
|
|
183
|
+
function readNumericSvgAttr(svg, attr) {
|
|
184
|
+
const match = svg.match(new RegExp(`\\b${attr}=["']([0-9.]+)`));
|
|
185
|
+
if (!match) return void 0;
|
|
186
|
+
const value = Number(match[1]);
|
|
187
|
+
return Number.isFinite(value) ? value : void 0;
|
|
188
|
+
}
|
|
189
|
+
function round(value, precision) {
|
|
190
|
+
const scale = 10 ** precision;
|
|
191
|
+
return Math.round(value * scale) / scale;
|
|
192
|
+
}
|
|
193
|
+
exports.handleSvgCustomGeometry = handleSvgCustomGeometry;
|
|
194
|
+
exports.setupSvgCustomGeometry = setupSvgCustomGeometry;
|
|
195
|
+
exports.svgToCustomGeometry = svgToCustomGeometry;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import paper from "paper-jsdom";
|
|
2
|
+
function setupSvgCustomGeometry(context) {
|
|
3
|
+
context.addShapeHandler(handleSvgCustomGeometry);
|
|
4
|
+
}
|
|
5
|
+
function handleSvgCustomGeometry({ target, shape, options }) {
|
|
6
|
+
const svg = options.svg;
|
|
7
|
+
if (String(shape) !== "custGeom" || typeof svg !== "string") return void 0;
|
|
8
|
+
const { svg: _svg, svgPrecision, ...baseOptions } = options;
|
|
9
|
+
const result = svgToCustomGeometry(svg, {
|
|
10
|
+
x: typeof options.x === "number" ? options.x : void 0,
|
|
11
|
+
y: typeof options.y === "number" ? options.y : void 0,
|
|
12
|
+
w: typeof options.w === "number" ? options.w : void 0,
|
|
13
|
+
h: typeof options.h === "number" ? options.h : void 0,
|
|
14
|
+
precision: svgPrecision
|
|
15
|
+
});
|
|
16
|
+
result.paths.forEach((path, index) => {
|
|
17
|
+
target.addShape("custGeom", {
|
|
18
|
+
...baseOptions,
|
|
19
|
+
x: options.x,
|
|
20
|
+
y: options.y,
|
|
21
|
+
w: result.width,
|
|
22
|
+
h: result.height,
|
|
23
|
+
objectName: options.objectName ? `${options.objectName} ${index + 1}` : void 0,
|
|
24
|
+
points: path.points,
|
|
25
|
+
fill: path.fill,
|
|
26
|
+
line: path.line
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
function svgToCustomGeometry(svg, options = {}) {
|
|
32
|
+
const viewBox = readViewBox(svg) ?? readSvgSize(svg) ?? [0, 0, 1, 1];
|
|
33
|
+
const [, , viewBoxWidth, viewBoxHeight] = viewBox;
|
|
34
|
+
const width = options.w ?? viewBoxWidth / 100;
|
|
35
|
+
const height = options.h ?? viewBoxHeight / 100;
|
|
36
|
+
const precision = options.precision ?? 5;
|
|
37
|
+
paper.setup(new paper.Size(viewBoxWidth || 1, viewBoxHeight || 1));
|
|
38
|
+
try {
|
|
39
|
+
const rootItem = paper.project.importSVG(svg, {
|
|
40
|
+
expandShapes: true,
|
|
41
|
+
insert: true
|
|
42
|
+
});
|
|
43
|
+
const items = rootItem.getItems({
|
|
44
|
+
match: (item) => item instanceof paper.Path || item instanceof paper.CompoundPath
|
|
45
|
+
});
|
|
46
|
+
const paths = collectUniquePathItems(items).map((pathItem) => paperPathToCustomGeometry(pathItem, viewBox, width, height, precision));
|
|
47
|
+
return {
|
|
48
|
+
width,
|
|
49
|
+
height,
|
|
50
|
+
viewBox,
|
|
51
|
+
paths
|
|
52
|
+
};
|
|
53
|
+
} finally {
|
|
54
|
+
paper.project.clear();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function collectUniquePathItems(items) {
|
|
58
|
+
const seen = /* @__PURE__ */ new Set();
|
|
59
|
+
const paths = [];
|
|
60
|
+
for (const item of items) {
|
|
61
|
+
const pathItems = paperItemToPaths(item);
|
|
62
|
+
for (const pathItem of pathItems) {
|
|
63
|
+
if (seen.has(pathItem)) continue;
|
|
64
|
+
seen.add(pathItem);
|
|
65
|
+
paths.push(pathItem);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return paths;
|
|
69
|
+
}
|
|
70
|
+
function paperItemToPaths(item) {
|
|
71
|
+
if (item instanceof paper.Path) return [item];
|
|
72
|
+
if (item instanceof paper.CompoundPath) return item.children.filter((child) => child instanceof paper.Path);
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
function paperPathToCustomGeometry(pathItem, viewBox, width, height, precision) {
|
|
76
|
+
const stroke = inheritedStroke(pathItem);
|
|
77
|
+
return {
|
|
78
|
+
points: pathToCustomGeometryPoints(pathItem, viewBox, width, height, precision),
|
|
79
|
+
fill: colorToFill(inheritedValue(pathItem, "fillColor")),
|
|
80
|
+
line: colorToLine(stroke.color, stroke.width, viewBox, width, height)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function inheritedStroke(item) {
|
|
84
|
+
let cursor = item;
|
|
85
|
+
while (cursor) {
|
|
86
|
+
if (cursor.strokeColor) {
|
|
87
|
+
return {
|
|
88
|
+
color: cursor.strokeColor,
|
|
89
|
+
width: cursor.strokeWidth
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
cursor = cursor.parent;
|
|
93
|
+
}
|
|
94
|
+
return { color: void 0, width: void 0 };
|
|
95
|
+
}
|
|
96
|
+
function inheritedValue(item, key) {
|
|
97
|
+
let cursor = item;
|
|
98
|
+
while (cursor) {
|
|
99
|
+
const value = cursor[key];
|
|
100
|
+
if (value !== void 0 && value !== null) return value;
|
|
101
|
+
cursor = cursor.parent;
|
|
102
|
+
}
|
|
103
|
+
return void 0;
|
|
104
|
+
}
|
|
105
|
+
function pathToCustomGeometryPoints(pathItem, viewBox, width, height, precision) {
|
|
106
|
+
const segments = pathItem.segments;
|
|
107
|
+
if (!segments || segments.length === 0) return [];
|
|
108
|
+
const points = [movePoint(segments[0].point, viewBox, width, height, precision)];
|
|
109
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
110
|
+
points.push(segmentPoint(segments[index - 1], segments[index], viewBox, width, height, precision));
|
|
111
|
+
}
|
|
112
|
+
if (pathItem.closed) {
|
|
113
|
+
points.push(segmentPoint(segments[segments.length - 1], segments[0], viewBox, width, height, precision));
|
|
114
|
+
points.push({ close: true });
|
|
115
|
+
}
|
|
116
|
+
return points;
|
|
117
|
+
}
|
|
118
|
+
function movePoint(point, viewBox, width, height, precision) {
|
|
119
|
+
return {
|
|
120
|
+
...scalePoint(point, viewBox, width, height, precision),
|
|
121
|
+
moveTo: true
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function segmentPoint(from, to, viewBox, width, height, precision) {
|
|
125
|
+
const end = scalePoint(to.point, viewBox, width, height, precision);
|
|
126
|
+
const fromHandle = from.handleOut;
|
|
127
|
+
const toHandle = to.handleIn;
|
|
128
|
+
if (!fromHandle.isZero() || !toHandle.isZero()) {
|
|
129
|
+
const c1 = scalePoint(from.point.add(fromHandle), viewBox, width, height, precision);
|
|
130
|
+
const c2 = scalePoint(to.point.add(toHandle), viewBox, width, height, precision);
|
|
131
|
+
return {
|
|
132
|
+
...end,
|
|
133
|
+
curve: {
|
|
134
|
+
type: "cubic",
|
|
135
|
+
x1: c1.x,
|
|
136
|
+
y1: c1.y,
|
|
137
|
+
x2: c2.x,
|
|
138
|
+
y2: c2.y
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return end;
|
|
143
|
+
}
|
|
144
|
+
function scalePoint(point, viewBox, width, height, precision) {
|
|
145
|
+
const [minX, minY, viewBoxWidth, viewBoxHeight] = viewBox;
|
|
146
|
+
return {
|
|
147
|
+
x: round((point.x - minX) / viewBoxWidth * width, precision),
|
|
148
|
+
y: round((point.y - minY) / viewBoxHeight * height, precision)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function colorToFill(color) {
|
|
152
|
+
const hex = paperColorToHex(color);
|
|
153
|
+
return hex ? { color: hex } : void 0;
|
|
154
|
+
}
|
|
155
|
+
function colorToLine(color, strokeWidth, viewBox, width, height) {
|
|
156
|
+
const hex = paperColorToHex(color);
|
|
157
|
+
if (!hex) return { type: "none" };
|
|
158
|
+
const [, , viewBoxWidth, viewBoxHeight] = viewBox;
|
|
159
|
+
const scale = (width / viewBoxWidth + height / viewBoxHeight) / 2;
|
|
160
|
+
return {
|
|
161
|
+
color: hex,
|
|
162
|
+
width: strokeWidth ? Math.max(0.1, strokeWidth * scale * 72) : 1
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function paperColorToHex(color) {
|
|
166
|
+
const css = color?.toCSS?.(true);
|
|
167
|
+
if (typeof css !== "string" || !css.startsWith("#")) return void 0;
|
|
168
|
+
return css.slice(1).toUpperCase();
|
|
169
|
+
}
|
|
170
|
+
function readViewBox(svg) {
|
|
171
|
+
const match = svg.match(/\bviewBox=["']([^"']+)["']/);
|
|
172
|
+
if (!match) return void 0;
|
|
173
|
+
const values = match[1].trim().split(/[\s,]+/).map(Number);
|
|
174
|
+
return values.length === 4 && values.every(Number.isFinite) ? values : void 0;
|
|
175
|
+
}
|
|
176
|
+
function readSvgSize(svg) {
|
|
177
|
+
const width = readNumericSvgAttr(svg, "width");
|
|
178
|
+
const height = readNumericSvgAttr(svg, "height");
|
|
179
|
+
return width && height ? [0, 0, width, height] : void 0;
|
|
180
|
+
}
|
|
181
|
+
function readNumericSvgAttr(svg, attr) {
|
|
182
|
+
const match = svg.match(new RegExp(`\\b${attr}=["']([0-9.]+)`));
|
|
183
|
+
if (!match) return void 0;
|
|
184
|
+
const value = Number(match[1]);
|
|
185
|
+
return Number.isFinite(value) ? value : void 0;
|
|
186
|
+
}
|
|
187
|
+
function round(value, precision) {
|
|
188
|
+
const scale = 10 ** precision;
|
|
189
|
+
return Math.round(value * scale) / scale;
|
|
190
|
+
}
|
|
191
|
+
export {
|
|
192
|
+
handleSvgCustomGeometry,
|
|
193
|
+
setupSvgCustomGeometry,
|
|
194
|
+
svgToCustomGeometry
|
|
195
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const node_fs = require("node:fs");
|
|
4
|
+
const promises = require("node:fs/promises");
|
|
5
|
+
const node_path = require("node:path");
|
|
6
|
+
const svgToPng = require("./svg-to-png.cjs");
|
|
7
|
+
const FIT_VALUES = /* @__PURE__ */ new Set(["cover", "contain", "fill", "inside", "outside"]);
|
|
8
|
+
async function main(args) {
|
|
9
|
+
const parsed = parseArgs(args);
|
|
10
|
+
if (!parsed.input) {
|
|
11
|
+
printUsage();
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const svg = await readSvgInput(parsed.input);
|
|
16
|
+
await svgToPng.writeSvgToPng(svg, parsed.output, parsed.options);
|
|
17
|
+
console.log(parsed.output);
|
|
18
|
+
}
|
|
19
|
+
function parseArgs(args) {
|
|
20
|
+
const result = {
|
|
21
|
+
output: "output.png",
|
|
22
|
+
options: {}
|
|
23
|
+
};
|
|
24
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
25
|
+
const arg = args[index];
|
|
26
|
+
switch (arg) {
|
|
27
|
+
case "-o":
|
|
28
|
+
case "--output":
|
|
29
|
+
result.output = readValue(args, ++index, arg);
|
|
30
|
+
break;
|
|
31
|
+
case "--width":
|
|
32
|
+
result.options.width = readNumber(args, ++index, arg);
|
|
33
|
+
break;
|
|
34
|
+
case "--height":
|
|
35
|
+
result.options.height = readNumber(args, ++index, arg);
|
|
36
|
+
break;
|
|
37
|
+
case "--density":
|
|
38
|
+
result.options.density = readNumber(args, ++index, arg);
|
|
39
|
+
break;
|
|
40
|
+
case "--fit": {
|
|
41
|
+
const fit = readValue(args, ++index, arg);
|
|
42
|
+
if (!FIT_VALUES.has(fit)) {
|
|
43
|
+
throw new Error(`Invalid --fit value "${fit}". Expected one of: ${Array.from(FIT_VALUES).join(", ")}`);
|
|
44
|
+
}
|
|
45
|
+
result.options.fit = fit;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case "--background":
|
|
49
|
+
result.options.background = readValue(args, ++index, arg);
|
|
50
|
+
break;
|
|
51
|
+
case "--compression-level":
|
|
52
|
+
result.options.compressionLevel = readNumber(args, ++index, arg);
|
|
53
|
+
break;
|
|
54
|
+
case "-h":
|
|
55
|
+
case "--help":
|
|
56
|
+
printUsage();
|
|
57
|
+
process.exit(0);
|
|
58
|
+
default:
|
|
59
|
+
if (arg.startsWith("-")) {
|
|
60
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
61
|
+
}
|
|
62
|
+
if (result.input !== void 0) {
|
|
63
|
+
throw new Error(`Unexpected extra argument: ${arg}`);
|
|
64
|
+
}
|
|
65
|
+
result.input = arg;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
async function readSvgInput(input) {
|
|
71
|
+
if (node_fs.existsSync(input)) {
|
|
72
|
+
return promises.readFile(input, "utf8");
|
|
73
|
+
}
|
|
74
|
+
return input;
|
|
75
|
+
}
|
|
76
|
+
function readValue(args, index, option) {
|
|
77
|
+
const value = args[index];
|
|
78
|
+
if (!value) {
|
|
79
|
+
throw new Error(`Missing value for ${option}`);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
function readNumber(args, index, option) {
|
|
84
|
+
const value = Number(readValue(args, index, option));
|
|
85
|
+
if (!Number.isFinite(value)) {
|
|
86
|
+
throw new Error(`Invalid numeric value for ${option}`);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
function printUsage() {
|
|
91
|
+
const command = node_path.basename(process.argv[1] ?? "svg-to-png-cli.js", node_path.extname(process.argv[1] ?? ""));
|
|
92
|
+
console.log(`Usage:
|
|
93
|
+
node ${command}.js "<svg ...>" -o output.png
|
|
94
|
+
node ${command}.js input.svg -o output.png
|
|
95
|
+
|
|
96
|
+
Options:
|
|
97
|
+
-o, --output <path> Output PNG path. Defaults to output.png.
|
|
98
|
+
--width <px> Resize output width.
|
|
99
|
+
--height <px> Resize output height.
|
|
100
|
+
--fit <mode> cover, contain, fill, inside, or outside. Defaults to contain.
|
|
101
|
+
--density <dpi> SVG render density.
|
|
102
|
+
--background <color> Flatten transparent background, e.g. "#ffffff".
|
|
103
|
+
--compression-level <0-9> PNG compression level.`);
|
|
104
|
+
}
|
|
105
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
106
|
+
console.error(error instanceof Error ? error.message : error);
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
});
|