@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/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @artifact-kit/deckkit-pro
|
|
2
|
+
|
|
3
|
+
Commercial DeckKit plugin bundle.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import DeckKit from '@artifact-kit/deckkit'
|
|
9
|
+
import deckkitPro from '@artifact-kit/deckkit-pro'
|
|
10
|
+
|
|
11
|
+
const deck = new DeckKit()
|
|
12
|
+
deck.use(deckkitPro())
|
|
13
|
+
|
|
14
|
+
const slide = deck.addSlide()
|
|
15
|
+
slide.addShape(deck.ShapeType.rect, {
|
|
16
|
+
x: 1,
|
|
17
|
+
y: 1,
|
|
18
|
+
w: 5,
|
|
19
|
+
h: 2,
|
|
20
|
+
fill: {
|
|
21
|
+
type: 'gradient',
|
|
22
|
+
angle: 90,
|
|
23
|
+
stops: [
|
|
24
|
+
{ position: 0, color: 'FF5A5F' },
|
|
25
|
+
{ position: 1, color: '2D9CDB' },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## SVG to PNG
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { renderSvgToPng, writeSvgToPng } from '@artifact-kit/deckkit-pro/svg-to-png'
|
|
35
|
+
|
|
36
|
+
const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="120" height="80">...</svg>'
|
|
37
|
+
const png = await renderSvgToPng(svg, {
|
|
38
|
+
width: 240,
|
|
39
|
+
height: 160,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
await writeSvgToPng(svg, 'output.png')
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
CLI:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
node dist/svg-to-png-cli.js '<svg xmlns="http://www.w3.org/2000/svg" width="120" height="80">...</svg>' -o output.png
|
|
49
|
+
node dist/svg-to-png-cli.js input.svg -o output.png --width 240 --height 160
|
|
50
|
+
```
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
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 image = require("./image.cjs");
|
|
7
|
+
const svgToPng = require("./svg-to-png.cjs");
|
|
8
|
+
const FIT_VALUES = /* @__PURE__ */ new Set(["cover", "contain", "fill", "inside", "outside"]);
|
|
9
|
+
const COMMANDS = {
|
|
10
|
+
"image-info": runImageInfo,
|
|
11
|
+
crop: runCrop,
|
|
12
|
+
"svg-to-png": runSvgToPng,
|
|
13
|
+
compare: runCompare,
|
|
14
|
+
overlay: runOverlay,
|
|
15
|
+
resize: runResize,
|
|
16
|
+
"sample-color": runSampleColor
|
|
17
|
+
};
|
|
18
|
+
async function main(args) {
|
|
19
|
+
const command = args[0];
|
|
20
|
+
if (!command || command === "-h" || command === "--help") {
|
|
21
|
+
printUsage();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const handler = COMMANDS[command];
|
|
25
|
+
if (!handler) {
|
|
26
|
+
throw new Error(`Unknown command: ${command}`);
|
|
27
|
+
}
|
|
28
|
+
await handler(args.slice(1));
|
|
29
|
+
}
|
|
30
|
+
async function runImageInfo(args) {
|
|
31
|
+
const input = readPositional(args, 0, "image");
|
|
32
|
+
const info = await image.getImageInfo(input);
|
|
33
|
+
console.log(JSON.stringify(info));
|
|
34
|
+
}
|
|
35
|
+
async function runCrop(args) {
|
|
36
|
+
const input = readPositional(args, 0, "image");
|
|
37
|
+
const options = parseOptions(args.slice(1));
|
|
38
|
+
const output = readRequiredOption(options, "output");
|
|
39
|
+
const buffer = await image.cropImage(input, {
|
|
40
|
+
x: readRequiredNumberOption(options, "x"),
|
|
41
|
+
y: readRequiredNumberOption(options, "y"),
|
|
42
|
+
width: readRequiredNumberOption(options, "w"),
|
|
43
|
+
height: readRequiredNumberOption(options, "h")
|
|
44
|
+
});
|
|
45
|
+
await image.writeImage(buffer, output);
|
|
46
|
+
console.log(output);
|
|
47
|
+
}
|
|
48
|
+
async function runSvgToPng(args) {
|
|
49
|
+
const input = readPositional(args, 0, "svg|string");
|
|
50
|
+
const options = parseOptions(args.slice(1));
|
|
51
|
+
const output = options.output ?? "output.png";
|
|
52
|
+
const fit = options.fit;
|
|
53
|
+
if (fit !== void 0 && !FIT_VALUES.has(fit)) {
|
|
54
|
+
throw new Error(`Invalid --fit value "${fit}". Expected one of: ${Array.from(FIT_VALUES).join(", ")}`);
|
|
55
|
+
}
|
|
56
|
+
const svgOptions = {
|
|
57
|
+
width: readOptionalNumberOption(options, "width"),
|
|
58
|
+
height: readOptionalNumberOption(options, "height"),
|
|
59
|
+
density: readOptionalNumberOption(options, "density"),
|
|
60
|
+
fit,
|
|
61
|
+
background: options.background,
|
|
62
|
+
compressionLevel: readOptionalNumberOption(options, "compression-level")
|
|
63
|
+
};
|
|
64
|
+
await svgToPng.writeSvgToPng(await readSvgInput(input), output, svgOptions);
|
|
65
|
+
console.log(output);
|
|
66
|
+
}
|
|
67
|
+
async function runCompare(args) {
|
|
68
|
+
const reference = readPositional(args, 0, "reference image");
|
|
69
|
+
const generated = readPositional(args, 1, "generated image");
|
|
70
|
+
const options = parseOptions(args.slice(2));
|
|
71
|
+
const output = readRequiredOption(options, "output");
|
|
72
|
+
const buffer = await image.compareImages(reference, generated, {
|
|
73
|
+
background: options.background,
|
|
74
|
+
gap: readOptionalNumberOption(options, "gap")
|
|
75
|
+
});
|
|
76
|
+
await image.writeImage(buffer, output);
|
|
77
|
+
console.log(output);
|
|
78
|
+
}
|
|
79
|
+
async function runOverlay(args) {
|
|
80
|
+
const reference = readPositional(args, 0, "reference image");
|
|
81
|
+
const generated = readPositional(args, 1, "generated image");
|
|
82
|
+
const options = parseOptions(args.slice(2));
|
|
83
|
+
const output = readRequiredOption(options, "output");
|
|
84
|
+
const buffer = await image.overlayImages(reference, generated, {
|
|
85
|
+
opacity: readOptionalNumberOption(options, "opacity"),
|
|
86
|
+
background: options.background
|
|
87
|
+
});
|
|
88
|
+
await image.writeImage(buffer, output);
|
|
89
|
+
console.log(output);
|
|
90
|
+
}
|
|
91
|
+
async function runResize(args) {
|
|
92
|
+
const input = readPositional(args, 0, "image");
|
|
93
|
+
const options = parseOptions(args.slice(1));
|
|
94
|
+
const output = readRequiredOption(options, "output");
|
|
95
|
+
const fit = options.fit;
|
|
96
|
+
if (fit !== void 0 && !FIT_VALUES.has(fit)) {
|
|
97
|
+
throw new Error(`Invalid --fit value "${fit}". Expected one of: ${Array.from(FIT_VALUES).join(", ")}`);
|
|
98
|
+
}
|
|
99
|
+
const resizeOptions = {
|
|
100
|
+
width: readOptionalNumberOption(options, "width"),
|
|
101
|
+
height: readOptionalNumberOption(options, "height"),
|
|
102
|
+
fit,
|
|
103
|
+
background: options.background
|
|
104
|
+
};
|
|
105
|
+
const buffer = await image.resizeImage(input, resizeOptions);
|
|
106
|
+
await image.writeImage(buffer, output);
|
|
107
|
+
console.log(output);
|
|
108
|
+
}
|
|
109
|
+
async function runSampleColor(args) {
|
|
110
|
+
const input = readPositional(args, 0, "image");
|
|
111
|
+
const options = parseOptions(args.slice(1));
|
|
112
|
+
const color = await image.sampleColor(input, readRequiredNumberOption(options, "x"), readRequiredNumberOption(options, "y"));
|
|
113
|
+
console.log(JSON.stringify(color));
|
|
114
|
+
}
|
|
115
|
+
function parseOptions(args) {
|
|
116
|
+
const options = {};
|
|
117
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
118
|
+
const arg = args[index];
|
|
119
|
+
if (!arg.startsWith("-")) {
|
|
120
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
121
|
+
}
|
|
122
|
+
const key = arg.replace(/^-+/, "");
|
|
123
|
+
options[normalizeOptionName(key)] = readValue(args, ++index, arg);
|
|
124
|
+
}
|
|
125
|
+
return options;
|
|
126
|
+
}
|
|
127
|
+
function normalizeOptionName(name) {
|
|
128
|
+
switch (name) {
|
|
129
|
+
case "o":
|
|
130
|
+
return "output";
|
|
131
|
+
default:
|
|
132
|
+
return name;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function readSvgInput(input) {
|
|
136
|
+
if (node_fs.existsSync(input)) {
|
|
137
|
+
return promises.readFile(input, "utf8");
|
|
138
|
+
}
|
|
139
|
+
return input;
|
|
140
|
+
}
|
|
141
|
+
function readPositional(args, index, label) {
|
|
142
|
+
const value = args[index];
|
|
143
|
+
if (!value || value.startsWith("-")) {
|
|
144
|
+
throw new Error(`Missing ${label}`);
|
|
145
|
+
}
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
function readRequiredOption(options, name) {
|
|
149
|
+
const value = options[name];
|
|
150
|
+
if (!value) {
|
|
151
|
+
throw new Error(`Missing --${name}`);
|
|
152
|
+
}
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
function readOptionalNumberOption(options, name) {
|
|
156
|
+
const value = options[name];
|
|
157
|
+
if (value === void 0) {
|
|
158
|
+
return void 0;
|
|
159
|
+
}
|
|
160
|
+
const number = Number(value);
|
|
161
|
+
if (!Number.isFinite(number)) {
|
|
162
|
+
throw new Error(`Invalid numeric value for --${name}`);
|
|
163
|
+
}
|
|
164
|
+
return number;
|
|
165
|
+
}
|
|
166
|
+
function readRequiredNumberOption(options, name) {
|
|
167
|
+
const value = readOptionalNumberOption(options, name);
|
|
168
|
+
if (value === void 0) {
|
|
169
|
+
throw new Error(`Missing --${name}`);
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
function readValue(args, index, option) {
|
|
174
|
+
const value = args[index];
|
|
175
|
+
if (!value) {
|
|
176
|
+
throw new Error(`Missing value for ${option}`);
|
|
177
|
+
}
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
function printUsage() {
|
|
181
|
+
const command = node_path.basename(process.argv[1] ?? "cli.js", node_path.extname(process.argv[1] ?? ""));
|
|
182
|
+
console.log(`Usage:
|
|
183
|
+
node ${command}.js image-info <image>
|
|
184
|
+
node ${command}.js crop <image> --x <px> --y <px> --w <px> --h <px> -o <png>
|
|
185
|
+
node ${command}.js svg-to-png <svg|string> -o <png>
|
|
186
|
+
node ${command}.js compare <ref> <generated> -o <png>
|
|
187
|
+
node ${command}.js overlay <ref> <generated> -o <png> --opacity <0-1>
|
|
188
|
+
node ${command}.js resize <image> --width <px> --height <px> -o <png>
|
|
189
|
+
node ${command}.js sample-color <image> --x <px> --y <px>`);
|
|
190
|
+
}
|
|
191
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
192
|
+
console.error(error instanceof Error ? error.message : error);
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
});
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { basename, extname } from "node:path";
|
|
5
|
+
import { sampleColor, resizeImage, writeImage, overlayImages, compareImages, cropImage, getImageInfo } from "./image.js";
|
|
6
|
+
import { writeSvgToPng } from "./svg-to-png.js";
|
|
7
|
+
const FIT_VALUES = /* @__PURE__ */ new Set(["cover", "contain", "fill", "inside", "outside"]);
|
|
8
|
+
const COMMANDS = {
|
|
9
|
+
"image-info": runImageInfo,
|
|
10
|
+
crop: runCrop,
|
|
11
|
+
"svg-to-png": runSvgToPng,
|
|
12
|
+
compare: runCompare,
|
|
13
|
+
overlay: runOverlay,
|
|
14
|
+
resize: runResize,
|
|
15
|
+
"sample-color": runSampleColor
|
|
16
|
+
};
|
|
17
|
+
async function main(args) {
|
|
18
|
+
const command = args[0];
|
|
19
|
+
if (!command || command === "-h" || command === "--help") {
|
|
20
|
+
printUsage();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const handler = COMMANDS[command];
|
|
24
|
+
if (!handler) {
|
|
25
|
+
throw new Error(`Unknown command: ${command}`);
|
|
26
|
+
}
|
|
27
|
+
await handler(args.slice(1));
|
|
28
|
+
}
|
|
29
|
+
async function runImageInfo(args) {
|
|
30
|
+
const input = readPositional(args, 0, "image");
|
|
31
|
+
const info = await getImageInfo(input);
|
|
32
|
+
console.log(JSON.stringify(info));
|
|
33
|
+
}
|
|
34
|
+
async function runCrop(args) {
|
|
35
|
+
const input = readPositional(args, 0, "image");
|
|
36
|
+
const options = parseOptions(args.slice(1));
|
|
37
|
+
const output = readRequiredOption(options, "output");
|
|
38
|
+
const buffer = await cropImage(input, {
|
|
39
|
+
x: readRequiredNumberOption(options, "x"),
|
|
40
|
+
y: readRequiredNumberOption(options, "y"),
|
|
41
|
+
width: readRequiredNumberOption(options, "w"),
|
|
42
|
+
height: readRequiredNumberOption(options, "h")
|
|
43
|
+
});
|
|
44
|
+
await writeImage(buffer, output);
|
|
45
|
+
console.log(output);
|
|
46
|
+
}
|
|
47
|
+
async function runSvgToPng(args) {
|
|
48
|
+
const input = readPositional(args, 0, "svg|string");
|
|
49
|
+
const options = parseOptions(args.slice(1));
|
|
50
|
+
const output = options.output ?? "output.png";
|
|
51
|
+
const fit = options.fit;
|
|
52
|
+
if (fit !== void 0 && !FIT_VALUES.has(fit)) {
|
|
53
|
+
throw new Error(`Invalid --fit value "${fit}". Expected one of: ${Array.from(FIT_VALUES).join(", ")}`);
|
|
54
|
+
}
|
|
55
|
+
const svgOptions = {
|
|
56
|
+
width: readOptionalNumberOption(options, "width"),
|
|
57
|
+
height: readOptionalNumberOption(options, "height"),
|
|
58
|
+
density: readOptionalNumberOption(options, "density"),
|
|
59
|
+
fit,
|
|
60
|
+
background: options.background,
|
|
61
|
+
compressionLevel: readOptionalNumberOption(options, "compression-level")
|
|
62
|
+
};
|
|
63
|
+
await writeSvgToPng(await readSvgInput(input), output, svgOptions);
|
|
64
|
+
console.log(output);
|
|
65
|
+
}
|
|
66
|
+
async function runCompare(args) {
|
|
67
|
+
const reference = readPositional(args, 0, "reference image");
|
|
68
|
+
const generated = readPositional(args, 1, "generated image");
|
|
69
|
+
const options = parseOptions(args.slice(2));
|
|
70
|
+
const output = readRequiredOption(options, "output");
|
|
71
|
+
const buffer = await compareImages(reference, generated, {
|
|
72
|
+
background: options.background,
|
|
73
|
+
gap: readOptionalNumberOption(options, "gap")
|
|
74
|
+
});
|
|
75
|
+
await writeImage(buffer, output);
|
|
76
|
+
console.log(output);
|
|
77
|
+
}
|
|
78
|
+
async function runOverlay(args) {
|
|
79
|
+
const reference = readPositional(args, 0, "reference image");
|
|
80
|
+
const generated = readPositional(args, 1, "generated image");
|
|
81
|
+
const options = parseOptions(args.slice(2));
|
|
82
|
+
const output = readRequiredOption(options, "output");
|
|
83
|
+
const buffer = await overlayImages(reference, generated, {
|
|
84
|
+
opacity: readOptionalNumberOption(options, "opacity"),
|
|
85
|
+
background: options.background
|
|
86
|
+
});
|
|
87
|
+
await writeImage(buffer, output);
|
|
88
|
+
console.log(output);
|
|
89
|
+
}
|
|
90
|
+
async function runResize(args) {
|
|
91
|
+
const input = readPositional(args, 0, "image");
|
|
92
|
+
const options = parseOptions(args.slice(1));
|
|
93
|
+
const output = readRequiredOption(options, "output");
|
|
94
|
+
const fit = options.fit;
|
|
95
|
+
if (fit !== void 0 && !FIT_VALUES.has(fit)) {
|
|
96
|
+
throw new Error(`Invalid --fit value "${fit}". Expected one of: ${Array.from(FIT_VALUES).join(", ")}`);
|
|
97
|
+
}
|
|
98
|
+
const resizeOptions = {
|
|
99
|
+
width: readOptionalNumberOption(options, "width"),
|
|
100
|
+
height: readOptionalNumberOption(options, "height"),
|
|
101
|
+
fit,
|
|
102
|
+
background: options.background
|
|
103
|
+
};
|
|
104
|
+
const buffer = await resizeImage(input, resizeOptions);
|
|
105
|
+
await writeImage(buffer, output);
|
|
106
|
+
console.log(output);
|
|
107
|
+
}
|
|
108
|
+
async function runSampleColor(args) {
|
|
109
|
+
const input = readPositional(args, 0, "image");
|
|
110
|
+
const options = parseOptions(args.slice(1));
|
|
111
|
+
const color = await sampleColor(input, readRequiredNumberOption(options, "x"), readRequiredNumberOption(options, "y"));
|
|
112
|
+
console.log(JSON.stringify(color));
|
|
113
|
+
}
|
|
114
|
+
function parseOptions(args) {
|
|
115
|
+
const options = {};
|
|
116
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
117
|
+
const arg = args[index];
|
|
118
|
+
if (!arg.startsWith("-")) {
|
|
119
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
120
|
+
}
|
|
121
|
+
const key = arg.replace(/^-+/, "");
|
|
122
|
+
options[normalizeOptionName(key)] = readValue(args, ++index, arg);
|
|
123
|
+
}
|
|
124
|
+
return options;
|
|
125
|
+
}
|
|
126
|
+
function normalizeOptionName(name) {
|
|
127
|
+
switch (name) {
|
|
128
|
+
case "o":
|
|
129
|
+
return "output";
|
|
130
|
+
default:
|
|
131
|
+
return name;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function readSvgInput(input) {
|
|
135
|
+
if (existsSync(input)) {
|
|
136
|
+
return readFile(input, "utf8");
|
|
137
|
+
}
|
|
138
|
+
return input;
|
|
139
|
+
}
|
|
140
|
+
function readPositional(args, index, label) {
|
|
141
|
+
const value = args[index];
|
|
142
|
+
if (!value || value.startsWith("-")) {
|
|
143
|
+
throw new Error(`Missing ${label}`);
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
function readRequiredOption(options, name) {
|
|
148
|
+
const value = options[name];
|
|
149
|
+
if (!value) {
|
|
150
|
+
throw new Error(`Missing --${name}`);
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
function readOptionalNumberOption(options, name) {
|
|
155
|
+
const value = options[name];
|
|
156
|
+
if (value === void 0) {
|
|
157
|
+
return void 0;
|
|
158
|
+
}
|
|
159
|
+
const number = Number(value);
|
|
160
|
+
if (!Number.isFinite(number)) {
|
|
161
|
+
throw new Error(`Invalid numeric value for --${name}`);
|
|
162
|
+
}
|
|
163
|
+
return number;
|
|
164
|
+
}
|
|
165
|
+
function readRequiredNumberOption(options, name) {
|
|
166
|
+
const value = readOptionalNumberOption(options, name);
|
|
167
|
+
if (value === void 0) {
|
|
168
|
+
throw new Error(`Missing --${name}`);
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
function readValue(args, index, option) {
|
|
173
|
+
const value = args[index];
|
|
174
|
+
if (!value) {
|
|
175
|
+
throw new Error(`Missing value for ${option}`);
|
|
176
|
+
}
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
function printUsage() {
|
|
180
|
+
const command = basename(process.argv[1] ?? "cli.js", extname(process.argv[1] ?? ""));
|
|
181
|
+
console.log(`Usage:
|
|
182
|
+
node ${command}.js image-info <image>
|
|
183
|
+
node ${command}.js crop <image> --x <px> --y <px> --w <px> --h <px> -o <png>
|
|
184
|
+
node ${command}.js svg-to-png <svg|string> -o <png>
|
|
185
|
+
node ${command}.js compare <ref> <generated> -o <png>
|
|
186
|
+
node ${command}.js overlay <ref> <generated> -o <png> --opacity <0-1>
|
|
187
|
+
node ${command}.js resize <image> --width <px> --height <px> -o <png>
|
|
188
|
+
node ${command}.js sample-color <image> --x <px> --y <px>`);
|
|
189
|
+
}
|
|
190
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
191
|
+
console.error(error instanceof Error ? error.message : error);
|
|
192
|
+
process.exitCode = 1;
|
|
193
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const SCHEME_COLOR_NAMES = /* @__PURE__ */ new Set(["tx1", "tx2", "bg1", "bg2", "accent1", "accent2", "accent3", "accent4", "accent5", "accent6"]);
|
|
4
|
+
const HEX_COLOR = /^[0-9a-fA-F]{6}$/;
|
|
5
|
+
function setupGradientFill(context) {
|
|
6
|
+
context.addFillRenderer(renderGradientFill);
|
|
7
|
+
}
|
|
8
|
+
function renderGradientFill(props) {
|
|
9
|
+
if (!isGradientFillProps(props)) return void 0;
|
|
10
|
+
const stops = props.stops.slice().sort((a, b) => normalizeStopPosition(a.position) - normalizeStopPosition(b.position)).map((stop) => renderGradientStop(stop)).join("");
|
|
11
|
+
const angle = normalizeAngle(props.angle ?? 90);
|
|
12
|
+
const xmlAngle = Math.round(normalizeAngle(360 - angle) * 6e4);
|
|
13
|
+
const rotWithShape = props.rotateWithShape === false ? "0" : "1";
|
|
14
|
+
return `<a:gradFill rotWithShape="${rotWithShape}"><a:gsLst>${stops}</a:gsLst><a:lin ang="${xmlAngle}" scaled="0"/></a:gradFill>`;
|
|
15
|
+
}
|
|
16
|
+
function isGradientFillProps(props) {
|
|
17
|
+
if (typeof props !== "object" || props === null) return false;
|
|
18
|
+
const fill = props;
|
|
19
|
+
return fill.type === "gradient" && Array.isArray(fill.stops) && fill.stops.length >= 2;
|
|
20
|
+
}
|
|
21
|
+
function normalizeAngle(angle) {
|
|
22
|
+
return (angle % 360 + 360) % 360;
|
|
23
|
+
}
|
|
24
|
+
function normalizeStopPosition(position) {
|
|
25
|
+
const percent = position > 1 ? position / 100 : position;
|
|
26
|
+
return Math.min(1, Math.max(0, percent));
|
|
27
|
+
}
|
|
28
|
+
function renderGradientStop(stop) {
|
|
29
|
+
const position = Math.round(normalizeStopPosition(stop.position) * 1e5);
|
|
30
|
+
return `<a:gs pos="${position}">${renderColor(stop.color, stop.transparency)}</a:gs>`;
|
|
31
|
+
}
|
|
32
|
+
function renderColor(color, transparency) {
|
|
33
|
+
const colorVal = color.replace("#", "");
|
|
34
|
+
const alpha = typeof transparency === "number" ? `<a:alpha val="${Math.round((100 - transparency) * 1e3)}"/>` : "";
|
|
35
|
+
if (HEX_COLOR.test(colorVal)) {
|
|
36
|
+
return alpha ? `<a:srgbClr val="${colorVal.toUpperCase()}">${alpha}</a:srgbClr>` : `<a:srgbClr val="${colorVal.toUpperCase()}"/>`;
|
|
37
|
+
}
|
|
38
|
+
if (SCHEME_COLOR_NAMES.has(colorVal)) {
|
|
39
|
+
return alpha ? `<a:schemeClr val="${colorVal}">${alpha}</a:schemeClr>` : `<a:schemeClr val="${colorVal}"/>`;
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Invalid gradient stop color: ${color}`);
|
|
42
|
+
}
|
|
43
|
+
exports.renderGradientFill = renderGradientFill;
|
|
44
|
+
exports.setupGradientFill = setupGradientFill;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const SCHEME_COLOR_NAMES = /* @__PURE__ */ new Set(["tx1", "tx2", "bg1", "bg2", "accent1", "accent2", "accent3", "accent4", "accent5", "accent6"]);
|
|
2
|
+
const HEX_COLOR = /^[0-9a-fA-F]{6}$/;
|
|
3
|
+
function setupGradientFill(context) {
|
|
4
|
+
context.addFillRenderer(renderGradientFill);
|
|
5
|
+
}
|
|
6
|
+
function renderGradientFill(props) {
|
|
7
|
+
if (!isGradientFillProps(props)) return void 0;
|
|
8
|
+
const stops = props.stops.slice().sort((a, b) => normalizeStopPosition(a.position) - normalizeStopPosition(b.position)).map((stop) => renderGradientStop(stop)).join("");
|
|
9
|
+
const angle = normalizeAngle(props.angle ?? 90);
|
|
10
|
+
const xmlAngle = Math.round(normalizeAngle(360 - angle) * 6e4);
|
|
11
|
+
const rotWithShape = props.rotateWithShape === false ? "0" : "1";
|
|
12
|
+
return `<a:gradFill rotWithShape="${rotWithShape}"><a:gsLst>${stops}</a:gsLst><a:lin ang="${xmlAngle}" scaled="0"/></a:gradFill>`;
|
|
13
|
+
}
|
|
14
|
+
function isGradientFillProps(props) {
|
|
15
|
+
if (typeof props !== "object" || props === null) return false;
|
|
16
|
+
const fill = props;
|
|
17
|
+
return fill.type === "gradient" && Array.isArray(fill.stops) && fill.stops.length >= 2;
|
|
18
|
+
}
|
|
19
|
+
function normalizeAngle(angle) {
|
|
20
|
+
return (angle % 360 + 360) % 360;
|
|
21
|
+
}
|
|
22
|
+
function normalizeStopPosition(position) {
|
|
23
|
+
const percent = position > 1 ? position / 100 : position;
|
|
24
|
+
return Math.min(1, Math.max(0, percent));
|
|
25
|
+
}
|
|
26
|
+
function renderGradientStop(stop) {
|
|
27
|
+
const position = Math.round(normalizeStopPosition(stop.position) * 1e5);
|
|
28
|
+
return `<a:gs pos="${position}">${renderColor(stop.color, stop.transparency)}</a:gs>`;
|
|
29
|
+
}
|
|
30
|
+
function renderColor(color, transparency) {
|
|
31
|
+
const colorVal = color.replace("#", "");
|
|
32
|
+
const alpha = typeof transparency === "number" ? `<a:alpha val="${Math.round((100 - transparency) * 1e3)}"/>` : "";
|
|
33
|
+
if (HEX_COLOR.test(colorVal)) {
|
|
34
|
+
return alpha ? `<a:srgbClr val="${colorVal.toUpperCase()}">${alpha}</a:srgbClr>` : `<a:srgbClr val="${colorVal.toUpperCase()}"/>`;
|
|
35
|
+
}
|
|
36
|
+
if (SCHEME_COLOR_NAMES.has(colorVal)) {
|
|
37
|
+
return alpha ? `<a:schemeClr val="${colorVal}">${alpha}</a:schemeClr>` : `<a:schemeClr val="${colorVal}"/>`;
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`Invalid gradient stop color: ${color}`);
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
renderGradientFill,
|
|
43
|
+
setupGradientFill
|
|
44
|
+
};
|
package/dist/image.cjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const promises = require("node:fs/promises");
|
|
4
|
+
const node_path = require("node:path");
|
|
5
|
+
const sharp = require("sharp");
|
|
6
|
+
async function getImageInfo(input) {
|
|
7
|
+
const metadata = await sharp(input).metadata();
|
|
8
|
+
return {
|
|
9
|
+
width: metadata.width,
|
|
10
|
+
height: metadata.height,
|
|
11
|
+
format: metadata.format,
|
|
12
|
+
channels: metadata.channels,
|
|
13
|
+
space: metadata.space,
|
|
14
|
+
hasAlpha: metadata.hasAlpha,
|
|
15
|
+
density: metadata.density,
|
|
16
|
+
size: metadata.size
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async function cropImage(input, options) {
|
|
20
|
+
return sharp(input).extract({
|
|
21
|
+
left: options.x,
|
|
22
|
+
top: options.y,
|
|
23
|
+
width: options.width,
|
|
24
|
+
height: options.height
|
|
25
|
+
}).png().toBuffer();
|
|
26
|
+
}
|
|
27
|
+
async function resizeImage(input, options) {
|
|
28
|
+
const image = sharp(input).resize({
|
|
29
|
+
width: options.width,
|
|
30
|
+
height: options.height,
|
|
31
|
+
fit: options.fit ?? "contain",
|
|
32
|
+
background: options.background
|
|
33
|
+
});
|
|
34
|
+
return image.png().toBuffer();
|
|
35
|
+
}
|
|
36
|
+
async function sampleColor(input, x, y) {
|
|
37
|
+
const { data, info } = await sharp(input).ensureAlpha().extract({ left: x, top: y, width: 1, height: 1 }).raw().toBuffer({ resolveWithObject: true });
|
|
38
|
+
return {
|
|
39
|
+
x,
|
|
40
|
+
y,
|
|
41
|
+
r: data[0] ?? 0,
|
|
42
|
+
g: data[1] ?? 0,
|
|
43
|
+
b: data[2] ?? 0,
|
|
44
|
+
alpha: info.channels >= 4 ? data[3] : void 0
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function compareImages(reference, generated, options = {}) {
|
|
48
|
+
const [refBuffer, genBuffer] = await Promise.all([
|
|
49
|
+
sharp(reference).png().toBuffer(),
|
|
50
|
+
sharp(generated).png().toBuffer()
|
|
51
|
+
]);
|
|
52
|
+
const [refInfo, genInfo] = await Promise.all([getImageInfo(refBuffer), getImageInfo(genBuffer)]);
|
|
53
|
+
const gap = options.gap ?? 12;
|
|
54
|
+
const width = (refInfo.width ?? 0) + (genInfo.width ?? 0) + gap;
|
|
55
|
+
const height = Math.max(refInfo.height ?? 0, genInfo.height ?? 0);
|
|
56
|
+
return sharp({
|
|
57
|
+
create: {
|
|
58
|
+
width,
|
|
59
|
+
height,
|
|
60
|
+
channels: 4,
|
|
61
|
+
background: options.background ?? { r: 255, g: 255, b: 255, alpha: 1 }
|
|
62
|
+
}
|
|
63
|
+
}).composite([
|
|
64
|
+
{ input: refBuffer, left: 0, top: 0 },
|
|
65
|
+
{ input: genBuffer, left: (refInfo.width ?? 0) + gap, top: 0 }
|
|
66
|
+
]).png().toBuffer();
|
|
67
|
+
}
|
|
68
|
+
async function overlayImages(reference, generated, options = {}) {
|
|
69
|
+
const refInfo = await getImageInfo(reference);
|
|
70
|
+
const refWidth = refInfo.width ?? 1;
|
|
71
|
+
const refHeight = refInfo.height ?? 1;
|
|
72
|
+
const generatedBuffer = await withOpacity(
|
|
73
|
+
await sharp(generated).resize({ width: refWidth, height: refHeight, fit: "contain" }).ensureAlpha().png().toBuffer(),
|
|
74
|
+
options.opacity ?? 0.5
|
|
75
|
+
);
|
|
76
|
+
return sharp(reference).resize({ width: refWidth, height: refHeight, fit: "contain", background: options.background }).composite([{ input: generatedBuffer, left: 0, top: 0 }]).png().toBuffer();
|
|
77
|
+
}
|
|
78
|
+
async function withOpacity(input, opacity) {
|
|
79
|
+
const clampedOpacity = Math.max(0, Math.min(1, opacity));
|
|
80
|
+
const { data, info } = await sharp(input).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
81
|
+
for (let index = 3; index < data.length; index += info.channels) {
|
|
82
|
+
data[index] = Math.round((data[index] ?? 0) * clampedOpacity);
|
|
83
|
+
}
|
|
84
|
+
return sharp(data, {
|
|
85
|
+
raw: {
|
|
86
|
+
width: info.width,
|
|
87
|
+
height: info.height,
|
|
88
|
+
channels: info.channels
|
|
89
|
+
}
|
|
90
|
+
}).png().toBuffer();
|
|
91
|
+
}
|
|
92
|
+
async function writeImage(buffer, output) {
|
|
93
|
+
await promises.mkdir(node_path.dirname(output), { recursive: true });
|
|
94
|
+
await sharp(buffer).toFile(output);
|
|
95
|
+
}
|
|
96
|
+
exports.compareImages = compareImages;
|
|
97
|
+
exports.cropImage = cropImage;
|
|
98
|
+
exports.getImageInfo = getImageInfo;
|
|
99
|
+
exports.overlayImages = overlayImages;
|
|
100
|
+
exports.resizeImage = resizeImage;
|
|
101
|
+
exports.sampleColor = sampleColor;
|
|
102
|
+
exports.writeImage = writeImage;
|