@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.
@@ -0,0 +1,5 @@
1
+ # DeckKit Pro Commercial License
2
+
3
+ DeckKit Pro is proprietary commercial software. Use, copying, modification,
4
+ distribution, and sublicensing require a separate written commercial agreement
5
+ with Artifact Kit.
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;