divoom-timesgate-sdk 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/LICENSE +21 -0
- package/README.md +371 -0
- package/dist/common-D8oHDNi6.d.cts +54 -0
- package/dist/common-D8oHDNi6.d.ts +54 -0
- package/dist/image/index.cjs +515 -0
- package/dist/image/index.cjs.map +1 -0
- package/dist/image/index.d.cts +279 -0
- package/dist/image/index.d.ts +279 -0
- package/dist/image/index.js +498 -0
- package/dist/image/index.js.map +1 -0
- package/dist/index.cjs +1021 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +921 -0
- package/dist/index.d.ts +921 -0
- package/dist/index.js +991 -0
- package/dist/index.js.map +1 -0
- package/package.json +87 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var sharp4 = require('sharp');
|
|
4
|
+
|
|
5
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
6
|
+
|
|
7
|
+
var sharp4__default = /*#__PURE__*/_interopDefault(sharp4);
|
|
8
|
+
|
|
9
|
+
// src/image/load.ts
|
|
10
|
+
|
|
11
|
+
// src/errors.ts
|
|
12
|
+
var DivoomError = class extends Error {
|
|
13
|
+
constructor(message, options) {
|
|
14
|
+
super(message, options);
|
|
15
|
+
this.name = "DivoomError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var DivoomValidationError = class extends DivoomError {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "DivoomValidationError";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var DivoomConnectionError = class extends DivoomError {
|
|
25
|
+
constructor(message, options) {
|
|
26
|
+
super(message, options);
|
|
27
|
+
this.name = "DivoomConnectionError";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var DivoomTimeoutError = class extends DivoomError {
|
|
31
|
+
/** The command that timed out. */
|
|
32
|
+
command;
|
|
33
|
+
/** The timeout budget, in milliseconds, that was exceeded. */
|
|
34
|
+
timeoutMs;
|
|
35
|
+
constructor(command, timeoutMs) {
|
|
36
|
+
super(`Command "${command}" timed out after ${timeoutMs}ms.`);
|
|
37
|
+
this.name = "DivoomTimeoutError";
|
|
38
|
+
this.command = command;
|
|
39
|
+
this.timeoutMs = timeoutMs;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/constants.ts
|
|
44
|
+
var PANEL_COUNT = 5;
|
|
45
|
+
var PANEL_SIZE = 128;
|
|
46
|
+
|
|
47
|
+
// src/utils.ts
|
|
48
|
+
async function drainBody(response) {
|
|
49
|
+
try {
|
|
50
|
+
await response.body?.cancel?.();
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function assertIntInRange(value, min, max, label) {
|
|
55
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
56
|
+
throw new DivoomValidationError(
|
|
57
|
+
`${label} must be an integer between ${min} and ${max} (received ${String(value)}).`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function assertPanelIndex(value) {
|
|
62
|
+
if (!Number.isInteger(value) || value < 0 || value >= PANEL_COUNT) {
|
|
63
|
+
throw new DivoomValidationError(
|
|
64
|
+
`panel must be an integer between 0 and ${PANEL_COUNT - 1} (received ${String(value)}).`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function assertNonEmptyString(value, label) {
|
|
69
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
70
|
+
throw new DivoomValidationError(`${label} must be a non-empty string.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/image/load.ts
|
|
75
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
|
|
76
|
+
var DEFAULT_MAX_IMAGE_BYTES = 64 * 1024 * 1024;
|
|
77
|
+
var MAX_REDIRECTS = 5;
|
|
78
|
+
function isSharp(value) {
|
|
79
|
+
return typeof value === "object" && value !== null && typeof value.toBuffer === "function" && typeof value.resize === "function";
|
|
80
|
+
}
|
|
81
|
+
async function readCapped(response, maxBytes, url) {
|
|
82
|
+
const body = response.body;
|
|
83
|
+
if (!body) {
|
|
84
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
85
|
+
if (buffer.byteLength > maxBytes) {
|
|
86
|
+
throw new DivoomValidationError(`Image at ${url} exceeds the ${maxBytes}-byte limit.`);
|
|
87
|
+
}
|
|
88
|
+
return buffer;
|
|
89
|
+
}
|
|
90
|
+
const reader = body.getReader();
|
|
91
|
+
const chunks = [];
|
|
92
|
+
let total = 0;
|
|
93
|
+
for (; ; ) {
|
|
94
|
+
const { done, value } = await reader.read();
|
|
95
|
+
if (done) break;
|
|
96
|
+
if (value) {
|
|
97
|
+
total += value.byteLength;
|
|
98
|
+
if (total > maxBytes) {
|
|
99
|
+
await reader.cancel();
|
|
100
|
+
throw new DivoomValidationError(`Image at ${url} exceeds the ${maxBytes}-byte limit.`);
|
|
101
|
+
}
|
|
102
|
+
chunks.push(value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return Buffer.concat(chunks);
|
|
106
|
+
}
|
|
107
|
+
async function fetchImageBytes(url, options) {
|
|
108
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
109
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_IMAGE_BYTES;
|
|
110
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
113
|
+
try {
|
|
114
|
+
let currentUrl = url;
|
|
115
|
+
for (let hop = 0; ; hop += 1) {
|
|
116
|
+
const response = await fetchImpl(currentUrl, {
|
|
117
|
+
signal: controller.signal,
|
|
118
|
+
redirect: "manual"
|
|
119
|
+
});
|
|
120
|
+
if (response.status >= 300 && response.status < 400) {
|
|
121
|
+
const location = response.headers.get("location");
|
|
122
|
+
await drainBody(response);
|
|
123
|
+
if (!location) {
|
|
124
|
+
throw new DivoomConnectionError(
|
|
125
|
+
`Image redirect from ${currentUrl} is missing a Location header.`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (hop >= MAX_REDIRECTS) {
|
|
129
|
+
throw new DivoomConnectionError(`Too many redirects downloading image from ${url}.`);
|
|
130
|
+
}
|
|
131
|
+
const next = new URL(location, currentUrl).toString();
|
|
132
|
+
if (!/^https?:\/\//i.test(next)) {
|
|
133
|
+
throw new DivoomValidationError(
|
|
134
|
+
`Refusing to follow a non-http(s) image redirect to "${next}".`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
currentUrl = next;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
await drainBody(response);
|
|
142
|
+
throw new DivoomConnectionError(
|
|
143
|
+
`Failed to download image from ${currentUrl} (HTTP ${response.status}).`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const declared = Number(response.headers.get("content-length"));
|
|
147
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
148
|
+
await drainBody(response);
|
|
149
|
+
throw new DivoomValidationError(
|
|
150
|
+
`Image at ${currentUrl} is ${declared} bytes, exceeding the ${maxBytes}-byte limit.`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return await readCapped(response, maxBytes, currentUrl);
|
|
154
|
+
}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error instanceof DivoomError) throw error;
|
|
157
|
+
if (controller.signal.aborted) {
|
|
158
|
+
throw new DivoomTimeoutError(`GET ${url}`, timeoutMs);
|
|
159
|
+
}
|
|
160
|
+
throw new DivoomConnectionError(`Failed to download image from ${url}.`, { cause: error });
|
|
161
|
+
} finally {
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function loadImage(source, options = {}) {
|
|
166
|
+
if (isSharp(source)) {
|
|
167
|
+
return source.clone();
|
|
168
|
+
}
|
|
169
|
+
if (typeof source === "string") {
|
|
170
|
+
if (/^https?:\/\//i.test(source)) {
|
|
171
|
+
return sharp4__default.default(await fetchImageBytes(source, options));
|
|
172
|
+
}
|
|
173
|
+
return sharp4__default.default(source);
|
|
174
|
+
}
|
|
175
|
+
if (Buffer.isBuffer(source) || source instanceof Uint8Array) {
|
|
176
|
+
return sharp4__default.default(source);
|
|
177
|
+
}
|
|
178
|
+
throw new DivoomValidationError(
|
|
179
|
+
"Unsupported image source. Provide a file path, an http(s) URL, a Buffer/Uint8Array, or a sharp instance."
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/image/color.ts
|
|
184
|
+
function hexToRgb(hex) {
|
|
185
|
+
const cleaned = hex.replace(/^#/, "").trim();
|
|
186
|
+
const full = cleaned.length === 3 ? cleaned.split("").map((c) => c + c).join("") : cleaned;
|
|
187
|
+
if (!/^[0-9a-fA-F]{6}$/.test(full)) {
|
|
188
|
+
throw new DivoomValidationError(`Invalid hex color: "${hex}".`);
|
|
189
|
+
}
|
|
190
|
+
const value = Number.parseInt(full, 16);
|
|
191
|
+
return [value >> 16 & 255, value >> 8 & 255, value & 255];
|
|
192
|
+
}
|
|
193
|
+
function rgbToHex([r, g, b]) {
|
|
194
|
+
const channel = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
|
|
195
|
+
return `#${channel(r)}${channel(g)}${channel(b)}`.toUpperCase();
|
|
196
|
+
}
|
|
197
|
+
function mixRgb(a, b, t) {
|
|
198
|
+
const clamped = Math.max(0, Math.min(1, t));
|
|
199
|
+
return [
|
|
200
|
+
a[0] + (b[0] - a[0]) * clamped,
|
|
201
|
+
a[1] + (b[1] - a[1]) * clamped,
|
|
202
|
+
a[2] + (b[2] - a[2]) * clamped
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
function scaleRgb([r, g, b], factor) {
|
|
206
|
+
return [r * factor, g * factor, b * factor];
|
|
207
|
+
}
|
|
208
|
+
async function getAccentColor(source, fallback = "#1DB954") {
|
|
209
|
+
const { data, info } = await (await loadImage(source)).resize(32, 32, { fit: "cover" }).flatten({ background: "#000000" }).removeAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
210
|
+
const channels = info.channels;
|
|
211
|
+
let best = null;
|
|
212
|
+
let bestSaturation = 0;
|
|
213
|
+
for (let i = 0; i + 2 < data.length; i += channels) {
|
|
214
|
+
const r = data[i] ?? 0;
|
|
215
|
+
const g = data[i + 1] ?? 0;
|
|
216
|
+
const b = data[i + 2] ?? 0;
|
|
217
|
+
const max = Math.max(r, g, b);
|
|
218
|
+
const min = Math.min(r, g, b);
|
|
219
|
+
if (max === 0) continue;
|
|
220
|
+
const saturation = (max - min) / max;
|
|
221
|
+
if (saturation > bestSaturation && max > 80) {
|
|
222
|
+
bestSaturation = saturation;
|
|
223
|
+
best = [r, g, b];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return best ? rgbToHex(best) : fallback;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/image/encode.ts
|
|
230
|
+
async function finalizeFrame(pipeline, options) {
|
|
231
|
+
const format = options.format ?? "jpeg";
|
|
232
|
+
let buffer;
|
|
233
|
+
if (format === "png") {
|
|
234
|
+
buffer = await pipeline.png({ compressionLevel: 9 }).toBuffer();
|
|
235
|
+
} else {
|
|
236
|
+
buffer = await pipeline.flatten({ background: options.background ?? "#000000" }).jpeg({
|
|
237
|
+
quality: options.quality ?? 95,
|
|
238
|
+
chromaSubsampling: options.chromaSubsampling ?? "4:4:4"
|
|
239
|
+
}).toBuffer();
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
data: buffer.toString("base64"),
|
|
243
|
+
buffer,
|
|
244
|
+
format,
|
|
245
|
+
width: options.size,
|
|
246
|
+
height: options.size
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
async function encodePanel(source, options = {}) {
|
|
250
|
+
const size = options.size ?? PANEL_SIZE;
|
|
251
|
+
assertIntInRange(size, 1, 4096, "size");
|
|
252
|
+
const pipeline = (await loadImage(source)).resize(size, size, {
|
|
253
|
+
fit: options.fit ?? "cover",
|
|
254
|
+
kernel: options.kernel ?? "lanczos3",
|
|
255
|
+
background: options.background ?? "#000000"
|
|
256
|
+
});
|
|
257
|
+
return finalizeFrame(pipeline, { ...options, size });
|
|
258
|
+
}
|
|
259
|
+
async function solidFrame(color, options = {}) {
|
|
260
|
+
const size = options.size ?? PANEL_SIZE;
|
|
261
|
+
assertIntInRange(size, 1, 4096, "size");
|
|
262
|
+
const [r, g, b] = hexToRgb(color);
|
|
263
|
+
const pipeline = sharp4__default.default({
|
|
264
|
+
create: { width: size, height: size, channels: 3, background: { r, g, b } }
|
|
265
|
+
});
|
|
266
|
+
return finalizeFrame(pipeline, { ...options, size });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/image/art.ts
|
|
270
|
+
async function prepareAlbumArt(source, options = {}) {
|
|
271
|
+
const size = options.size ?? PANEL_SIZE;
|
|
272
|
+
const style = options.style ?? "smooth";
|
|
273
|
+
const base = await loadImage(source);
|
|
274
|
+
const pipeline = style === "pixel" ? base.resize(32, 32, { kernel: "nearest", fit: "cover" }).resize(size, size, { kernel: "nearest" }).modulate({ saturation: 1.5 }) : base.resize(size, size, { kernel: "lanczos3", fit: "cover" }).modulate({ saturation: 1.15 }).linear(1.05, -6.4).sharpen({ sigma: 1 });
|
|
275
|
+
return finalizeFrame(pipeline, { ...options, size });
|
|
276
|
+
}
|
|
277
|
+
async function splitImageAcrossPanels(source, options = {}) {
|
|
278
|
+
const panels = options.panels ?? [0, 1, 2, 3, 4];
|
|
279
|
+
const size = options.size ?? PANEL_SIZE;
|
|
280
|
+
assertIntInRange(size, 1, 4096, "size");
|
|
281
|
+
const layout = options.layout ?? "horizontal";
|
|
282
|
+
if (panels.length > PANEL_COUNT) {
|
|
283
|
+
throw new DivoomValidationError(
|
|
284
|
+
`splitImageAcrossPanels supports at most ${PANEL_COUNT} panels (received ${panels.length}).`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
panels.forEach(assertPanelIndex);
|
|
288
|
+
const count = panels.length;
|
|
289
|
+
if (count === 0) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
const base = await loadImage(source);
|
|
293
|
+
const resized = layout === "horizontal" ? base.resize(size * count, size, { fit: "cover", kernel: "lanczos3" }) : base.resize(size, size * count, { fit: "cover", kernel: "lanczos3" });
|
|
294
|
+
const canvas = await resized.png().toBuffer();
|
|
295
|
+
const results = [];
|
|
296
|
+
for (let i = 0; i < count; i += 1) {
|
|
297
|
+
const panel = panels[i];
|
|
298
|
+
if (panel === void 0) continue;
|
|
299
|
+
const tile = sharp4__default.default(canvas).extract({
|
|
300
|
+
left: layout === "horizontal" ? i * size : 0,
|
|
301
|
+
top: layout === "vertical" ? i * size : 0,
|
|
302
|
+
width: size,
|
|
303
|
+
height: size
|
|
304
|
+
});
|
|
305
|
+
results.push({ panel, frame: await finalizeFrame(tile, { ...options, size }) });
|
|
306
|
+
}
|
|
307
|
+
return results;
|
|
308
|
+
}
|
|
309
|
+
var MAX_PANEL_SIZE = 4096;
|
|
310
|
+
function escapeMarkup(text) {
|
|
311
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
312
|
+
}
|
|
313
|
+
function normalizeHex(color) {
|
|
314
|
+
return rgbToHex(hexToRgb(color));
|
|
315
|
+
}
|
|
316
|
+
async function renderText(opts) {
|
|
317
|
+
const markup = `<span foreground="${normalizeHex(opts.color)}"${opts.bold ? ' weight="bold"' : ""}>${escapeMarkup(opts.text)}</span>`;
|
|
318
|
+
const textInput = {
|
|
319
|
+
text: markup,
|
|
320
|
+
font: `${opts.family} ${opts.size}`,
|
|
321
|
+
rgba: true,
|
|
322
|
+
align: opts.align ?? "left",
|
|
323
|
+
width: Math.max(1, Math.round(opts.width)),
|
|
324
|
+
wrap: "word"
|
|
325
|
+
};
|
|
326
|
+
if (opts.maxHeight !== void 0) {
|
|
327
|
+
textInput.height = Math.max(1, Math.round(opts.maxHeight));
|
|
328
|
+
}
|
|
329
|
+
const buffer = await sharp4__default.default({ text: textInput }).png().toBuffer();
|
|
330
|
+
const meta = await sharp4__default.default(buffer).metadata();
|
|
331
|
+
return { buffer, width: meta.width ?? 0, height: meta.height ?? 0 };
|
|
332
|
+
}
|
|
333
|
+
async function fitOverlay(buffer, top, left, size) {
|
|
334
|
+
const safeTop = Math.max(0, Math.min(Math.round(top), size - 1));
|
|
335
|
+
const safeLeft = Math.max(0, Math.min(Math.round(left), size - 1));
|
|
336
|
+
const maxW = size - safeLeft;
|
|
337
|
+
const maxH = size - safeTop;
|
|
338
|
+
const meta = await sharp4__default.default(buffer).metadata();
|
|
339
|
+
if ((meta.width ?? 0) <= maxW && (meta.height ?? 0) <= maxH) {
|
|
340
|
+
return { input: buffer, top: safeTop, left: safeLeft };
|
|
341
|
+
}
|
|
342
|
+
const clamped = await sharp4__default.default(buffer).resize(maxW, maxH, { fit: "inside", withoutEnlargement: true }).png().toBuffer();
|
|
343
|
+
return { input: clamped, top: safeTop, left: safeLeft };
|
|
344
|
+
}
|
|
345
|
+
async function renderFittedTitle(text, family, width, budget, size) {
|
|
346
|
+
const candidates = [0.25, 0.21, 0.18, 0.16, 0.14].map((f) => Math.max(10, Math.round(size * f)));
|
|
347
|
+
for (const fontSize2 of candidates) {
|
|
348
|
+
const rendered = await renderText({
|
|
349
|
+
text,
|
|
350
|
+
family,
|
|
351
|
+
size: fontSize2,
|
|
352
|
+
color: "#FFFFFF",
|
|
353
|
+
bold: true,
|
|
354
|
+
width
|
|
355
|
+
});
|
|
356
|
+
if (rendered.height <= budget && rendered.width <= width) {
|
|
357
|
+
return rendered;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const fontSize = Math.max(10, Math.round(size * 0.14));
|
|
361
|
+
return renderText({
|
|
362
|
+
text,
|
|
363
|
+
family,
|
|
364
|
+
size: fontSize,
|
|
365
|
+
color: "#FFFFFF",
|
|
366
|
+
bold: true,
|
|
367
|
+
width,
|
|
368
|
+
maxHeight: Math.max(fontSize, budget)
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
function buildBackground(background, accent, size) {
|
|
372
|
+
if (background === "solid") {
|
|
373
|
+
const [r, g, b] = accent;
|
|
374
|
+
return sharp4__default.default({ create: { width: size, height: size, channels: 3, background: { r, g, b } } });
|
|
375
|
+
}
|
|
376
|
+
if (background !== "gradient") {
|
|
377
|
+
const [r, g, b] = hexToRgb(background);
|
|
378
|
+
return sharp4__default.default({ create: { width: size, height: size, channels: 3, background: { r, g, b } } });
|
|
379
|
+
}
|
|
380
|
+
const [tr, tg, tb] = scaleRgb(accent, 0.3).map(Math.round);
|
|
381
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}">
|
|
382
|
+
<defs><linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
|
383
|
+
<stop offset="0" stop-color="rgb(${tr},${tg},${tb})"/>
|
|
384
|
+
<stop offset="1" stop-color="rgb(8,8,10)"/>
|
|
385
|
+
</linearGradient></defs>
|
|
386
|
+
<rect width="${size}" height="${size}" fill="url(#bg)"/>
|
|
387
|
+
</svg>`;
|
|
388
|
+
return sharp4__default.default(Buffer.from(svg)).resize(size, size);
|
|
389
|
+
}
|
|
390
|
+
function buildShapes(opts) {
|
|
391
|
+
const { size, margin, accent, eyebrowY, hasEyebrow, progress, barY } = opts;
|
|
392
|
+
const [r, g, b] = accent;
|
|
393
|
+
const accentCss = `rgb(${r},${g},${b})`;
|
|
394
|
+
const parts = [];
|
|
395
|
+
if (hasEyebrow) {
|
|
396
|
+
const t = Math.round(size * 0.045);
|
|
397
|
+
const y = eyebrowY;
|
|
398
|
+
parts.push(
|
|
399
|
+
`<polygon points="${margin},${y} ${margin},${y + t} ${margin + t},${y + t / 2}" fill="${accentCss}"/>`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
if (progress !== void 0) {
|
|
403
|
+
const x0 = margin;
|
|
404
|
+
const x1 = size - margin;
|
|
405
|
+
const p = Math.max(0, Math.min(1, progress));
|
|
406
|
+
const fx = x0 + (x1 - x0) * p;
|
|
407
|
+
const [tr, tg, tb] = scaleRgb(accent, 0.45).map(Math.round);
|
|
408
|
+
parts.push(
|
|
409
|
+
`<line x1="${x0}" y1="${barY}" x2="${x1}" y2="${barY}" stroke="rgb(${tr},${tg},${tb})" stroke-width="3" stroke-linecap="round"/>`,
|
|
410
|
+
`<line x1="${x0}" y1="${barY}" x2="${fx}" y2="${barY}" stroke="${accentCss}" stroke-width="3" stroke-linecap="round"/>`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
return Buffer.from(
|
|
414
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}">${parts.join("")}</svg>`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
async function renderTextPanel(options) {
|
|
418
|
+
const size = options.size ?? PANEL_SIZE;
|
|
419
|
+
assertIntInRange(size, 1, MAX_PANEL_SIZE, "size");
|
|
420
|
+
assertNonEmptyString(options.title, "title");
|
|
421
|
+
const family = options.font ?? "DejaVu Sans";
|
|
422
|
+
const accentHex = options.accent ?? "#1DB954";
|
|
423
|
+
const accent = hexToRgb(accentHex);
|
|
424
|
+
const margin = Math.round(size * 0.055);
|
|
425
|
+
const maxWidth = Math.max(1, size - margin * 2);
|
|
426
|
+
const eyebrowText = options.eyebrow?.trim();
|
|
427
|
+
const subtitleText = options.subtitle?.trim();
|
|
428
|
+
const overlays = [];
|
|
429
|
+
let contentTop = margin;
|
|
430
|
+
const eyebrowY = margin + Math.round(size * 0.015);
|
|
431
|
+
if (eyebrowText) {
|
|
432
|
+
const glyph = Math.round(size * 0.045);
|
|
433
|
+
const eyebrowMaxHeight = Math.max(glyph, Math.round(size * 0.12));
|
|
434
|
+
const eyebrow = await renderText({
|
|
435
|
+
text: eyebrowText.toUpperCase(),
|
|
436
|
+
family,
|
|
437
|
+
size: Math.max(7, Math.round(size * 0.07)),
|
|
438
|
+
color: accentHex,
|
|
439
|
+
bold: true,
|
|
440
|
+
width: Math.max(1, maxWidth - glyph)
|
|
441
|
+
});
|
|
442
|
+
overlays.push({
|
|
443
|
+
buffer: eyebrow.buffer,
|
|
444
|
+
top: eyebrowY,
|
|
445
|
+
left: margin + glyph + Math.round(size * 0.03)
|
|
446
|
+
});
|
|
447
|
+
contentTop = eyebrowY + Math.min(Math.max(eyebrow.height, glyph), eyebrowMaxHeight) + Math.round(size * 0.03);
|
|
448
|
+
}
|
|
449
|
+
contentTop = Math.min(contentTop, size - Math.round(size * 0.2));
|
|
450
|
+
const hasProgress = typeof options.progress === "number";
|
|
451
|
+
const barY = size - Math.round(size * 0.08);
|
|
452
|
+
const bottomLimit = hasProgress ? barY - Math.round(size * 0.06) : size - margin;
|
|
453
|
+
let subtitleTop = bottomLimit;
|
|
454
|
+
if (subtitleText) {
|
|
455
|
+
const subSize = Math.max(9, Math.round(size * 0.11));
|
|
456
|
+
const maxSubHeight = subSize * 2 + 4;
|
|
457
|
+
let subtitle = await renderText({
|
|
458
|
+
text: subtitleText,
|
|
459
|
+
family,
|
|
460
|
+
size: subSize,
|
|
461
|
+
color: accentHex,
|
|
462
|
+
width: maxWidth
|
|
463
|
+
});
|
|
464
|
+
if (subtitle.height > maxSubHeight) {
|
|
465
|
+
const cropped = await sharp4__default.default(subtitle.buffer).extract({ left: 0, top: 0, width: subtitle.width, height: maxSubHeight }).png().toBuffer();
|
|
466
|
+
subtitle = { buffer: cropped, width: subtitle.width, height: maxSubHeight };
|
|
467
|
+
}
|
|
468
|
+
subtitleTop = Math.max(contentTop, bottomLimit - subtitle.height);
|
|
469
|
+
overlays.push({ buffer: subtitle.buffer, top: subtitleTop, left: margin });
|
|
470
|
+
}
|
|
471
|
+
const titleBottom = subtitleText ? subtitleTop : bottomLimit;
|
|
472
|
+
const titleBudget = Math.max(
|
|
473
|
+
Math.round(size * 0.12),
|
|
474
|
+
titleBottom - contentTop - Math.round(size * 0.03)
|
|
475
|
+
);
|
|
476
|
+
const title = await renderFittedTitle(options.title, family, maxWidth, titleBudget, size);
|
|
477
|
+
overlays.push({ buffer: title.buffer, top: contentTop, left: margin });
|
|
478
|
+
const composites = [
|
|
479
|
+
{
|
|
480
|
+
input: buildShapes({
|
|
481
|
+
size,
|
|
482
|
+
margin,
|
|
483
|
+
accent,
|
|
484
|
+
eyebrowY,
|
|
485
|
+
hasEyebrow: Boolean(eyebrowText),
|
|
486
|
+
progress: options.progress,
|
|
487
|
+
barY
|
|
488
|
+
}),
|
|
489
|
+
top: 0,
|
|
490
|
+
left: 0
|
|
491
|
+
}
|
|
492
|
+
];
|
|
493
|
+
for (const overlay of overlays) {
|
|
494
|
+
composites.push(await fitOverlay(overlay.buffer, overlay.top, overlay.left, size));
|
|
495
|
+
}
|
|
496
|
+
const pipeline = buildBackground(options.background ?? "gradient", accent, size).composite(
|
|
497
|
+
composites
|
|
498
|
+
);
|
|
499
|
+
return finalizeFrame(pipeline, { ...options, size });
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
exports.encodePanel = encodePanel;
|
|
503
|
+
exports.finalizeFrame = finalizeFrame;
|
|
504
|
+
exports.getAccentColor = getAccentColor;
|
|
505
|
+
exports.hexToRgb = hexToRgb;
|
|
506
|
+
exports.loadImage = loadImage;
|
|
507
|
+
exports.mixRgb = mixRgb;
|
|
508
|
+
exports.prepareAlbumArt = prepareAlbumArt;
|
|
509
|
+
exports.renderTextPanel = renderTextPanel;
|
|
510
|
+
exports.rgbToHex = rgbToHex;
|
|
511
|
+
exports.scaleRgb = scaleRgb;
|
|
512
|
+
exports.solidFrame = solidFrame;
|
|
513
|
+
exports.splitImageAcrossPanels = splitImageAcrossPanels;
|
|
514
|
+
//# sourceMappingURL=index.cjs.map
|
|
515
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/errors.ts","../../src/constants.ts","../../src/utils.ts","../../src/image/load.ts","../../src/image/color.ts","../../src/image/encode.ts","../../src/image/art.ts","../../src/image/split.ts","../../src/image/text.ts"],"names":["sharp","fontSize"],"mappings":";;;;;;;;;;;AAYO,IAAM,WAAA,GAAN,cAA0B,KAAA,CAAM;AAAA,EACrC,WAAA,CAAY,SAAiB,OAAA,EAA+B;AAC1D,IAAA,KAAA,CAAM,SAAS,OAAmC,CAAA;AAClD,IAAA,IAAA,CAAK,IAAA,GAAO,aAAA;AAAA,EACd;AACF,CAAA;AAGO,IAAM,qBAAA,GAAN,cAAoC,WAAA,CAAY;AAAA,EACrD,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,uBAAA;AAAA,EACd;AACF,CAAA;AAGO,IAAM,qBAAA,GAAN,cAAoC,WAAA,CAAY;AAAA,EACrD,WAAA,CAAY,SAAiB,OAAA,EAA+B;AAC1D,IAAA,KAAA,CAAM,SAAS,OAAO,CAAA;AACtB,IAAA,IAAA,CAAK,IAAA,GAAO,uBAAA;AAAA,EACd;AACF,CAAA;AAGO,IAAM,kBAAA,GAAN,cAAiC,WAAA,CAAY;AAAA;AAAA,EAEzC,OAAA;AAAA;AAAA,EAEA,SAAA;AAAA,EAET,WAAA,CAAY,SAAiB,SAAA,EAAmB;AAC9C,IAAA,KAAA,CAAM,CAAA,SAAA,EAAY,OAAO,CAAA,kBAAA,EAAqB,SAAS,CAAA,GAAA,CAAK,CAAA;AAC5D,IAAA,IAAA,CAAK,IAAA,GAAO,oBAAA;AACZ,IAAA,IAAA,CAAK,OAAA,GAAU,OAAA;AACf,IAAA,IAAA,CAAK,SAAA,GAAY,SAAA;AAAA,EACnB;AACF,CAAA;;;ACzCO,IAAM,WAAA,GAAc,CAAA;AAGpB,IAAM,UAAA,GAAa,GAAA;;;ACU1B,eAAsB,UAAU,QAAA,EAEd;AAChB,EAAA,IAAI;AACF,IAAA,MAAM,QAAA,CAAS,MAAM,MAAA,IAAS;AAAA,EAChC,CAAA,CAAA,MAAQ;AAAA,EAER;AACF;AAMO,SAAS,gBAAA,CAAiB,KAAA,EAAe,GAAA,EAAa,GAAA,EAAa,KAAA,EAAqB;AAC7F,EAAA,IAAI,CAAC,OAAO,SAAA,CAAU,KAAK,KAAK,KAAA,GAAQ,GAAA,IAAO,QAAQ,GAAA,EAAK;AAC1D,IAAA,MAAM,IAAI,qBAAA;AAAA,MACR,CAAA,EAAG,KAAK,CAAA,4BAAA,EAA+B,GAAG,QAAQ,GAAG,CAAA,WAAA,EAAc,MAAA,CAAO,KAAK,CAAC,CAAA,EAAA;AAAA,KAClF;AAAA,EACF;AACF;AAGO,SAAS,iBAAiB,KAAA,EAA4C;AAC3E,EAAA,IAAI,CAAC,OAAO,SAAA,CAAU,KAAK,KAAK,KAAA,GAAQ,CAAA,IAAK,SAAS,WAAA,EAAa;AACjE,IAAA,MAAM,IAAI,qBAAA;AAAA,MACR,0CAA0C,WAAA,GAAc,CAAC,CAAA,WAAA,EAAc,MAAA,CAAO,KAAK,CAAC,CAAA,EAAA;AAAA,KACtF;AAAA,EACF;AACF;AAGO,SAAS,oBAAA,CAAqB,OAAgB,KAAA,EAAwC;AAC3F,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,MAAM,IAAA,EAAK,CAAE,WAAW,CAAA,EAAG;AAC1D,IAAA,MAAM,IAAI,qBAAA,CAAsB,CAAA,EAAG,KAAK,CAAA,4BAAA,CAA8B,CAAA;AAAA,EACxE;AACF;;;AC7BA,IAAM,wBAAA,GAA2B,IAAA;AAGjC,IAAM,uBAAA,GAA0B,KAAK,IAAA,GAAO,IAAA;AAG5C,IAAM,aAAA,GAAgB,CAAA;AAatB,SAAS,QAAQ,KAAA,EAAgC;AAC/C,EAAA,OACE,OAAO,KAAA,KAAU,QAAA,IACjB,KAAA,KAAU,IAAA,IACV,OAAQ,KAAA,CAAgB,QAAA,KAAa,UAAA,IACrC,OAAQ,KAAA,CAAgB,MAAA,KAAW,UAAA;AAEvC;AAGA,eAAe,UAAA,CACb,QAAA,EACA,QAAA,EACA,GAAA,EACiB;AACjB,EAAA,MAAM,OAAO,QAAA,CAAS,IAAA;AACtB,EAAA,IAAI,CAAC,IAAA,EAAM;AACT,IAAA,MAAM,SAAS,MAAA,CAAO,IAAA,CAAK,MAAM,QAAA,CAAS,aAAa,CAAA;AACvD,IAAA,IAAI,MAAA,CAAO,aAAa,QAAA,EAAU;AAChC,MAAA,MAAM,IAAI,qBAAA,CAAsB,CAAA,SAAA,EAAY,GAAG,CAAA,aAAA,EAAgB,QAAQ,CAAA,YAAA,CAAc,CAAA;AAAA,IACvF;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAM,MAAA,GAAS,KAAK,SAAA,EAAU;AAC9B,EAAA,MAAM,SAAuB,EAAC;AAC9B,EAAA,IAAI,KAAA,GAAQ,CAAA;AACZ,EAAA,WAAS;AACP,IAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAM,GAAI,MAAM,OAAO,IAAA,EAAK;AAC1C,IAAA,IAAI,IAAA,EAAM;AACV,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,KAAA,IAAS,KAAA,CAAM,UAAA;AACf,MAAA,IAAI,QAAQ,QAAA,EAAU;AACpB,QAAA,MAAM,OAAO,MAAA,EAAO;AACpB,QAAA,MAAM,IAAI,qBAAA,CAAsB,CAAA,SAAA,EAAY,GAAG,CAAA,aAAA,EAAgB,QAAQ,CAAA,YAAA,CAAc,CAAA;AAAA,MACvF;AACA,MAAA,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,IACnB;AAAA,EACF;AACA,EAAA,OAAO,MAAA,CAAO,OAAO,MAAM,CAAA;AAC7B;AAGA,eAAe,eAAA,CAAgB,KAAa,OAAA,EAA4C;AACtF,EAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,wBAAA;AACvC,EAAA,MAAM,QAAA,GAAW,QAAQ,QAAA,IAAY,uBAAA;AACrC,EAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,KAAA,IAAS,UAAA,CAAW,KAAA;AAE9C,EAAA,MAAM,UAAA,GAAa,IAAI,eAAA,EAAgB;AACvC,EAAA,MAAM,QAAQ,UAAA,CAAW,MAAM,UAAA,CAAW,KAAA,IAAS,SAAS,CAAA;AAC5D,EAAA,IAAI;AAIF,IAAA,IAAI,UAAA,GAAa,GAAA;AACjB,IAAA,KAAA,IAAS,GAAA,GAAM,CAAA,IAAK,GAAA,IAAO,CAAA,EAAG;AAC5B,MAAA,MAAM,QAAA,GAAW,MAAM,SAAA,CAAU,UAAA,EAAY;AAAA,QAC3C,QAAQ,UAAA,CAAW,MAAA;AAAA,QACnB,QAAA,EAAU;AAAA,OACX,CAAA;AAED,MAAA,IAAI,QAAA,CAAS,MAAA,IAAU,GAAA,IAAO,QAAA,CAAS,SAAS,GAAA,EAAK;AACnD,QAAA,MAAM,QAAA,GAAW,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,UAAU,CAAA;AAChD,QAAA,MAAM,UAAU,QAAQ,CAAA;AACxB,QAAA,IAAI,CAAC,QAAA,EAAU;AACb,UAAA,MAAM,IAAI,qBAAA;AAAA,YACR,uBAAuB,UAAU,CAAA,8BAAA;AAAA,WACnC;AAAA,QACF;AACA,QAAA,IAAI,OAAO,aAAA,EAAe;AACxB,UAAA,MAAM,IAAI,qBAAA,CAAsB,CAAA,0CAAA,EAA6C,GAAG,CAAA,CAAA,CAAG,CAAA;AAAA,QACrF;AACA,QAAA,MAAM,OAAO,IAAI,GAAA,CAAI,QAAA,EAAU,UAAU,EAAE,QAAA,EAAS;AACpD,QAAA,IAAI,CAAC,eAAA,CAAgB,IAAA,CAAK,IAAI,CAAA,EAAG;AAC/B,UAAA,MAAM,IAAI,qBAAA;AAAA,YACR,uDAAuD,IAAI,CAAA,EAAA;AAAA,WAC7D;AAAA,QACF;AACA,QAAA,UAAA,GAAa,IAAA;AACb,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,MAAM,UAAU,QAAQ,CAAA;AACxB,QAAA,MAAM,IAAI,qBAAA;AAAA,UACR,CAAA,8BAAA,EAAiC,UAAU,CAAA,OAAA,EAAU,QAAA,CAAS,MAAM,CAAA,EAAA;AAAA,SACtE;AAAA,MACF;AACA,MAAA,MAAM,WAAW,MAAA,CAAO,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,gBAAgB,CAAC,CAAA;AAC9D,MAAA,IAAI,MAAA,CAAO,QAAA,CAAS,QAAQ,CAAA,IAAK,WAAW,QAAA,EAAU;AACpD,QAAA,MAAM,UAAU,QAAQ,CAAA;AACxB,QAAA,MAAM,IAAI,qBAAA;AAAA,UACR,CAAA,SAAA,EAAY,UAAU,CAAA,IAAA,EAAO,QAAQ,yBAAyB,QAAQ,CAAA,YAAA;AAAA,SACxE;AAAA,MACF;AACA,MAAA,OAAO,MAAM,UAAA,CAAW,QAAA,EAAU,QAAA,EAAU,UAAU,CAAA;AAAA,IACxD;AAAA,EACF,SAAS,KAAA,EAAO;AACd,IAAA,IAAI,KAAA,YAAiB,aAAa,MAAM,KAAA;AACxC,IAAA,IAAI,UAAA,CAAW,OAAO,OAAA,EAAS;AAC7B,MAAA,MAAM,IAAI,kBAAA,CAAmB,CAAA,IAAA,EAAO,GAAG,IAAI,SAAS,CAAA;AAAA,IACtD;AACA,IAAA,MAAM,IAAI,sBAAsB,CAAA,8BAAA,EAAiC,GAAG,KAAK,EAAE,KAAA,EAAO,OAAO,CAAA;AAAA,EAC3F,CAAA,SAAE;AACA,IAAA,YAAA,CAAa,KAAK,CAAA;AAAA,EACpB;AACF;AAcA,eAAsB,SAAA,CACpB,MAAA,EACA,OAAA,GAA4B,EAAC,EACb;AAChB,EAAA,IAAI,OAAA,CAAQ,MAAM,CAAA,EAAG;AACnB,IAAA,OAAO,OAAO,KAAA,EAAM;AAAA,EACtB;AACA,EAAA,IAAI,OAAO,WAAW,QAAA,EAAU;AAC9B,IAAA,IAAI,eAAA,CAAgB,IAAA,CAAK,MAAM,CAAA,EAAG;AAChC,MAAA,OAAOA,uBAAA,CAAM,MAAM,eAAA,CAAgB,MAAA,EAAQ,OAAO,CAAC,CAAA;AAAA,IACrD;AACA,IAAA,OAAOA,wBAAM,MAAM,CAAA;AAAA,EACrB;AACA,EAAA,IAAI,MAAA,CAAO,QAAA,CAAS,MAAM,CAAA,IAAK,kBAAkB,UAAA,EAAY;AAC3D,IAAA,OAAOA,wBAAM,MAAM,CAAA;AAAA,EACrB;AACA,EAAA,MAAM,IAAI,qBAAA;AAAA,IACR;AAAA,GACF;AACF;;;AC1KO,SAAS,SAAS,GAAA,EAAkB;AACzC,EAAA,MAAM,UAAU,GAAA,CAAI,OAAA,CAAQ,IAAA,EAAM,EAAE,EAAE,IAAA,EAAK;AAC3C,EAAA,MAAM,OACJ,OAAA,CAAQ,MAAA,KAAW,CAAA,GACf,OAAA,CACG,MAAM,EAAE,CAAA,CACR,GAAA,CAAI,CAAC,MAAM,CAAA,GAAI,CAAC,CAAA,CAChB,IAAA,CAAK,EAAE,CAAA,GACV,OAAA;AACN,EAAA,IAAI,CAAC,kBAAA,CAAmB,IAAA,CAAK,IAAI,CAAA,EAAG;AAClC,IAAA,MAAM,IAAI,qBAAA,CAAsB,CAAA,oBAAA,EAAuB,GAAG,CAAA,EAAA,CAAI,CAAA;AAAA,EAChE;AACA,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,QAAA,CAAS,IAAA,EAAM,EAAE,CAAA;AACtC,EAAA,OAAO,CAAE,SAAS,EAAA,GAAM,GAAA,EAAO,SAAS,CAAA,GAAK,GAAA,EAAM,QAAQ,GAAI,CAAA;AACjE;AAGO,SAAS,QAAA,CAAS,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA,EAAgB;AAC/C,EAAA,MAAM,OAAA,GAAU,CAAC,CAAA,KACf,IAAA,CAAK,IAAI,CAAA,EAAG,IAAA,CAAK,IAAI,GAAA,EAAK,IAAA,CAAK,MAAM,CAAC,CAAC,CAAC,CAAA,CACrC,QAAA,CAAS,EAAE,CAAA,CACX,QAAA,CAAS,GAAG,GAAG,CAAA;AACpB,EAAA,OAAO,CAAA,CAAA,EAAI,OAAA,CAAQ,CAAC,CAAC,CAAA,EAAG,OAAA,CAAQ,CAAC,CAAC,CAAA,EAAG,OAAA,CAAQ,CAAC,CAAC,GAAG,WAAA,EAAY;AAChE;AAGO,SAAS,MAAA,CAAO,CAAA,EAAQ,CAAA,EAAQ,CAAA,EAAgB;AACrD,EAAA,MAAM,OAAA,GAAU,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,GAAA,CAAI,CAAA,EAAG,CAAC,CAAC,CAAA;AAC1C,EAAA,OAAO;AAAA,IACL,CAAA,CAAE,CAAC,CAAA,GAAA,CAAK,CAAA,CAAE,CAAC,CAAA,GAAI,CAAA,CAAE,CAAC,CAAA,IAAK,OAAA;AAAA,IACvB,CAAA,CAAE,CAAC,CAAA,GAAA,CAAK,CAAA,CAAE,CAAC,CAAA,GAAI,CAAA,CAAE,CAAC,CAAA,IAAK,OAAA;AAAA,IACvB,CAAA,CAAE,CAAC,CAAA,GAAA,CAAK,CAAA,CAAE,CAAC,CAAA,GAAI,CAAA,CAAE,CAAC,CAAA,IAAK;AAAA,GACzB;AACF;AAGO,SAAS,SAAS,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,GAAQ,MAAA,EAAqB;AAC5D,EAAA,OAAO,CAAC,CAAA,GAAI,MAAA,EAAQ,CAAA,GAAI,MAAA,EAAQ,IAAI,MAAM,CAAA;AAC5C;AAWA,eAAsB,cAAA,CAAe,MAAA,EAAqB,QAAA,GAAW,SAAA,EAA4B;AAC/F,EAAA,MAAM,EAAE,IAAA,EAAM,IAAA,EAAK,GAAI,MAAA,CACrB,MAAM,SAAA,CAAU,MAAM,CAAA,EAErB,MAAA,CAAO,EAAA,EAAI,EAAA,EAAI,EAAE,GAAA,EAAK,OAAA,EAAS,CAAA,CAG/B,OAAA,CAAQ,EAAE,UAAA,EAAY,SAAA,EAAW,CAAA,CACjC,WAAA,EAAY,CACZ,GAAA,EAAI,CACJ,QAAA,CAAS,EAAE,iBAAA,EAAmB,MAAM,CAAA;AAEvC,EAAA,MAAM,WAAW,IAAA,CAAK,QAAA;AACtB,EAAA,IAAI,IAAA,GAAmB,IAAA;AACvB,EAAA,IAAI,cAAA,GAAiB,CAAA;AAErB,EAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,IAAI,IAAA,CAAK,MAAA,EAAQ,KAAK,QAAA,EAAU;AAClD,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,CAAC,CAAA,IAAK,CAAA;AACrB,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,CAAA,GAAI,CAAC,CAAA,IAAK,CAAA;AACzB,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,CAAA,GAAI,CAAC,CAAA,IAAK,CAAA;AACzB,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,GAAG,CAAC,CAAA;AAC5B,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,GAAG,CAAC,CAAA;AAC5B,IAAA,IAAI,QAAQ,CAAA,EAAG;AACf,IAAA,MAAM,UAAA,GAAA,CAAc,MAAM,GAAA,IAAO,GAAA;AACjC,IAAA,IAAI,UAAA,GAAa,cAAA,IAAkB,GAAA,GAAM,EAAA,EAAI;AAC3C,MAAA,cAAA,GAAiB,UAAA;AACjB,MAAA,IAAA,GAAO,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAAA,IACjB;AAAA,EACF;AAEA,EAAA,OAAO,IAAA,GAAO,QAAA,CAAS,IAAI,CAAA,GAAI,QAAA;AACjC;;;AC3EA,eAAsB,aAAA,CACpB,UACA,OAAA,EACuB;AACvB,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,MAAA;AACjC,EAAA,IAAI,MAAA;AAEJ,EAAA,IAAI,WAAW,KAAA,EAAO;AACpB,IAAA,MAAA,GAAS,MAAM,SAAS,GAAA,CAAI,EAAE,kBAAkB,CAAA,EAAG,EAAE,QAAA,EAAS;AAAA,EAChE,CAAA,MAAO;AACL,IAAA,MAAA,GAAS,MAAM,QAAA,CACZ,OAAA,CAAQ,EAAE,UAAA,EAAY,QAAQ,UAAA,IAAc,SAAA,EAAW,CAAA,CACvD,IAAA,CAAK;AAAA,MACJ,OAAA,EAAS,QAAQ,OAAA,IAAW,EAAA;AAAA,MAC5B,iBAAA,EAAmB,QAAQ,iBAAA,IAAqB;AAAA,KACjD,EACA,QAAA,EAAS;AAAA,EACd;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,MAAA,CAAO,QAAA,CAAS,QAAQ,CAAA;AAAA,IAC9B,MAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAO,OAAA,CAAQ,IAAA;AAAA,IACf,QAAQ,OAAA,CAAQ;AAAA,GAClB;AACF;AAWA,eAAsB,WAAA,CACpB,MAAA,EACA,OAAA,GAA8B,EAAC,EACR;AACvB,EAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,IAAQ,UAAA;AAC7B,EAAA,gBAAA,CAAiB,IAAA,EAAM,CAAA,EAAG,IAAA,EAAM,MAAM,CAAA;AACtC,EAAA,MAAM,YAAY,MAAM,SAAA,CAAU,MAAM,CAAA,EAAG,MAAA,CAAO,MAAM,IAAA,EAAM;AAAA,IAC5D,GAAA,EAAK,QAAQ,GAAA,IAAO,OAAA;AAAA,IACpB,MAAA,EAAQ,QAAQ,MAAA,IAAU,UAAA;AAAA,IAC1B,UAAA,EAAY,QAAQ,UAAA,IAAc;AAAA,GACnC,CAAA;AACD,EAAA,OAAO,cAAc,QAAA,EAAU,EAAE,GAAG,OAAA,EAAS,MAAM,CAAA;AACrD;AAWA,eAAsB,UAAA,CACpB,KAAA,EACA,OAAA,GAAyB,EAAC,EACH;AACvB,EAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,IAAQ,UAAA;AAC7B,EAAA,gBAAA,CAAiB,IAAA,EAAM,CAAA,EAAG,IAAA,EAAM,MAAM,CAAA;AACtC,EAAA,MAAM,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA,GAAI,SAAS,KAAK,CAAA;AAChC,EAAA,MAAM,WAAWA,uBAAAA,CAAM;AAAA,IACrB,MAAA,EAAQ,EAAE,KAAA,EAAO,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,QAAA,EAAU,CAAA,EAAG,UAAA,EAAY,EAAE,CAAA,EAAG,CAAA,EAAG,GAAE;AAAE,GAC3E,CAAA;AACD,EAAA,OAAO,cAAc,QAAA,EAAU,EAAE,GAAG,OAAA,EAAS,MAAM,CAAA;AACrD;;;ACxDA,eAAsB,eAAA,CACpB,MAAA,EACA,OAAA,GAAkC,EAAC,EACZ;AACvB,EAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,IAAQ,UAAA;AAC7B,EAAA,MAAM,KAAA,GAAQ,QAAQ,KAAA,IAAS,QAAA;AAC/B,EAAA,MAAM,IAAA,GAAO,MAAM,SAAA,CAAU,MAAM,CAAA;AAEnC,EAAA,MAAM,QAAA,GACJ,KAAA,KAAU,OAAA,GACN,IAAA,CACG,MAAA,CAAO,IAAI,EAAA,EAAI,EAAE,MAAA,EAAQ,SAAA,EAAW,GAAA,EAAK,OAAA,EAAS,CAAA,CAClD,MAAA,CAAO,IAAA,EAAM,IAAA,EAAM,EAAE,MAAA,EAAQ,SAAA,EAAW,CAAA,CACxC,QAAA,CAAS,EAAE,UAAA,EAAY,GAAA,EAAK,IAC/B,IAAA,CACG,MAAA,CAAO,IAAA,EAAM,IAAA,EAAM,EAAE,MAAA,EAAQ,UAAA,EAAY,GAAA,EAAK,OAAA,EAAS,CAAA,CACvD,QAAA,CAAS,EAAE,UAAA,EAAY,MAAM,CAAA,CAE7B,MAAA,CAAO,IAAA,EAAM,IAAa,CAAA,CAC1B,OAAA,CAAQ,EAAE,KAAA,EAAO,CAAA,EAAK,CAAA;AAE/B,EAAA,OAAO,cAAc,QAAA,EAAU,EAAE,GAAG,OAAA,EAAS,MAAM,CAAA;AACrD;ACVA,eAAsB,sBAAA,CACpB,MAAA,EACA,OAAA,GAAwB,EAAC,EACF;AACvB,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,CAAC,GAAG,CAAA,EAAG,CAAA,EAAG,GAAG,CAAC,CAAA;AAC/C,EAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,IAAQ,UAAA;AAC7B,EAAA,gBAAA,CAAiB,IAAA,EAAM,CAAA,EAAG,IAAA,EAAM,MAAM,CAAA;AACtC,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,YAAA;AAGjC,EAAA,IAAI,MAAA,CAAO,SAAS,WAAA,EAAa;AAC/B,IAAA,MAAM,IAAI,qBAAA;AAAA,MACR,CAAA,wCAAA,EAA2C,WAAW,CAAA,kBAAA,EAAqB,MAAA,CAAO,MAAM,CAAA,EAAA;AAAA,KAC1F;AAAA,EACF;AACA,EAAA,MAAA,CAAO,QAAQ,gBAAgB,CAAA;AAC/B,EAAA,MAAM,QAAQ,MAAA,CAAO,MAAA;AAErB,EAAA,IAAI,UAAU,CAAA,EAAG;AACf,IAAA,OAAO,EAAC;AAAA,EACV;AAEA,EAAA,MAAM,IAAA,GAAO,MAAM,SAAA,CAAU,MAAM,CAAA;AACnC,EAAA,MAAM,OAAA,GACJ,MAAA,KAAW,YAAA,GACP,IAAA,CAAK,MAAA,CAAO,OAAO,KAAA,EAAO,IAAA,EAAM,EAAE,GAAA,EAAK,OAAA,EAAS,MAAA,EAAQ,YAAY,CAAA,GACpE,IAAA,CAAK,MAAA,CAAO,IAAA,EAAM,IAAA,GAAO,KAAA,EAAO,EAAE,GAAA,EAAK,OAAA,EAAS,MAAA,EAAQ,UAAA,EAAY,CAAA;AAG1E,EAAA,MAAM,MAAA,GAAS,MAAM,OAAA,CAAQ,GAAA,GAAM,QAAA,EAAS;AAE5C,EAAA,MAAM,UAAwB,EAAC;AAC/B,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,EAAO,KAAK,CAAA,EAAG;AACjC,IAAA,MAAM,KAAA,GAAQ,OAAO,CAAC,CAAA;AACtB,IAAA,IAAI,UAAU,MAAA,EAAW;AACzB,IAAA,MAAM,IAAA,GAAOA,uBAAAA,CAAM,MAAM,CAAA,CAAE,OAAA,CAAQ;AAAA,MACjC,IAAA,EAAM,MAAA,KAAW,YAAA,GAAe,CAAA,GAAI,IAAA,GAAO,CAAA;AAAA,MAC3C,GAAA,EAAK,MAAA,KAAW,UAAA,GAAa,CAAA,GAAI,IAAA,GAAO,CAAA;AAAA,MACxC,KAAA,EAAO,IAAA;AAAA,MACP,MAAA,EAAQ;AAAA,KACT,CAAA;AACD,IAAA,OAAA,CAAQ,IAAA,CAAK,EAAE,KAAA,EAAO,KAAA,EAAO,MAAM,aAAA,CAAc,IAAA,EAAM,EAAE,GAAG,OAAA,EAAS,IAAA,EAAM,GAAG,CAAA;AAAA,EAChF;AACA,EAAA,OAAO,OAAA;AACT;AC9CA,IAAM,cAAA,GAAiB,IAAA;AAGvB,SAAS,aAAa,IAAA,EAAsB;AAC1C,EAAA,OAAO,IAAA,CACJ,OAAA,CAAQ,IAAA,EAAM,OAAO,EACrB,OAAA,CAAQ,IAAA,EAAM,MAAM,CAAA,CACpB,QAAQ,IAAA,EAAM,MAAM,CAAA,CACpB,OAAA,CAAQ,MAAM,QAAQ,CAAA;AAC3B;AAOA,SAAS,aAAa,KAAA,EAAuB;AAC3C,EAAA,OAAO,QAAA,CAAS,QAAA,CAAS,KAAK,CAAC,CAAA;AACjC;AAGA,eAAe,WAAW,IAAA,EASA;AACxB,EAAA,MAAM,MAAA,GAAS,CAAA,kBAAA,EAAqB,YAAA,CAAa,IAAA,CAAK,KAAK,CAAC,CAAA,CAAA,EAC1D,IAAA,CAAK,IAAA,GAAO,mBAAmB,EACjC,CAAA,CAAA,EAAI,YAAA,CAAa,IAAA,CAAK,IAAI,CAAC,CAAA,OAAA,CAAA;AAE3B,EAAA,MAAM,SAAA,GAAwB;AAAA,IAC5B,IAAA,EAAM,MAAA;AAAA,IACN,MAAM,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,CAAA,EAAI,KAAK,IAAI,CAAA,CAAA;AAAA,IACjC,IAAA,EAAM,IAAA;AAAA,IACN,KAAA,EAAO,KAAK,KAAA,IAAS,MAAA;AAAA,IACrB,KAAA,EAAO,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,KAAA,CAAM,IAAA,CAAK,KAAK,CAAC,CAAA;AAAA,IACzC,IAAA,EAAM;AAAA,GACR;AACA,EAAA,IAAI,IAAA,CAAK,cAAc,MAAA,EAAW;AAChC,IAAA,SAAA,CAAU,MAAA,GAAS,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,KAAA,CAAM,IAAA,CAAK,SAAS,CAAC,CAAA;AAAA,EAC3D;AAEA,EAAA,MAAM,MAAA,GAAS,MAAMA,uBAAAA,CAAM,EAAE,IAAA,EAAM,WAAW,CAAA,CAAE,GAAA,EAAI,CAAE,QAAA,EAAS;AAC/D,EAAA,MAAM,IAAA,GAAO,MAAMA,uBAAAA,CAAM,MAAM,EAAE,QAAA,EAAS;AAC1C,EAAA,OAAO,EAAE,QAAQ,KAAA,EAAO,IAAA,CAAK,SAAS,CAAA,EAAG,MAAA,EAAQ,IAAA,CAAK,MAAA,IAAU,CAAA,EAAE;AACpE;AAQA,eAAe,UAAA,CACb,MAAA,EACA,GAAA,EACA,IAAA,EACA,IAAA,EACyB;AACzB,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA,EAAG,IAAA,GAAO,CAAC,CAAC,CAAA;AAC/D,EAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA,EAAG,IAAA,GAAO,CAAC,CAAC,CAAA;AACjE,EAAA,MAAM,OAAO,IAAA,GAAO,QAAA;AACpB,EAAA,MAAM,OAAO,IAAA,GAAO,OAAA;AAEpB,EAAA,MAAM,IAAA,GAAO,MAAMA,uBAAAA,CAAM,MAAM,EAAE,QAAA,EAAS;AAC1C,EAAA,IAAA,CAAK,KAAK,KAAA,IAAS,CAAA,KAAM,SAAS,IAAA,CAAK,MAAA,IAAU,MAAM,IAAA,EAAM;AAC3D,IAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAQ,GAAA,EAAK,OAAA,EAAS,MAAM,QAAA,EAAS;AAAA,EACvD;AACA,EAAA,MAAM,UAAU,MAAMA,uBAAAA,CAAM,MAAM,CAAA,CAC/B,OAAO,IAAA,EAAM,IAAA,EAAM,EAAE,GAAA,EAAK,UAAU,kBAAA,EAAoB,IAAA,EAAM,CAAA,CAC9D,GAAA,GACA,QAAA,EAAS;AACZ,EAAA,OAAO,EAAE,KAAA,EAAO,OAAA,EAAS,GAAA,EAAK,OAAA,EAAS,MAAM,QAAA,EAAS;AACxD;AAGA,eAAe,iBAAA,CACb,IAAA,EACA,MAAA,EACA,KAAA,EACA,QACA,IAAA,EACuB;AACvB,EAAA,MAAM,aAAa,CAAC,IAAA,EAAM,MAAM,IAAA,EAAM,IAAA,EAAM,IAAI,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,KAAM,IAAA,CAAK,IAAI,EAAA,EAAI,IAAA,CAAK,MAAM,IAAA,GAAO,CAAC,CAAC,CAAC,CAAA;AAC/F,EAAA,KAAA,MAAWC,aAAY,UAAA,EAAY;AACjC,IAAA,MAAM,QAAA,GAAW,MAAM,UAAA,CAAW;AAAA,MAChC,IAAA;AAAA,MACA,MAAA;AAAA,MACA,IAAA,EAAMA,SAAAA;AAAA,MACN,KAAA,EAAO,SAAA;AAAA,MACP,IAAA,EAAM,IAAA;AAAA,MACN;AAAA,KACD,CAAA;AACD,IAAA,IAAI,QAAA,CAAS,MAAA,IAAU,MAAA,IAAU,QAAA,CAAS,SAAS,KAAA,EAAO;AACxD,MAAA,OAAO,QAAA;AAAA,IACT;AAAA,EACF;AAGA,EAAA,MAAM,QAAA,GAAW,KAAK,GAAA,CAAI,EAAA,EAAI,KAAK,KAAA,CAAM,IAAA,GAAO,IAAI,CAAC,CAAA;AACrD,EAAA,OAAO,UAAA,CAAW;AAAA,IAChB,IAAA;AAAA,IACA,MAAA;AAAA,IACA,IAAA,EAAM,QAAA;AAAA,IACN,KAAA,EAAO,SAAA;AAAA,IACP,IAAA,EAAM,IAAA;AAAA,IACN,KAAA;AAAA,IACA,SAAA,EAAW,IAAA,CAAK,GAAA,CAAI,QAAA,EAAU,MAAM;AAAA,GACrC,CAAA;AACH;AAGA,SAAS,eAAA,CAAgB,UAAA,EAAoB,MAAA,EAAa,IAAA,EAAqB;AAC7E,EAAA,IAAI,eAAe,OAAA,EAAS;AAC1B,IAAA,MAAM,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA,GAAI,MAAA;AAClB,IAAA,OAAOD,wBAAM,EAAE,MAAA,EAAQ,EAAE,KAAA,EAAO,MAAM,MAAA,EAAQ,IAAA,EAAM,QAAA,EAAU,CAAA,EAAG,YAAY,EAAE,CAAA,EAAG,GAAG,CAAA,EAAE,IAAK,CAAA;AAAA,EAC9F;AACA,EAAA,IAAI,eAAe,UAAA,EAAY;AAC7B,IAAA,MAAM,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA,GAAI,SAAS,UAAU,CAAA;AACrC,IAAA,OAAOA,wBAAM,EAAE,MAAA,EAAQ,EAAE,KAAA,EAAO,MAAM,MAAA,EAAQ,IAAA,EAAM,QAAA,EAAU,CAAA,EAAG,YAAY,EAAE,CAAA,EAAG,GAAG,CAAA,EAAE,IAAK,CAAA;AAAA,EAC9F;AACA,EAAA,MAAM,CAAC,EAAA,EAAI,EAAA,EAAI,EAAE,CAAA,GAAI,QAAA,CAAS,MAAA,EAAQ,GAAG,CAAA,CAAE,GAAA,CAAI,IAAA,CAAK,KAAK,CAAA;AACzD,EAAA,MAAM,GAAA,GAAM,CAAA,+CAAA,EAAkD,IAAI,CAAA,UAAA,EAAa,IAAI,CAAA;AAAA;AAAA,uCAAA,EAE5C,EAAE,CAAA,CAAA,EAAI,EAAE,CAAA,CAAA,EAAI,EAAE,CAAA;AAAA;AAAA;AAAA,iBAAA,EAGpC,IAAI,aAAa,IAAI,CAAA;AAAA,QAAA,CAAA;AAEtC,EAAA,OAAOA,uBAAAA,CAAM,OAAO,IAAA,CAAK,GAAG,CAAC,CAAA,CAAE,MAAA,CAAO,MAAM,IAAI,CAAA;AAClD;AAGA,SAAS,YAAY,IAAA,EAQV;AACT,EAAA,MAAM,EAAE,MAAM,MAAA,EAAQ,MAAA,EAAQ,UAAU,UAAA,EAAY,QAAA,EAAU,MAAK,GAAI,IAAA;AACvE,EAAA,MAAM,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA,GAAI,MAAA;AAClB,EAAA,MAAM,YAAY,CAAA,IAAA,EAAO,CAAC,CAAA,CAAA,EAAI,CAAC,IAAI,CAAC,CAAA,CAAA,CAAA;AACpC,EAAA,MAAM,QAAkB,EAAC;AAEzB,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,IAAA,GAAO,KAAK,CAAA;AACjC,IAAA,MAAM,CAAA,GAAI,QAAA;AACV,IAAA,KAAA,CAAM,IAAA;AAAA,MACJ,oBAAoB,MAAM,CAAA,CAAA,EAAI,CAAC,CAAA,CAAA,EAAI,MAAM,CAAA,CAAA,EAAI,CAAA,GAAI,CAAC,CAAA,CAAA,EAAI,SAAS,CAAC,CAAA,CAAA,EAAI,IAAI,CAAA,GAAI,CAAC,WAAW,SAAS,CAAA,GAAA;AAAA,KACnG;AAAA,EACF;AAEA,EAAA,IAAI,aAAa,MAAA,EAAW;AAC1B,IAAA,MAAM,EAAA,GAAK,MAAA;AACX,IAAA,MAAM,KAAK,IAAA,GAAO,MAAA;AAClB,IAAA,MAAM,CAAA,GAAI,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,GAAA,CAAI,CAAA,EAAG,QAAQ,CAAC,CAAA;AAC3C,IAAA,MAAM,EAAA,GAAK,EAAA,GAAA,CAAM,EAAA,GAAK,EAAA,IAAM,CAAA;AAC5B,IAAA,MAAM,CAAC,EAAA,EAAI,EAAA,EAAI,EAAE,CAAA,GAAI,QAAA,CAAS,MAAA,EAAQ,IAAI,CAAA,CAAE,GAAA,CAAI,IAAA,CAAK,KAAK,CAAA;AAC1D,IAAA,KAAA,CAAM,IAAA;AAAA,MACJ,CAAA,UAAA,EAAa,EAAE,CAAA,MAAA,EAAS,IAAI,CAAA,MAAA,EAAS,EAAE,CAAA,MAAA,EAAS,IAAI,CAAA,cAAA,EAAiB,EAAE,CAAA,CAAA,EAAI,EAAE,IAAI,EAAE,CAAA,4CAAA,CAAA;AAAA,MACnF,CAAA,UAAA,EAAa,EAAE,CAAA,MAAA,EAAS,IAAI,SAAS,EAAE,CAAA,MAAA,EAAS,IAAI,CAAA,UAAA,EAAa,SAAS,CAAA,2CAAA;AAAA,KAC5E;AAAA,EACF;AAEA,EAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IACZ,CAAA,+CAAA,EAAkD,IAAI,CAAA,UAAA,EAAa,IAAI,KAAK,KAAA,CAAM,IAAA,CAAK,EAAE,CAAC,CAAA,MAAA;AAAA,GAC5F;AACF;AAmBA,eAAsB,gBAAgB,OAAA,EAAwD;AAC5F,EAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,IAAQ,UAAA;AAC7B,EAAA,gBAAA,CAAiB,IAAA,EAAM,CAAA,EAAG,cAAA,EAAgB,MAAM,CAAA;AAChD,EAAA,oBAAA,CAAqB,OAAA,CAAQ,OAAO,OAAO,CAAA;AAE3C,EAAA,MAAM,MAAA,GAAS,QAAQ,IAAA,IAAQ,aAAA;AAC/B,EAAA,MAAM,SAAA,GAAY,QAAQ,MAAA,IAAU,SAAA;AACpC,EAAA,MAAM,MAAA,GAAS,SAAS,SAAS,CAAA;AACjC,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,IAAA,GAAO,KAAK,CAAA;AACtC,EAAA,MAAM,WAAW,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,GAAO,SAAS,CAAC,CAAA;AAG9C,EAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,OAAA,EAAS,IAAA,EAAK;AAC1C,EAAA,MAAM,YAAA,GAAe,OAAA,CAAQ,QAAA,EAAU,IAAA,EAAK;AAG5C,EAAA,MAAM,WAAiE,EAAC;AAGxE,EAAA,IAAI,UAAA,GAAa,MAAA;AACjB,EAAA,MAAM,QAAA,GAAW,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,OAAO,KAAK,CAAA;AACjD,EAAA,IAAI,WAAA,EAAa;AACf,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,IAAA,GAAO,KAAK,CAAA;AACrC,IAAA,MAAM,gBAAA,GAAmB,KAAK,GAAA,CAAI,KAAA,EAAO,KAAK,KAAA,CAAM,IAAA,GAAO,IAAI,CAAC,CAAA;AAChE,IAAA,MAAM,OAAA,GAAU,MAAM,UAAA,CAAW;AAAA,MAC/B,IAAA,EAAM,YAAY,WAAA,EAAY;AAAA,MAC9B,MAAA;AAAA,MACA,IAAA,EAAM,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,KAAA,CAAM,IAAA,GAAO,IAAI,CAAC,CAAA;AAAA,MACzC,KAAA,EAAO,SAAA;AAAA,MACP,IAAA,EAAM,IAAA;AAAA,MACN,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,WAAW,KAAK;AAAA,KACpC,CAAA;AACD,IAAA,QAAA,CAAS,IAAA,CAAK;AAAA,MACZ,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,GAAA,EAAK,QAAA;AAAA,MACL,MAAM,MAAA,GAAS,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,OAAO,IAAI;AAAA,KAC9C,CAAA;AACD,IAAA,UAAA,GACE,QAAA,GACA,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,IAAI,OAAA,CAAQ,MAAA,EAAQ,KAAK,CAAA,EAAG,gBAAgB,CAAA,GAC1D,IAAA,CAAK,KAAA,CAAM,OAAO,IAAI,CAAA;AAAA,EAC1B;AAEA,EAAA,UAAA,GAAa,IAAA,CAAK,IAAI,UAAA,EAAY,IAAA,GAAO,KAAK,KAAA,CAAM,IAAA,GAAO,GAAG,CAAC,CAAA;AAG/D,EAAA,MAAM,WAAA,GAAc,OAAO,OAAA,CAAQ,QAAA,KAAa,QAAA;AAChD,EAAA,MAAM,IAAA,GAAO,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,OAAO,IAAI,CAAA;AAC1C,EAAA,MAAM,WAAA,GAAc,cAAc,IAAA,GAAO,IAAA,CAAK,MAAM,IAAA,GAAO,IAAI,IAAI,IAAA,GAAO,MAAA;AAG1E,EAAA,IAAI,WAAA,GAAc,WAAA;AAClB,EAAA,IAAI,YAAA,EAAc;AAChB,IAAA,MAAM,OAAA,GAAU,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,KAAA,CAAM,IAAA,GAAO,IAAI,CAAC,CAAA;AACnD,IAAA,MAAM,YAAA,GAAe,UAAU,CAAA,GAAI,CAAA;AAKnC,IAAA,IAAI,QAAA,GAAW,MAAM,UAAA,CAAW;AAAA,MAC9B,IAAA,EAAM,YAAA;AAAA,MACN,MAAA;AAAA,MACA,IAAA,EAAM,OAAA;AAAA,MACN,KAAA,EAAO,SAAA;AAAA,MACP,KAAA,EAAO;AAAA,KACR,CAAA;AACD,IAAA,IAAI,QAAA,CAAS,SAAS,YAAA,EAAc;AAClC,MAAA,MAAM,OAAA,GAAU,MAAMA,uBAAAA,CAAM,QAAA,CAAS,MAAM,CAAA,CACxC,OAAA,CAAQ,EAAE,IAAA,EAAM,CAAA,EAAG,KAAK,CAAA,EAAG,KAAA,EAAO,SAAS,KAAA,EAAO,MAAA,EAAQ,cAAc,CAAA,CACxE,GAAA,EAAI,CACJ,QAAA,EAAS;AACZ,MAAA,QAAA,GAAW,EAAE,MAAA,EAAQ,OAAA,EAAS,OAAO,QAAA,CAAS,KAAA,EAAO,QAAQ,YAAA,EAAa;AAAA,IAC5E;AACA,IAAA,WAAA,GAAc,IAAA,CAAK,GAAA,CAAI,UAAA,EAAY,WAAA,GAAc,SAAS,MAAM,CAAA;AAChE,IAAA,QAAA,CAAS,IAAA,CAAK,EAAE,MAAA,EAAQ,QAAA,CAAS,QAAQ,GAAA,EAAK,WAAA,EAAa,IAAA,EAAM,MAAA,EAAQ,CAAA;AAAA,EAC3E;AAGA,EAAA,MAAM,WAAA,GAAc,eAAe,WAAA,GAAc,WAAA;AACjD,EAAA,MAAM,cAAc,IAAA,CAAK,GAAA;AAAA,IACvB,IAAA,CAAK,KAAA,CAAM,IAAA,GAAO,IAAI,CAAA;AAAA,IACtB,WAAA,GAAc,UAAA,GAAa,IAAA,CAAK,KAAA,CAAM,OAAO,IAAI;AAAA,GACnD;AACA,EAAA,MAAM,KAAA,GAAQ,MAAM,iBAAA,CAAkB,OAAA,CAAQ,OAAO,MAAA,EAAQ,QAAA,EAAU,aAAa,IAAI,CAAA;AACxF,EAAA,QAAA,CAAS,IAAA,CAAK,EAAE,MAAA,EAAQ,KAAA,CAAM,QAAQ,GAAA,EAAK,UAAA,EAAY,IAAA,EAAM,MAAA,EAAQ,CAAA;AAGrE,EAAA,MAAM,UAAA,GAA+B;AAAA,IACnC;AAAA,MACE,OAAO,WAAA,CAAY;AAAA,QACjB,IAAA;AAAA,QACA,MAAA;AAAA,QACA,MAAA;AAAA,QACA,QAAA;AAAA,QACA,UAAA,EAAY,QAAQ,WAAW,CAAA;AAAA,QAC/B,UAAU,OAAA,CAAQ,QAAA;AAAA,QAClB;AAAA,OACD,CAAA;AAAA,MACD,GAAA,EAAK,CAAA;AAAA,MACL,IAAA,EAAM;AAAA;AACR,GACF;AACA,EAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC9B,IAAA,UAAA,CAAW,IAAA,CAAK,MAAM,UAAA,CAAW,OAAA,CAAQ,MAAA,EAAQ,QAAQ,GAAA,EAAK,OAAA,CAAQ,IAAA,EAAM,IAAI,CAAC,CAAA;AAAA,EACnF;AAEA,EAAA,MAAM,WAAW,eAAA,CAAgB,OAAA,CAAQ,cAAc,UAAA,EAAY,MAAA,EAAQ,IAAI,CAAA,CAAE,SAAA;AAAA,IAC/E;AAAA,GACF;AACA,EAAA,OAAO,cAAc,QAAA,EAAU,EAAE,GAAG,OAAA,EAAS,MAAM,CAAA;AACrD","file":"index.cjs","sourcesContent":["/**\n * Error hierarchy for the Divoom Times Gate SDK.\n *\n * Every error thrown by the SDK extends {@link DivoomError}, so you can catch\n * the whole family with a single `instanceof DivoomError` check, or narrow to a\n * specific subclass when you need to react differently (e.g. retry on a\n * timeout, but surface a validation mistake to the user).\n *\n * @packageDocumentation\n */\n\n/** Base class for every error thrown by the SDK. */\nexport class DivoomError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options as ErrorOptions | undefined);\n this.name = 'DivoomError';\n }\n}\n\n/** Thrown when arguments fail validation before a request is ever sent. */\nexport class DivoomValidationError extends DivoomError {\n constructor(message: string) {\n super(message);\n this.name = 'DivoomValidationError';\n }\n}\n\n/** Thrown when the device cannot be reached (DNS, connection refused, etc.). */\nexport class DivoomConnectionError extends DivoomError {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = 'DivoomConnectionError';\n }\n}\n\n/** Thrown when a request does not complete within the configured timeout. */\nexport class DivoomTimeoutError extends DivoomError {\n /** The command that timed out. */\n readonly command: string;\n /** The timeout budget, in milliseconds, that was exceeded. */\n readonly timeoutMs: number;\n\n constructor(command: string, timeoutMs: number) {\n super(`Command \"${command}\" timed out after ${timeoutMs}ms.`);\n this.name = 'DivoomTimeoutError';\n this.command = command;\n this.timeoutMs = timeoutMs;\n }\n}\n\n/** Thrown when the device responds with a non-2xx HTTP status. */\nexport class DivoomHttpError extends DivoomError {\n /** The HTTP status code returned by the device. */\n readonly status: number;\n /** The command that produced the error. */\n readonly command: string;\n /** The full request URL. */\n readonly url: string;\n\n constructor(status: number, command: string, url: string) {\n super(`Times Gate returned HTTP ${status} for command \"${command}\" (${url}).`);\n this.name = 'DivoomHttpError';\n this.status = status;\n this.command = command;\n this.url = url;\n }\n}\n\n/**\n * Thrown when a Divoom **cloud** API (`app.divoom-gz.com`) reports a non-zero\n * `ReturnCode`.\n */\nexport class DivoomCloudError extends DivoomError {\n /** The non-zero `ReturnCode` reported by the cloud API. */\n readonly returnCode: number;\n /** The cloud endpoint path that failed. */\n readonly endpoint: string;\n /** The `ReturnMessage`, when provided. */\n readonly returnMessage?: string;\n\n constructor(returnCode: number, endpoint: string, returnMessage?: string) {\n super(\n `Divoom cloud endpoint \"${endpoint}\" failed with ReturnCode ${returnCode}` +\n (returnMessage ? `: ${returnMessage}` : '.'),\n );\n this.name = 'DivoomCloudError';\n this.returnCode = returnCode;\n this.endpoint = endpoint;\n this.returnMessage = returnMessage;\n }\n}\n\n/**\n * Thrown when the device accepts the request but reports a non-zero\n * `error_code` in the response body.\n */\nexport class DivoomDeviceError extends DivoomError {\n /** The non-zero `error_code` reported by the device. */\n readonly errorCode: number;\n /** The command that was rejected. */\n readonly command: string;\n /** The raw response body returned by the device. */\n readonly response: unknown;\n\n constructor(errorCode: number, command: string, response: unknown) {\n super(\n `Times Gate rejected command \"${command}\" with error_code ${errorCode}. ` +\n `This often means the LocalToken is missing or incorrect for a command that requires it.`,\n );\n this.name = 'DivoomDeviceError';\n this.errorCode = errorCode;\n this.command = command;\n this.response = response;\n }\n}\n","/**\n * Device-level constants for the Divoom Times Gate.\n *\n * @packageDocumentation\n */\n\n/** The Times Gate has five individually-addressable LCD panels. */\nexport const PANEL_COUNT = 5;\n\n/** Each panel renders a square {@link PANEL_SIZE}×{@link PANEL_SIZE} image. */\nexport const PANEL_SIZE = 128;\n\n/** Default HTTP port for hardware version 400 devices. */\nexport const DEFAULT_PORT = 80;\n\n/** Default request path for hardware version 400 devices (`http://IP:80/post`). */\nexport const DEFAULT_PATH = '/post';\n\n/** HTTP port for hardware version 402 devices. */\nexport const HARDWARE_402_PORT = 9000;\n\n/** Request path for hardware version 402 devices (`http://IP:9000/divoom_api`). */\nexport const HARDWARE_402_PATH = '/divoom_api';\n\n/**\n * Divoom's cloud endpoint that returns every Divoom device currently visible\n * on the same LAN as the caller. Used by {@link discoverDevices}.\n */\nexport const LAN_DISCOVERY_URL = 'https://app.divoom-gz.com/Device/ReturnSameLANDevice';\n","/**\n * Small, dependency-free helpers shared across the SDK: argument validation,\n * panel-mask construction, and a monotonic PicID generator.\n *\n * @packageDocumentation\n */\n\nimport { PANEL_COUNT } from './constants';\nimport { DivoomValidationError } from './errors';\nimport type { LcdArray, PanelIndex } from './types/common';\n\n/** Resolves after `ms` milliseconds. */\nexport const delay = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * Best-effort release of a fetch `Response` body on error paths where it is\n * never read, so the underlying socket returns to the connection pool instead of\n * being pinned until garbage collection. Never throws.\n */\nexport async function drainBody(response: {\n body?: { cancel?: () => Promise<unknown> } | null;\n}): Promise<void> {\n try {\n await response.body?.cancel?.();\n } catch {\n // ignore — releasing the body is best-effort\n }\n}\n\n/**\n * Asserts that `value` is an integer within `[min, max]`, throwing a\n * {@link DivoomValidationError} otherwise.\n */\nexport function assertIntInRange(value: number, min: number, max: number, label: string): void {\n if (!Number.isInteger(value) || value < min || value > max) {\n throw new DivoomValidationError(\n `${label} must be an integer between ${min} and ${max} (received ${String(value)}).`,\n );\n }\n}\n\n/** Asserts that `value` is a valid {@link PanelIndex} (`0`–`4`). */\nexport function assertPanelIndex(value: number): asserts value is PanelIndex {\n if (!Number.isInteger(value) || value < 0 || value >= PANEL_COUNT) {\n throw new DivoomValidationError(\n `panel must be an integer between 0 and ${PANEL_COUNT - 1} (received ${String(value)}).`,\n );\n }\n}\n\n/** Asserts that `value` is a non-empty string after trimming. */\nexport function assertNonEmptyString(value: unknown, label: string): asserts value is string {\n if (typeof value !== 'string' || value.trim().length === 0) {\n throw new DivoomValidationError(`${label} must be a non-empty string.`);\n }\n}\n\n/**\n * Builds a panel selection mask for a single panel.\n *\n * @example\n * ```ts\n * panelToLcdArray(2); // → [0, 0, 1, 0, 0]\n * ```\n */\nexport function panelToLcdArray(panel: PanelIndex): LcdArray {\n assertPanelIndex(panel);\n const arr: LcdArray = [0, 0, 0, 0, 0];\n arr[panel] = 1;\n return arr;\n}\n\n/**\n * Builds a panel selection mask from a set of panels.\n *\n * @example\n * ```ts\n * panelsToLcdArray([0, 4]); // → [1, 0, 0, 0, 1]\n * ```\n */\nexport function panelsToLcdArray(panels: Iterable<PanelIndex>): LcdArray {\n const arr: LcdArray = [0, 0, 0, 0, 0];\n for (const panel of panels) {\n assertPanelIndex(panel);\n arr[panel] = 1;\n }\n return arr;\n}\n\n/**\n * Generates strictly-increasing PicIDs for image uploads. The Times Gate caches\n * frames by ID, so every pushed frame needs a fresh, monotonically growing ID.\n *\n * Uniqueness within a single generator comes from the `+10` step taken on every\n * `next()` call; the per-panel offset only spaces consecutive IDs apart and is\n * not what prevents collisions. The generator is seeded from the current time in\n * seconds, so two *separate* generators created within the same second can\n * produce overlapping IDs — share one generator if you need cross-instance\n * uniqueness (or seed from the device via `Draw/GetHttpGifId`).\n */\nexport class PicIdGenerator {\n private base: number;\n\n constructor(seed: number = Math.floor(Date.now() / 1000)) {\n this.base = seed;\n }\n\n /** Returns the next unique PicID for the given panel. */\n next(panel = 0): number {\n this.base += 10;\n return this.base + panel;\n }\n}\n","/**\n * Turns any {@link ImageSource} into a `sharp` pipeline.\n *\n * @remarks\n * **Trust model.** A `string` source is treated as an `http(s)` URL when it\n * starts with that scheme, and otherwise as a **local file path**. Do not pass\n * untrusted/attacker-influenced strings here: a URL can trigger a server-side\n * request (SSRF) and a path can read arbitrary local files. URL downloads are\n * bounded by a timeout and a maximum size, but callers handling untrusted input\n * should validate/allowlist the source themselves (and prefer passing a\n * `Buffer` you fetched under your own controls).\n *\n * @packageDocumentation\n */\n\nimport sharp, { type Sharp } from 'sharp';\nimport {\n DivoomConnectionError,\n DivoomError,\n DivoomTimeoutError,\n DivoomValidationError,\n} from '../errors';\nimport type { FetchLike } from '../types/common';\nimport { drainBody } from '../utils';\nimport type { ImageSource } from './types';\n\n/** Default per-download timeout for URL sources. */\nconst DEFAULT_FETCH_TIMEOUT_MS = 15_000;\n\n/** Default maximum download size for URL sources (64 MiB). */\nconst DEFAULT_MAX_IMAGE_BYTES = 64 * 1024 * 1024;\n\n/** Maximum redirect hops followed when downloading a URL image. */\nconst MAX_REDIRECTS = 5;\n\n/** Options controlling how URL image sources are downloaded. */\nexport interface LoadImageOptions {\n /** Per-download timeout in milliseconds (URL sources only). Defaults to `15000`. */\n timeoutMs?: number;\n /** Maximum bytes to download before aborting (URL sources only). Defaults to 64 MiB. */\n maxBytes?: number;\n /** A custom `fetch` for URL sources. Defaults to the global `fetch`. */\n fetch?: FetchLike;\n}\n\n/** Duck-types a `sharp` instance without relying on `instanceof`. */\nfunction isSharp(value: unknown): value is Sharp {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as Sharp).toBuffer === 'function' &&\n typeof (value as Sharp).resize === 'function'\n );\n}\n\n/** Reads a response body, aborting if it exceeds `maxBytes`. */\nasync function readCapped(\n response: Awaited<ReturnType<FetchLike>>,\n maxBytes: number,\n url: string,\n): Promise<Buffer> {\n const body = response.body;\n if (!body) {\n const buffer = Buffer.from(await response.arrayBuffer());\n if (buffer.byteLength > maxBytes) {\n throw new DivoomValidationError(`Image at ${url} exceeds the ${maxBytes}-byte limit.`);\n }\n return buffer;\n }\n\n const reader = body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n total += value.byteLength;\n if (total > maxBytes) {\n await reader.cancel();\n throw new DivoomValidationError(`Image at ${url} exceeds the ${maxBytes}-byte limit.`);\n }\n chunks.push(value);\n }\n }\n return Buffer.concat(chunks);\n}\n\n/** Downloads an `http(s)` image with a timeout and size cap, returning its bytes. */\nasync function fetchImageBytes(url: string, options: LoadImageOptions): Promise<Buffer> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;\n const maxBytes = options.maxBytes ?? DEFAULT_MAX_IMAGE_BYTES;\n const fetchImpl = options.fetch ?? globalThis.fetch;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n // Follow redirects manually so every hop is re-validated as http(s): a\n // default 'follow' would silently chase a 3xx to file://, or to an\n // unexpected host, widening the SSRF surface. The chain is capped.\n let currentUrl = url;\n for (let hop = 0; ; hop += 1) {\n const response = await fetchImpl(currentUrl, {\n signal: controller.signal,\n redirect: 'manual',\n });\n\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get('location');\n await drainBody(response);\n if (!location) {\n throw new DivoomConnectionError(\n `Image redirect from ${currentUrl} is missing a Location header.`,\n );\n }\n if (hop >= MAX_REDIRECTS) {\n throw new DivoomConnectionError(`Too many redirects downloading image from ${url}.`);\n }\n const next = new URL(location, currentUrl).toString();\n if (!/^https?:\\/\\//i.test(next)) {\n throw new DivoomValidationError(\n `Refusing to follow a non-http(s) image redirect to \"${next}\".`,\n );\n }\n currentUrl = next;\n continue;\n }\n\n if (!response.ok) {\n await drainBody(response);\n throw new DivoomConnectionError(\n `Failed to download image from ${currentUrl} (HTTP ${response.status}).`,\n );\n }\n const declared = Number(response.headers.get('content-length'));\n if (Number.isFinite(declared) && declared > maxBytes) {\n await drainBody(response);\n throw new DivoomValidationError(\n `Image at ${currentUrl} is ${declared} bytes, exceeding the ${maxBytes}-byte limit.`,\n );\n }\n return await readCapped(response, maxBytes, currentUrl);\n }\n } catch (error) {\n if (error instanceof DivoomError) throw error;\n if (controller.signal.aborted) {\n throw new DivoomTimeoutError(`GET ${url}`, timeoutMs);\n }\n throw new DivoomConnectionError(`Failed to download image from ${url}.`, { cause: error });\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Resolves an {@link ImageSource} to a `sharp` pipeline. Existing pipelines are\n * cloned so callers can reuse the original safely.\n *\n * @param source - A file path, an `http(s)` URL, raw bytes, or a `sharp` instance.\n * See the module-level note on the URL/path trust model before passing\n * untrusted input.\n * @param options - Download controls for URL sources (timeout, size cap, fetch).\n * @throws {@link DivoomValidationError} for unsupported sources or oversized downloads.\n * @throws {@link DivoomConnectionError} when a URL cannot be downloaded.\n * @throws {@link DivoomTimeoutError} when a download exceeds `timeoutMs`.\n */\nexport async function loadImage(\n source: ImageSource,\n options: LoadImageOptions = {},\n): Promise<Sharp> {\n if (isSharp(source)) {\n return source.clone();\n }\n if (typeof source === 'string') {\n if (/^https?:\\/\\//i.test(source)) {\n return sharp(await fetchImageBytes(source, options));\n }\n return sharp(source);\n }\n if (Buffer.isBuffer(source) || source instanceof Uint8Array) {\n return sharp(source);\n }\n throw new DivoomValidationError(\n 'Unsupported image source. Provide a file path, an http(s) URL, a Buffer/Uint8Array, or a sharp instance.',\n );\n}\n","/**\n * Color helpers: hex⇄RGB conversion, mixing, and accent extraction from\n * artwork (used to tint the now-playing panels).\n *\n * @packageDocumentation\n */\n\nimport { DivoomValidationError } from '../errors';\nimport { loadImage } from './load';\nimport type { ImageSource } from './types';\n\n/** An `[r, g, b]` triple with each channel in `0`–`255`. */\nexport type Rgb = [number, number, number];\n\n/** Parses a `#RGB` or `#RRGGBB` hex string into an {@link Rgb} triple. */\nexport function hexToRgb(hex: string): Rgb {\n const cleaned = hex.replace(/^#/, '').trim();\n const full =\n cleaned.length === 3\n ? cleaned\n .split('')\n .map((c) => c + c)\n .join('')\n : cleaned;\n if (!/^[0-9a-fA-F]{6}$/.test(full)) {\n throw new DivoomValidationError(`Invalid hex color: \"${hex}\".`);\n }\n const value = Number.parseInt(full, 16);\n return [(value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff];\n}\n\n/** Formats an {@link Rgb} triple as an uppercase `#RRGGBB` string. */\nexport function rgbToHex([r, g, b]: Rgb): string {\n const channel = (n: number): string =>\n Math.max(0, Math.min(255, Math.round(n)))\n .toString(16)\n .padStart(2, '0');\n return `#${channel(r)}${channel(g)}${channel(b)}`.toUpperCase();\n}\n\n/** Linearly interpolates between two colors (`t` in `0`–`1`). */\nexport function mixRgb(a: Rgb, b: Rgb, t: number): Rgb {\n const clamped = Math.max(0, Math.min(1, t));\n return [\n a[0] + (b[0] - a[0]) * clamped,\n a[1] + (b[1] - a[1]) * clamped,\n a[2] + (b[2] - a[2]) * clamped,\n ];\n}\n\n/** Scales every channel of a color by `factor`. */\nexport function scaleRgb([r, g, b]: Rgb, factor: number): Rgb {\n return [r * factor, g * factor, b * factor];\n}\n\n/**\n * Extracts a vivid accent color from artwork — the most saturated reasonably\n * bright pixel — for tinting backgrounds and progress bars. Mirrors the proven\n * heuristic from the reference now-playing app.\n *\n * @param source - The artwork to sample.\n * @param fallback - Returned when no saturated pixel is found. Defaults to\n * Spotify green (`#1DB954`).\n */\nexport async function getAccentColor(source: ImageSource, fallback = '#1DB954'): Promise<string> {\n const { data, info } = await (\n await loadImage(source)\n )\n .resize(32, 32, { fit: 'cover' })\n // Composite over black before dropping alpha, so transparent regions don't\n // sample arbitrary RGB and skew the saturation search.\n .flatten({ background: '#000000' })\n .removeAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true });\n\n const channels = info.channels;\n let best: Rgb | null = null;\n let bestSaturation = 0;\n\n for (let i = 0; i + 2 < data.length; i += channels) {\n const r = data[i] ?? 0;\n const g = data[i + 1] ?? 0;\n const b = data[i + 2] ?? 0;\n const max = Math.max(r, g, b);\n const min = Math.min(r, g, b);\n if (max === 0) continue;\n const saturation = (max - min) / max;\n if (saturation > bestSaturation && max > 80) {\n bestSaturation = saturation;\n best = [r, g, b];\n }\n }\n\n return best ? rgbToHex(best) : fallback;\n}\n","/**\n * Frame encoding: resize-and-encode any source to a panel-ready\n * {@link EncodedFrame}, plus a helper for solid-color frames.\n *\n * @packageDocumentation\n */\n\nimport sharp, { type Sharp } from 'sharp';\nimport { PANEL_SIZE } from '../constants';\nimport { assertIntInRange } from '../utils';\nimport { hexToRgb } from './color';\nimport { loadImage } from './load';\nimport type { EncodeOptions, EncodedFrame, EncodePanelOptions, ImageSource } from './types';\n\n/**\n * Encodes an already-sized `sharp` pipeline into an {@link EncodedFrame},\n * applying the SDK's default high-quality JPEG settings (q95, 4:4:4 chroma).\n *\n * @internal Shared by the higher-level helpers; exported for advanced use.\n */\nexport async function finalizeFrame(\n pipeline: Sharp,\n options: EncodeOptions & { size: number },\n): Promise<EncodedFrame> {\n const format = options.format ?? 'jpeg';\n let buffer: Buffer;\n\n if (format === 'png') {\n buffer = await pipeline.png({ compressionLevel: 9 }).toBuffer();\n } else {\n buffer = await pipeline\n .flatten({ background: options.background ?? '#000000' })\n .jpeg({\n quality: options.quality ?? 95,\n chromaSubsampling: options.chromaSubsampling ?? '4:4:4',\n })\n .toBuffer();\n }\n\n return {\n data: buffer.toString('base64'),\n buffer,\n format,\n width: options.size,\n height: options.size,\n };\n}\n\n/**\n * Resizes and encodes any {@link ImageSource} into a single square panel frame.\n *\n * @example\n * ```ts\n * const frame = await encodePanel('./album.jpg'); // 128×128 JPEG, q95\n * await client.draw.sendImage(0, frame.data);\n * ```\n */\nexport async function encodePanel(\n source: ImageSource,\n options: EncodePanelOptions = {},\n): Promise<EncodedFrame> {\n const size = options.size ?? PANEL_SIZE;\n assertIntInRange(size, 1, 4096, 'size');\n const pipeline = (await loadImage(source)).resize(size, size, {\n fit: options.fit ?? 'cover',\n kernel: options.kernel ?? 'lanczos3',\n background: options.background ?? '#000000',\n });\n return finalizeFrame(pipeline, { ...options, size });\n}\n\n/**\n * Produces a solid-color panel frame — handy for clearing a panel or showing a\n * status color.\n *\n * @example\n * ```ts\n * await client.draw.sendImage(1, (await solidFrame('#000000')).data); // blank\n * ```\n */\nexport async function solidFrame(\n color: string,\n options: EncodeOptions = {},\n): Promise<EncodedFrame> {\n const size = options.size ?? PANEL_SIZE;\n assertIntInRange(size, 1, 4096, 'size');\n const [r, g, b] = hexToRgb(color);\n const pipeline = sharp({\n create: { width: size, height: size, channels: 3, background: { r, g, b } },\n });\n return finalizeFrame(pipeline, { ...options, size });\n}\n","/**\n * Album-art styling — the two looks from the reference now-playing app:\n * a crisp \"smooth\" treatment and a chunky \"pixel\" treatment.\n *\n * @packageDocumentation\n */\n\nimport { PANEL_SIZE } from '../constants';\nimport { finalizeFrame } from './encode';\nimport { loadImage } from './load';\nimport type { EncodeOptions, EncodedFrame, ImageSource } from './types';\n\n/** Visual treatment applied by {@link prepareAlbumArt}. */\nexport type AlbumArtStyle = 'smooth' | 'pixel';\n\n/** Options for {@link prepareAlbumArt}. */\nexport interface PrepareAlbumArtOptions extends EncodeOptions {\n /**\n * `\"smooth\"` (default) gently boosts saturation/contrast and unsharp-masks for\n * a clean look; `\"pixel\"` downsamples to 32×32 nearest-neighbour for a retro,\n * blocky aesthetic.\n */\n style?: AlbumArtStyle;\n}\n\n/**\n * Prepares album/cover artwork for a panel, matching the look of the reference\n * Spotify app.\n *\n * @example\n * ```ts\n * const art = await prepareAlbumArt(coverUrl, { style: 'smooth' });\n * await client.draw.sendImage(0, art.data);\n * ```\n */\nexport async function prepareAlbumArt(\n source: ImageSource,\n options: PrepareAlbumArtOptions = {},\n): Promise<EncodedFrame> {\n const size = options.size ?? PANEL_SIZE;\n const style = options.style ?? 'smooth';\n const base = await loadImage(source);\n\n const pipeline =\n style === 'pixel'\n ? base\n .resize(32, 32, { kernel: 'nearest', fit: 'cover' })\n .resize(size, size, { kernel: 'nearest' })\n .modulate({ saturation: 1.5 })\n : base\n .resize(size, size, { kernel: 'lanczos3', fit: 'cover' })\n .modulate({ saturation: 1.15 })\n // contrast ≈ 1.05 around mid-grey: out = 1.05·in − (128·0.05)\n .linear(1.05, -(128 * 0.05))\n .sharpen({ sigma: 1.0 });\n\n return finalizeFrame(pipeline, { ...options, size });\n}\n","/**\n * Split one image across several panels to create a single wide (or tall)\n * canvas spanning the Times Gate.\n *\n * @packageDocumentation\n */\n\nimport sharp from 'sharp';\nimport { PANEL_COUNT, PANEL_SIZE } from '../constants';\nimport { DivoomValidationError } from '../errors';\nimport type { PanelIndex } from '../types/common';\nimport { assertIntInRange, assertPanelIndex } from '../utils';\nimport { finalizeFrame } from './encode';\nimport { loadImage } from './load';\nimport type { EncodeOptions, EncodedFrame, ImageSource } from './types';\n\n/** A frame paired with the panel it belongs on. */\nexport interface PanelFrame {\n /** The target panel index. */\n panel: PanelIndex;\n /** The encoded frame for that panel. */\n frame: EncodedFrame;\n}\n\n/** Options for {@link splitImageAcrossPanels}. */\nexport interface SplitOptions extends EncodeOptions {\n /** Panels to span, in visual order. Defaults to all five (`[0,1,2,3,4]`). */\n panels?: PanelIndex[];\n /**\n * `\"horizontal\"` (default) lays the image out left-to-right across panels;\n * `\"vertical\"` stacks top-to-bottom.\n */\n layout?: 'horizontal' | 'vertical';\n}\n\n/**\n * Slices a single image into one square frame per panel, so a panorama spans\n * the whole device.\n *\n * @example\n * ```ts\n * const tiles = await splitImageAcrossPanels('./wide-banner.png');\n * for (const { panel, frame } of tiles) {\n * await client.draw.sendImage(panel, frame.data);\n * }\n * ```\n */\nexport async function splitImageAcrossPanels(\n source: ImageSource,\n options: SplitOptions = {},\n): Promise<PanelFrame[]> {\n const panels = options.panels ?? [0, 1, 2, 3, 4];\n const size = options.size ?? PANEL_SIZE;\n assertIntInRange(size, 1, 4096, 'size');\n const layout = options.layout ?? 'horizontal';\n // Bound the canvas: at most one tile per physical panel, each a valid index.\n // (The PanelIndex[] type is erased at runtime, so validate the real values.)\n if (panels.length > PANEL_COUNT) {\n throw new DivoomValidationError(\n `splitImageAcrossPanels supports at most ${PANEL_COUNT} panels (received ${panels.length}).`,\n );\n }\n panels.forEach(assertPanelIndex);\n const count = panels.length;\n\n if (count === 0) {\n return [];\n }\n\n const base = await loadImage(source);\n const resized =\n layout === 'horizontal'\n ? base.resize(size * count, size, { fit: 'cover', kernel: 'lanczos3' })\n : base.resize(size, size * count, { fit: 'cover', kernel: 'lanczos3' });\n\n // Encode once losslessly, then extract each tile from the buffer.\n const canvas = await resized.png().toBuffer();\n\n const results: PanelFrame[] = [];\n for (let i = 0; i < count; i += 1) {\n const panel = panels[i];\n if (panel === undefined) continue;\n const tile = sharp(canvas).extract({\n left: layout === 'horizontal' ? i * size : 0,\n top: layout === 'vertical' ? i * size : 0,\n width: size,\n height: size,\n });\n results.push({ panel, frame: await finalizeFrame(tile, { ...options, size }) });\n }\n return results;\n}\n","/**\n * Renders a designed \"now-playing\"-style text panel: a tinted gradient\n * background, an optional eyebrow with a play glyph, an auto-fitted title, a\n * subtitle, and an optional progress bar. Text is rendered with Pango (via\n * sharp) for crisp, properly-wrapped output that doesn't depend on the host's\n * system fonts the way the original Python reference did.\n *\n * @packageDocumentation\n */\n\nimport sharp, { type CreateText, type OverlayOptions, type Sharp } from 'sharp';\nimport { PANEL_SIZE } from '../constants';\nimport { assertIntInRange, assertNonEmptyString } from '../utils';\nimport { hexToRgb, rgbToHex, type Rgb, scaleRgb } from './color';\nimport { finalizeFrame } from './encode';\nimport type { EncodeOptions, EncodedFrame } from './types';\n\n/** Options for {@link renderTextPanel}. */\nexport interface RenderTextPanelOptions extends EncodeOptions {\n /** The headline text (e.g. a track title). Auto-sized to fit. */\n title: string;\n /** Secondary text below the title (e.g. an artist). Optional. */\n subtitle?: string;\n /** Small uppercase label above the title (e.g. `\"NOW PLAYING\"`). Optional. */\n eyebrow?: string;\n /** Accent color (hex) used for the tint, eyebrow, subtitle and bar. Defaults to `#1DB954`. */\n accent?: string;\n /** Progress fraction `0`–`1`. When set, a progress bar is drawn. */\n progress?: number;\n /**\n * Background style: `\"gradient\"` (default, an accent-tinted vertical fade),\n * `\"solid\"` (a flat accent), or any hex string for a custom flat color.\n */\n background?: 'gradient' | 'solid' | string;\n /** Pango font family. Defaults to `\"DejaVu Sans\"`. */\n font?: string;\n}\n\ninterface RenderedText {\n buffer: Buffer;\n width: number;\n height: number;\n}\n\n/** Largest panel edge we'll render, as a sanity bound. */\nconst MAX_PANEL_SIZE = 4096;\n\n/** Escapes a string for safe inclusion in Pango markup. */\nfunction escapeMarkup(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n\n/**\n * Validates and canonicalizes a color to `#RRGGBB` _inside_ the markup builder,\n * so the rendered Pango span can never be poisoned by an unexpected color value\n * regardless of how callers validate upstream. Throws on a non-hex color.\n */\nfunction normalizeHex(color: string): string {\n return rgbToHex(hexToRgb(color));\n}\n\n/** Renders a single run of text with Pango and reports its true rendered size. */\nasync function renderText(opts: {\n text: string;\n family: string;\n size: number;\n color: string;\n bold?: boolean;\n width: number;\n maxHeight?: number;\n align?: 'left' | 'center' | 'right';\n}): Promise<RenderedText> {\n const markup = `<span foreground=\"${normalizeHex(opts.color)}\"${\n opts.bold ? ' weight=\"bold\"' : ''\n }>${escapeMarkup(opts.text)}</span>`;\n\n const textInput: CreateText = {\n text: markup,\n font: `${opts.family} ${opts.size}`,\n rgba: true,\n align: opts.align ?? 'left',\n width: Math.max(1, Math.round(opts.width)),\n wrap: 'word',\n };\n if (opts.maxHeight !== undefined) {\n textInput.height = Math.max(1, Math.round(opts.maxHeight));\n }\n\n const buffer = await sharp({ text: textInput }).png().toBuffer();\n const meta = await sharp(buffer).metadata();\n return { buffer, width: meta.width ?? 0, height: meta.height ?? 0 };\n}\n\n/**\n * Guarantees an overlay fits inside the panel at its placement, shrinking it\n * (preserving aspect ratio) if Pango overflowed the soft wrap width or the\n * vertical budget. Without this, `sharp.composite` throws when an unbreakable\n * token (a long word/URL/CJK run) renders wider than the panel.\n */\nasync function fitOverlay(\n buffer: Buffer,\n top: number,\n left: number,\n size: number,\n): Promise<OverlayOptions> {\n const safeTop = Math.max(0, Math.min(Math.round(top), size - 1));\n const safeLeft = Math.max(0, Math.min(Math.round(left), size - 1));\n const maxW = size - safeLeft;\n const maxH = size - safeTop;\n\n const meta = await sharp(buffer).metadata();\n if ((meta.width ?? 0) <= maxW && (meta.height ?? 0) <= maxH) {\n return { input: buffer, top: safeTop, left: safeLeft };\n }\n const clamped = await sharp(buffer)\n .resize(maxW, maxH, { fit: 'inside', withoutEnlargement: true })\n .png()\n .toBuffer();\n return { input: clamped, top: safeTop, left: safeLeft };\n}\n\n/** Picks the largest title size whose rendered text fits the width and budget. */\nasync function renderFittedTitle(\n text: string,\n family: string,\n width: number,\n budget: number,\n size: number,\n): Promise<RenderedText> {\n const candidates = [0.25, 0.21, 0.18, 0.16, 0.14].map((f) => Math.max(10, Math.round(size * f)));\n for (const fontSize of candidates) {\n const rendered = await renderText({\n text,\n family,\n size: fontSize,\n color: '#FFFFFF',\n bold: true,\n width,\n });\n if (rendered.height <= budget && rendered.width <= width) {\n return rendered;\n }\n }\n // Nothing fit cleanly (e.g. an unbreakable token). Render the smallest size,\n // bounded vertically; `fitOverlay` clamps any residual width overflow.\n const fontSize = Math.max(10, Math.round(size * 0.14));\n return renderText({\n text,\n family,\n size: fontSize,\n color: '#FFFFFF',\n bold: true,\n width,\n maxHeight: Math.max(fontSize, budget),\n });\n}\n\n/** Builds the accent-tinted background pipeline. */\nfunction buildBackground(background: string, accent: Rgb, size: number): Sharp {\n if (background === 'solid') {\n const [r, g, b] = accent;\n return sharp({ create: { width: size, height: size, channels: 3, background: { r, g, b } } });\n }\n if (background !== 'gradient') {\n const [r, g, b] = hexToRgb(background);\n return sharp({ create: { width: size, height: size, channels: 3, background: { r, g, b } } });\n }\n const [tr, tg, tb] = scaleRgb(accent, 0.3).map(Math.round) as [number, number, number];\n const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\">\n <defs><linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0\" stop-color=\"rgb(${tr},${tg},${tb})\"/>\n <stop offset=\"1\" stop-color=\"rgb(8,8,10)\"/>\n </linearGradient></defs>\n <rect width=\"${size}\" height=\"${size}\" fill=\"url(#bg)\"/>\n </svg>`;\n return sharp(Buffer.from(svg)).resize(size, size);\n}\n\n/** Builds the SVG overlay carrying the play glyph and progress bar. */\nfunction buildShapes(opts: {\n size: number;\n margin: number;\n accent: Rgb;\n eyebrowY: number;\n hasEyebrow: boolean;\n progress: number | undefined;\n barY: number;\n}): Buffer {\n const { size, margin, accent, eyebrowY, hasEyebrow, progress, barY } = opts;\n const [r, g, b] = accent;\n const accentCss = `rgb(${r},${g},${b})`;\n const parts: string[] = [];\n\n if (hasEyebrow) {\n const t = Math.round(size * 0.045);\n const y = eyebrowY;\n parts.push(\n `<polygon points=\"${margin},${y} ${margin},${y + t} ${margin + t},${y + t / 2}\" fill=\"${accentCss}\"/>`,\n );\n }\n\n if (progress !== undefined) {\n const x0 = margin;\n const x1 = size - margin;\n const p = Math.max(0, Math.min(1, progress));\n const fx = x0 + (x1 - x0) * p;\n const [tr, tg, tb] = scaleRgb(accent, 0.45).map(Math.round) as [number, number, number];\n parts.push(\n `<line x1=\"${x0}\" y1=\"${barY}\" x2=\"${x1}\" y2=\"${barY}\" stroke=\"rgb(${tr},${tg},${tb})\" stroke-width=\"3\" stroke-linecap=\"round\"/>`,\n `<line x1=\"${x0}\" y1=\"${barY}\" x2=\"${fx}\" y2=\"${barY}\" stroke=\"${accentCss}\" stroke-width=\"3\" stroke-linecap=\"round\"/>`,\n );\n }\n\n return Buffer.from(\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\">${parts.join('')}</svg>`,\n );\n}\n\n/**\n * Renders a designed now-playing-style panel and returns an\n * {@link EncodedFrame} ready for a panel. Overly long titles/subtitles are\n * shrunk to fit rather than overflowing the panel.\n *\n * @example\n * ```ts\n * const panel = await renderTextPanel({\n * eyebrow: 'Now Playing',\n * title: 'Bohemian Rhapsody',\n * subtitle: 'Queen',\n * accent: '#E94F37',\n * progress: 0.42,\n * });\n * await client.draw.sendImage(1, panel.data);\n * ```\n */\nexport async function renderTextPanel(options: RenderTextPanelOptions): Promise<EncodedFrame> {\n const size = options.size ?? PANEL_SIZE;\n assertIntInRange(size, 1, MAX_PANEL_SIZE, 'size');\n assertNonEmptyString(options.title, 'title');\n\n const family = options.font ?? 'DejaVu Sans';\n const accentHex = options.accent ?? '#1DB954';\n const accent = hexToRgb(accentHex);\n const margin = Math.round(size * 0.055);\n const maxWidth = Math.max(1, size - margin * 2);\n\n // Blank optional runs are skipped (Pango rejects empty text).\n const eyebrowText = options.eyebrow?.trim();\n const subtitleText = options.subtitle?.trim();\n\n // Collected as raw placements, then each is clamped to the panel via fitOverlay.\n const overlays: Array<{ buffer: Buffer; top: number; left: number }> = [];\n\n // Eyebrow ---------------------------------------------------------------\n let contentTop = margin;\n const eyebrowY = margin + Math.round(size * 0.015);\n if (eyebrowText) {\n const glyph = Math.round(size * 0.045);\n const eyebrowMaxHeight = Math.max(glyph, Math.round(size * 0.12));\n const eyebrow = await renderText({\n text: eyebrowText.toUpperCase(),\n family,\n size: Math.max(7, Math.round(size * 0.07)),\n color: accentHex,\n bold: true,\n width: Math.max(1, maxWidth - glyph),\n });\n overlays.push({\n buffer: eyebrow.buffer,\n top: eyebrowY,\n left: margin + glyph + Math.round(size * 0.03),\n });\n contentTop =\n eyebrowY +\n Math.min(Math.max(eyebrow.height, glyph), eyebrowMaxHeight) +\n Math.round(size * 0.03);\n }\n // Always leave vertical room for the title.\n contentTop = Math.min(contentTop, size - Math.round(size * 0.2));\n\n // Progress bar + bottom limit ------------------------------------------\n const hasProgress = typeof options.progress === 'number';\n const barY = size - Math.round(size * 0.08);\n const bottomLimit = hasProgress ? barY - Math.round(size * 0.06) : size - margin;\n\n // Subtitle (rendered first so we know its height) ----------------------\n let subtitleTop = bottomLimit;\n if (subtitleText) {\n const subSize = Math.max(9, Math.round(size * 0.11));\n const maxSubHeight = subSize * 2 + 4;\n // Render at the chosen size WITHOUT a CreateText `height`: setting `height`\n // makes Pango auto-fit (scale) the glyphs to fill the box, which balloons\n // short subtitles to ~2.5× their intended size. Cap to ~two lines by\n // cropping the rendered buffer instead.\n let subtitle = await renderText({\n text: subtitleText,\n family,\n size: subSize,\n color: accentHex,\n width: maxWidth,\n });\n if (subtitle.height > maxSubHeight) {\n const cropped = await sharp(subtitle.buffer)\n .extract({ left: 0, top: 0, width: subtitle.width, height: maxSubHeight })\n .png()\n .toBuffer();\n subtitle = { buffer: cropped, width: subtitle.width, height: maxSubHeight };\n }\n subtitleTop = Math.max(contentTop, bottomLimit - subtitle.height);\n overlays.push({ buffer: subtitle.buffer, top: subtitleTop, left: margin });\n }\n\n // Title (fills the remaining vertical budget) --------------------------\n const titleBottom = subtitleText ? subtitleTop : bottomLimit;\n const titleBudget = Math.max(\n Math.round(size * 0.12),\n titleBottom - contentTop - Math.round(size * 0.03),\n );\n const title = await renderFittedTitle(options.title, family, maxWidth, titleBudget, size);\n overlays.push({ buffer: title.buffer, top: contentTop, left: margin });\n\n // Composite: shapes underlay, then every text overlay clamped to the panel.\n const composites: OverlayOptions[] = [\n {\n input: buildShapes({\n size,\n margin,\n accent,\n eyebrowY,\n hasEyebrow: Boolean(eyebrowText),\n progress: options.progress,\n barY,\n }),\n top: 0,\n left: 0,\n },\n ];\n for (const overlay of overlays) {\n composites.push(await fitOverlay(overlay.buffer, overlay.top, overlay.left, size));\n }\n\n const pipeline = buildBackground(options.background ?? 'gradient', accent, size).composite(\n composites,\n );\n return finalizeFrame(pipeline, { ...options, size });\n}\n"]}
|