@ewanc26/og 0.1.1
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 +661 -0
- package/README.md +145 -0
- package/dist/chunk-EPPJ2HBS.js +258 -0
- package/dist/chunk-EPPJ2HBS.js.map +1 -0
- package/dist/fonts/Inter-Bold.ttf +0 -0
- package/dist/fonts/Inter-Regular.ttf +0 -0
- package/dist/index.cjs +663 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +134 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.js +365 -0
- package/dist/index.js.map +1 -0
- package/dist/templates/index.cjs +288 -0
- package/dist/templates/index.cjs.map +1 -0
- package/dist/templates/index.d.cts +183 -0
- package/dist/templates/index.d.ts +183 -0
- package/dist/templates/index.js +15 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/types-Bn2R50Vr.d.cts +60 -0
- package/dist/types-Bn2R50Vr.d.ts +60 -0
- package/fonts/Inter-Bold.ttf +0 -0
- package/fonts/Inter-Regular.ttf +0 -0
- package/package.json +63 -0
- package/src/endpoint.ts +92 -0
- package/src/fonts.ts +121 -0
- package/src/generate.ts +137 -0
- package/src/index.ts +51 -0
- package/src/noise.ts +90 -0
- package/src/png-encoder.ts +101 -0
- package/src/svg.ts +51 -0
- package/src/templates/blog.ts +79 -0
- package/src/templates/default.ts +76 -0
- package/src/templates/index.ts +27 -0
- package/src/templates/profile.ts +102 -0
- package/src/types.ts +92 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getTemplate
|
|
3
|
+
} from "./chunk-EPPJ2HBS.js";
|
|
4
|
+
|
|
5
|
+
// src/generate.ts
|
|
6
|
+
import satori from "satori";
|
|
7
|
+
import { Resvg } from "@resvg/resvg-js";
|
|
8
|
+
|
|
9
|
+
// src/fonts.ts
|
|
10
|
+
import { readFile } from "fs/promises";
|
|
11
|
+
import { dirname, resolve } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
function getModuleDir() {
|
|
14
|
+
if (typeof import.meta !== "undefined" && import.meta.url) {
|
|
15
|
+
return dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
}
|
|
17
|
+
if (typeof __dirname !== "undefined") {
|
|
18
|
+
return __dirname;
|
|
19
|
+
}
|
|
20
|
+
return resolve(process.cwd(), "node_modules/@ewanc26/og/dist");
|
|
21
|
+
}
|
|
22
|
+
function getFontsDir() {
|
|
23
|
+
return resolve(getModuleDir(), "../fonts");
|
|
24
|
+
}
|
|
25
|
+
var BUNDLED_FONTS = {
|
|
26
|
+
get heading() {
|
|
27
|
+
return resolve(getFontsDir(), "Inter-Bold.ttf");
|
|
28
|
+
},
|
|
29
|
+
get body() {
|
|
30
|
+
return resolve(getFontsDir(), "Inter-Regular.ttf");
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
async function loadFonts(config) {
|
|
34
|
+
const headingPath = config?.heading ?? BUNDLED_FONTS.heading;
|
|
35
|
+
const bodyPath = config?.body ?? BUNDLED_FONTS.body;
|
|
36
|
+
const [heading, body] = await Promise.all([
|
|
37
|
+
loadFontFile(headingPath),
|
|
38
|
+
loadFontFile(bodyPath)
|
|
39
|
+
]);
|
|
40
|
+
return { heading, body };
|
|
41
|
+
}
|
|
42
|
+
async function loadFontFile(source) {
|
|
43
|
+
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
44
|
+
const response = await fetch(source);
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(`Failed to load font from URL: ${source}`);
|
|
47
|
+
}
|
|
48
|
+
return response.arrayBuffer();
|
|
49
|
+
}
|
|
50
|
+
const buffer = await readFile(source);
|
|
51
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
52
|
+
}
|
|
53
|
+
function createSatoriFonts(fonts) {
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
name: "Inter",
|
|
57
|
+
data: fonts.heading,
|
|
58
|
+
weight: 700,
|
|
59
|
+
style: "normal"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "Inter",
|
|
63
|
+
data: fonts.body,
|
|
64
|
+
weight: 400,
|
|
65
|
+
style: "normal"
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/noise.ts
|
|
71
|
+
import { generateNoisePixels } from "@ewanc26/noise";
|
|
72
|
+
|
|
73
|
+
// src/png-encoder.ts
|
|
74
|
+
import { deflateSync } from "zlib";
|
|
75
|
+
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
76
|
+
function crc32(data) {
|
|
77
|
+
let crc = 4294967295;
|
|
78
|
+
const table = [];
|
|
79
|
+
for (let n = 0; n < 256; n++) {
|
|
80
|
+
let c = n;
|
|
81
|
+
for (let k = 0; k < 8; k++) {
|
|
82
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
83
|
+
}
|
|
84
|
+
table[n] = c;
|
|
85
|
+
}
|
|
86
|
+
for (let i = 0; i < data.length; i++) {
|
|
87
|
+
crc = table[(crc ^ data[i]) & 255] ^ crc >>> 8;
|
|
88
|
+
}
|
|
89
|
+
return (crc ^ 4294967295) >>> 0;
|
|
90
|
+
}
|
|
91
|
+
function createChunk(type, data) {
|
|
92
|
+
const length = Buffer.alloc(4);
|
|
93
|
+
length.writeUInt32BE(data.length, 0);
|
|
94
|
+
const typeBuffer = Buffer.from(type, "ascii");
|
|
95
|
+
const crcData = Buffer.concat([typeBuffer, data]);
|
|
96
|
+
const crc = Buffer.alloc(4);
|
|
97
|
+
crc.writeUInt32BE(crc32(crcData), 0);
|
|
98
|
+
return Buffer.concat([length, typeBuffer, data, crc]);
|
|
99
|
+
}
|
|
100
|
+
function createIHDR(width, height) {
|
|
101
|
+
const data = Buffer.alloc(13);
|
|
102
|
+
data.writeUInt32BE(width, 0);
|
|
103
|
+
data.writeUInt32BE(height, 4);
|
|
104
|
+
data.writeUInt8(8, 8);
|
|
105
|
+
data.writeUInt8(2, 9);
|
|
106
|
+
data.writeUInt8(0, 10);
|
|
107
|
+
data.writeUInt8(0, 11);
|
|
108
|
+
data.writeUInt8(0, 12);
|
|
109
|
+
return createChunk("IHDR", data);
|
|
110
|
+
}
|
|
111
|
+
function createIDAT(pixels, width, height) {
|
|
112
|
+
const rawData = Buffer.alloc(height * (width * 3 + 1));
|
|
113
|
+
let srcOffset = 0;
|
|
114
|
+
let dstOffset = 0;
|
|
115
|
+
for (let y = 0; y < height; y++) {
|
|
116
|
+
rawData[dstOffset++] = 0;
|
|
117
|
+
for (let x = 0; x < width; x++) {
|
|
118
|
+
const r = pixels[srcOffset++];
|
|
119
|
+
const g = pixels[srcOffset++];
|
|
120
|
+
const b = pixels[srcOffset++];
|
|
121
|
+
srcOffset++;
|
|
122
|
+
rawData[dstOffset++] = r;
|
|
123
|
+
rawData[dstOffset++] = g;
|
|
124
|
+
rawData[dstOffset++] = b;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const compressed = deflateSync(rawData);
|
|
128
|
+
return createChunk("IDAT", compressed);
|
|
129
|
+
}
|
|
130
|
+
function createIEND() {
|
|
131
|
+
return createChunk("IEND", Buffer.alloc(0));
|
|
132
|
+
}
|
|
133
|
+
function encodePNG(pixels, width, height) {
|
|
134
|
+
return Buffer.concat([
|
|
135
|
+
PNG_SIGNATURE,
|
|
136
|
+
createIHDR(width, height),
|
|
137
|
+
createIDAT(pixels, width, height),
|
|
138
|
+
createIEND()
|
|
139
|
+
]);
|
|
140
|
+
}
|
|
141
|
+
var PNGEncoder = {
|
|
142
|
+
encode: encodePNG
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// src/noise.ts
|
|
146
|
+
function generateNoiseDataUrl(options) {
|
|
147
|
+
const { seed, width, height, opacity = 0.4, colorMode = "grayscale" } = options;
|
|
148
|
+
const pixels = generateNoisePixels(width, height, seed, {
|
|
149
|
+
gridSize: 4,
|
|
150
|
+
octaves: 3,
|
|
151
|
+
colorMode: colorMode === "grayscale" ? { type: "grayscale", range: [20, 60] } : { type: "hsl", hueRange: 40, saturationRange: [30, 50], lightnessRange: [30, 50] }
|
|
152
|
+
});
|
|
153
|
+
if (opacity < 1) {
|
|
154
|
+
for (let i = 3; i < pixels.length; i += 4) {
|
|
155
|
+
pixels[i] = Math.round(pixels[i] * opacity);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const pngBuffer = PNGEncoder.encode(pixels, width, height);
|
|
159
|
+
return `data:image/png;base64,${pngBuffer.toString("base64")}`;
|
|
160
|
+
}
|
|
161
|
+
function generateCircleNoiseDataUrl(options) {
|
|
162
|
+
const { seed, size, opacity = 0.15, colorMode = "grayscale" } = options;
|
|
163
|
+
const pixels = generateNoisePixels(size, size, seed, {
|
|
164
|
+
gridSize: 4,
|
|
165
|
+
octaves: 3,
|
|
166
|
+
colorMode: colorMode === "grayscale" ? { type: "grayscale", range: [30, 70] } : { type: "hsl", hueRange: 40, saturationRange: [30, 50], lightnessRange: [30, 50] }
|
|
167
|
+
});
|
|
168
|
+
const center = size / 2;
|
|
169
|
+
const radius = size / 2;
|
|
170
|
+
for (let y = 0; y < size; y++) {
|
|
171
|
+
for (let x = 0; x < size; x++) {
|
|
172
|
+
const idx = (y * size + x) * 4;
|
|
173
|
+
const dx = x - center + 0.5;
|
|
174
|
+
const dy = y - center + 0.5;
|
|
175
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
176
|
+
if (dist > radius) {
|
|
177
|
+
pixels[idx + 3] = 0;
|
|
178
|
+
} else if (dist > radius - 2) {
|
|
179
|
+
const edgeOpacity = (radius - dist) / 2;
|
|
180
|
+
pixels[idx + 3] = Math.round(255 * edgeOpacity * opacity);
|
|
181
|
+
} else {
|
|
182
|
+
pixels[idx + 3] = Math.round(255 * opacity);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const pngBuffer = PNGEncoder.encode(pixels, size, size);
|
|
187
|
+
return `data:image/png;base64,${pngBuffer.toString("base64")}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/types.ts
|
|
191
|
+
var defaultColors = {
|
|
192
|
+
background: "#0f1a15",
|
|
193
|
+
text: "#e8f5e9",
|
|
194
|
+
accent: "#86efac"
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// src/generate.ts
|
|
198
|
+
var OG_WIDTH = 1200;
|
|
199
|
+
var OG_HEIGHT = 630;
|
|
200
|
+
async function generateOgImage(options) {
|
|
201
|
+
const {
|
|
202
|
+
title,
|
|
203
|
+
description,
|
|
204
|
+
siteName,
|
|
205
|
+
image,
|
|
206
|
+
template = "blog",
|
|
207
|
+
colors: colorOverrides,
|
|
208
|
+
fonts: fontConfig,
|
|
209
|
+
noise: noiseConfig,
|
|
210
|
+
noiseSeed,
|
|
211
|
+
width = OG_WIDTH,
|
|
212
|
+
height = OG_HEIGHT,
|
|
213
|
+
debugSvg = false
|
|
214
|
+
} = options;
|
|
215
|
+
const colors = {
|
|
216
|
+
...defaultColors,
|
|
217
|
+
...colorOverrides
|
|
218
|
+
};
|
|
219
|
+
const fonts = await loadFonts(fontConfig);
|
|
220
|
+
const satoriFonts = createSatoriFonts(fonts);
|
|
221
|
+
const noiseEnabled = noiseConfig?.enabled !== false;
|
|
222
|
+
const noiseSeedValue = noiseSeed || noiseConfig?.seed || title;
|
|
223
|
+
const noiseDataUrl = noiseEnabled ? generateNoiseDataUrl({
|
|
224
|
+
seed: noiseSeedValue,
|
|
225
|
+
width,
|
|
226
|
+
height,
|
|
227
|
+
opacity: noiseConfig?.opacity ?? 0.4,
|
|
228
|
+
colorMode: noiseConfig?.colorMode ?? "grayscale"
|
|
229
|
+
}) : void 0;
|
|
230
|
+
const circleNoiseDataUrl = noiseEnabled ? generateCircleNoiseDataUrl({
|
|
231
|
+
seed: `${noiseSeedValue}-circle`,
|
|
232
|
+
size: 200,
|
|
233
|
+
opacity: noiseConfig?.opacity ?? 0.15,
|
|
234
|
+
colorMode: noiseConfig?.colorMode ?? "grayscale"
|
|
235
|
+
}) : void 0;
|
|
236
|
+
const templateFn = getTemplate(template);
|
|
237
|
+
const props = {
|
|
238
|
+
title,
|
|
239
|
+
description,
|
|
240
|
+
siteName,
|
|
241
|
+
image,
|
|
242
|
+
colors,
|
|
243
|
+
noiseDataUrl,
|
|
244
|
+
circleNoiseDataUrl,
|
|
245
|
+
width,
|
|
246
|
+
height
|
|
247
|
+
};
|
|
248
|
+
const element = templateFn(props);
|
|
249
|
+
const svg = await satori(element, {
|
|
250
|
+
width,
|
|
251
|
+
height,
|
|
252
|
+
fonts: satoriFonts
|
|
253
|
+
});
|
|
254
|
+
if (debugSvg) {
|
|
255
|
+
return Buffer.from(svg);
|
|
256
|
+
}
|
|
257
|
+
const resvg = new Resvg(svg, {
|
|
258
|
+
fitTo: {
|
|
259
|
+
mode: "width",
|
|
260
|
+
value: width
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
const pngData = resvg.render();
|
|
264
|
+
return Buffer.from(pngData.asPng());
|
|
265
|
+
}
|
|
266
|
+
async function generateOgImageDataUrl(options) {
|
|
267
|
+
const png = await generateOgImage(options);
|
|
268
|
+
return `data:image/png;base64,${png.toString("base64")}`;
|
|
269
|
+
}
|
|
270
|
+
async function generateOgResponse(options, cacheMaxAge = 3600) {
|
|
271
|
+
const png = await generateOgImage(options);
|
|
272
|
+
return new Response(png, {
|
|
273
|
+
headers: {
|
|
274
|
+
"Content-Type": "image/png",
|
|
275
|
+
"Cache-Control": `public, max-age=${cacheMaxAge}`
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/endpoint.ts
|
|
281
|
+
function createOgEndpoint(options) {
|
|
282
|
+
const {
|
|
283
|
+
siteName,
|
|
284
|
+
defaultTemplate: template = "default",
|
|
285
|
+
colors,
|
|
286
|
+
fonts,
|
|
287
|
+
noise,
|
|
288
|
+
cacheMaxAge = 3600,
|
|
289
|
+
width,
|
|
290
|
+
height
|
|
291
|
+
} = options;
|
|
292
|
+
return async ({ url }) => {
|
|
293
|
+
const title = url.searchParams.get("title");
|
|
294
|
+
const description = url.searchParams.get("description") ?? void 0;
|
|
295
|
+
const image = url.searchParams.get("image") ?? void 0;
|
|
296
|
+
const noiseSeed = url.searchParams.get("seed") ?? void 0;
|
|
297
|
+
if (!title) {
|
|
298
|
+
return new Response("Missing title parameter", { status: 400 });
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
return await generateOgResponse(
|
|
302
|
+
{
|
|
303
|
+
title,
|
|
304
|
+
description,
|
|
305
|
+
siteName,
|
|
306
|
+
image,
|
|
307
|
+
template,
|
|
308
|
+
colors,
|
|
309
|
+
fonts,
|
|
310
|
+
noise,
|
|
311
|
+
noiseSeed,
|
|
312
|
+
width,
|
|
313
|
+
height
|
|
314
|
+
},
|
|
315
|
+
cacheMaxAge
|
|
316
|
+
);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.error("Failed to generate OG image:", error);
|
|
319
|
+
return new Response("Failed to generate image", { status: 500 });
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/svg.ts
|
|
325
|
+
import { Resvg as Resvg2 } from "@resvg/resvg-js";
|
|
326
|
+
function svgToPng(svg, options = {}) {
|
|
327
|
+
const opts = {
|
|
328
|
+
fitTo: options.fitWidth ? { mode: "width", value: options.fitWidth } : void 0,
|
|
329
|
+
background: options.backgroundColor
|
|
330
|
+
};
|
|
331
|
+
const resvg = new Resvg2(svg, opts);
|
|
332
|
+
const rendered = resvg.render();
|
|
333
|
+
return Buffer.from(rendered.asPng());
|
|
334
|
+
}
|
|
335
|
+
function svgToPngDataUrl(svg, options = {}) {
|
|
336
|
+
const png = svgToPng(svg, options);
|
|
337
|
+
return `data:image/png;base64,${png.toString("base64")}`;
|
|
338
|
+
}
|
|
339
|
+
function svgToPngResponse(svg, options = {}, cacheMaxAge = 3600) {
|
|
340
|
+
const png = svgToPng(svg, options);
|
|
341
|
+
return new Response(png, {
|
|
342
|
+
headers: {
|
|
343
|
+
"Content-Type": "image/png",
|
|
344
|
+
"Cache-Control": `public, max-age=${cacheMaxAge}`
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
export {
|
|
349
|
+
BUNDLED_FONTS,
|
|
350
|
+
OG_HEIGHT,
|
|
351
|
+
OG_WIDTH,
|
|
352
|
+
createOgEndpoint,
|
|
353
|
+
createSatoriFonts,
|
|
354
|
+
defaultColors,
|
|
355
|
+
generateCircleNoiseDataUrl,
|
|
356
|
+
generateNoiseDataUrl,
|
|
357
|
+
generateOgImage,
|
|
358
|
+
generateOgImageDataUrl,
|
|
359
|
+
generateOgResponse,
|
|
360
|
+
loadFonts,
|
|
361
|
+
svgToPng,
|
|
362
|
+
svgToPngDataUrl,
|
|
363
|
+
svgToPngResponse
|
|
364
|
+
};
|
|
365
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/generate.ts","../src/fonts.ts","../src/noise.ts","../src/png-encoder.ts","../src/types.ts","../src/endpoint.ts","../src/svg.ts"],"sourcesContent":["/**\n * Core OG image generation.\n * Uses satori for JSX-to-SVG and resvg-js for SVG-to-PNG.\n */\n\nimport satori from 'satori'\nimport { Resvg } from '@resvg/resvg-js'\nimport { loadFonts, createSatoriFonts } from './fonts.js'\nimport { generateNoiseDataUrl, generateCircleNoiseDataUrl } from './noise.js'\nimport { getTemplate } from './templates/index.js'\nimport { defaultColors } from './types.js'\nimport type {\n\tOgGenerateOptions,\n\tOgColorConfig,\n\tOgTemplateProps,\n} from './types.js'\n\n// Standard OG image dimensions\nexport const OG_WIDTH = 1200\nexport const OG_HEIGHT = 630\n\n/**\n * Generate an OG image as PNG Buffer.\n */\nexport async function generateOgImage(options: OgGenerateOptions): Promise<Buffer> {\n\tconst {\n\t\ttitle,\n\t\tdescription,\n\t\tsiteName,\n\t\timage,\n\t\ttemplate = 'blog',\n\t\tcolors: colorOverrides,\n\t\tfonts: fontConfig,\n\t\tnoise: noiseConfig,\n\t\tnoiseSeed,\n\t\twidth = OG_WIDTH,\n\t\theight = OG_HEIGHT,\n\t\tdebugSvg = false,\n\t} = options\n\n\t// Merge colours\n\tconst colors: OgColorConfig = {\n\t\t...defaultColors,\n\t\t...colorOverrides,\n\t}\n\n\t// Load fonts\n\tconst fonts = await loadFonts(fontConfig)\n\tconst satoriFonts = createSatoriFonts(fonts)\n\n\t// Generate noise background\n\tconst noiseEnabled = noiseConfig?.enabled !== false\n\tconst noiseSeedValue = noiseSeed || noiseConfig?.seed || title\n\tconst noiseDataUrl = noiseEnabled\n\t\t? generateNoiseDataUrl({\n\t\t\t\tseed: noiseSeedValue,\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\topacity: noiseConfig?.opacity ?? 0.4,\n\t\t\t\tcolorMode: noiseConfig?.colorMode ?? 'grayscale',\n\t\t\t})\n\t\t: undefined\n\n\t// Generate circular noise decoration\n\tconst circleNoiseDataUrl = noiseEnabled\n\t\t? generateCircleNoiseDataUrl({\n\t\t\t\tseed: `${noiseSeedValue}-circle`,\n\t\t\t\tsize: 200,\n\t\t\t\topacity: noiseConfig?.opacity ?? 0.15,\n\t\t\t\tcolorMode: noiseConfig?.colorMode ?? 'grayscale',\n\t\t\t})\n\t\t: undefined\n\n\t// Get template function\n\tconst templateFn = getTemplate(template as Parameters<typeof getTemplate>[0])\n\n\t// Build template props\n\tconst props: OgTemplateProps = {\n\t\ttitle,\n\t\tdescription,\n\t\tsiteName,\n\t\timage,\n\t\tcolors,\n\t\tnoiseDataUrl,\n\t\tcircleNoiseDataUrl,\n\t\twidth,\n\t\theight,\n\t}\n\n\t// Render template to Satori-compatible structure\n\tconst element = templateFn(props)\n\n\t// Generate SVG with satori\n\tconst svg = await satori(element as Parameters<typeof satori>[0], {\n\t\twidth,\n\t\theight,\n\t\tfonts: satoriFonts,\n\t})\n\n\t// Debug: return SVG string\n\tif (debugSvg) {\n\t\treturn Buffer.from(svg)\n\t}\n\n\t// Convert SVG to PNG with resvg-js\n\tconst resvg = new Resvg(svg, {\n\t\tfitTo: {\n\t\t\tmode: 'width',\n\t\t\tvalue: width,\n\t\t},\n\t})\n\tconst pngData = resvg.render()\n\n\treturn Buffer.from(pngData.asPng())\n}\n\n/**\n * Generate OG image and return as base64 data URL.\n */\nexport async function generateOgImageDataUrl(options: OgGenerateOptions): Promise<string> {\n\tconst png = await generateOgImage(options)\n\treturn `data:image/png;base64,${png.toString('base64')}`\n}\n\n/**\n * Generate OG image and return as Response (for SvelteKit endpoints).\n */\nexport async function generateOgResponse(options: OgGenerateOptions, cacheMaxAge = 3600): Promise<Response> {\n\tconst png = await generateOgImage(options)\n\n\treturn new Response(png, {\n\t\theaders: {\n\t\t\t'Content-Type': 'image/png',\n\t\t\t'Cache-Control': `public, max-age=${cacheMaxAge}`,\n\t\t},\n\t})\n}\n","/**\n * @ewanc26/og fonts\n *\n * Font loading utilities. Bundles Inter font by default.\n */\n\nimport { readFile } from 'node:fs/promises'\nimport { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { OgFontConfig } from './types.js'\n\n// Declare __dirname for CJS contexts (injected by bundlers)\ndeclare const __dirname: string | undefined\n\n// ─── Paths ────────────────────────────────────────────────────────────────────\n\n/**\n * Get the directory of the current module.\n * Works in both ESM and bundled contexts.\n */\nfunction getModuleDir(): string {\n\t// ESM context\n\tif (typeof import.meta !== 'undefined' && import.meta.url) {\n\t\treturn dirname(fileURLToPath(import.meta.url))\n\t}\n\t// Bundled CJS context - __dirname is injected by bundlers\n\tif (typeof __dirname !== 'undefined') {\n\t\treturn __dirname\n\t}\n\t// Fallback\n\treturn resolve(process.cwd(), 'node_modules/@ewanc26/og/dist')\n}\n\n/**\n * Resolve the fonts directory relative to the installed package.\n */\nfunction getFontsDir(): string {\n\treturn resolve(getModuleDir(), '../fonts')\n}\n\n/**\n * Resolve bundled font paths. Uses getters to defer resolution until runtime.\n */\nexport const BUNDLED_FONTS = {\n\tget heading() {\n\t\treturn resolve(getFontsDir(), 'Inter-Bold.ttf')\n\t},\n\tget body() {\n\t\treturn resolve(getFontsDir(), 'Inter-Regular.ttf')\n\t},\n} as const\n\n// ─── Font Loading ──────────────────────────────────────────────────────────────\n\nexport interface LoadedFonts {\n\theading: ArrayBuffer\n\tbody: ArrayBuffer\n}\n\n/**\n * Load fonts from config, falling back to bundled DM Sans.\n */\nexport async function loadFonts(config?: OgFontConfig): Promise<LoadedFonts> {\n\tconst headingPath = config?.heading ?? BUNDLED_FONTS.heading\n\tconst bodyPath = config?.body ?? BUNDLED_FONTS.body\n\n\tconst [heading, body] = await Promise.all([\n\t\tloadFontFile(headingPath),\n\t\tloadFontFile(bodyPath),\n\t])\n\n\treturn { heading, body }\n}\n\n/**\n * Load a font from file path or URL.\n */\nasync function loadFontFile(source: string): Promise<ArrayBuffer> {\n\t// Handle URLs\n\tif (source.startsWith('http://') || source.startsWith('https://')) {\n\t\tconst response = await fetch(source)\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Failed to load font from URL: ${source}`)\n\t\t}\n\t\treturn response.arrayBuffer()\n\t}\n\n\t// Handle file paths\n\tconst buffer = await readFile(source)\n\treturn buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)\n}\n\n// ─── Font Registration for Satori ─────────────────────────────────────────────\n\nexport type SatoriFontConfig = {\n\tname: string\n\tdata: ArrayBuffer\n\tweight: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900\n\tstyle: 'normal' | 'italic'\n}\n\n/**\n * Create Satori-compatible font config from loaded fonts.\n * Uses Inter font family with weight 700 for headings and 400 for body.\n */\nexport function createSatoriFonts(fonts: LoadedFonts): SatoriFontConfig[] {\n\treturn [\n\t\t{\n\t\t\tname: 'Inter',\n\t\t\tdata: fonts.heading,\n\t\t\tweight: 700,\n\t\t\tstyle: 'normal',\n\t\t},\n\t\t{\n\t\t\tname: 'Inter',\n\t\t\tdata: fonts.body,\n\t\t\tweight: 400,\n\t\t\tstyle: 'normal',\n\t\t},\n\t]\n}\n","/**\n * @ewanc26/og noise\n */\n\nimport { generateNoisePixels } from '@ewanc26/noise'\nimport { PNGEncoder } from './png-encoder.js'\nimport type { OgNoiseConfig } from './types.js'\n\nexport interface NoiseOptions {\n\tseed: string\n\twidth: number\n\theight: number\n\topacity?: number\n\tcolorMode?: OgNoiseConfig['colorMode']\n}\n\nexport interface CircleNoiseOptions {\n\tseed: string\n\tsize: number\n\topacity?: number\n\tcolorMode?: OgNoiseConfig['colorMode']\n}\n\n/**\n * Generate a noise PNG as a data URL.\n */\nexport function generateNoiseDataUrl(options: NoiseOptions): string {\n\tconst { seed, width, height, opacity = 0.4, colorMode = 'grayscale' } = options\n\n\tconst pixels = generateNoisePixels(width, height, seed, {\n\t\tgridSize: 4,\n\t\toctaves: 3,\n\t\tcolorMode: colorMode === 'grayscale'\n\t\t\t? { type: 'grayscale', range: [20, 60] }\n\t\t\t: { type: 'hsl', hueRange: 40, saturationRange: [30, 50], lightnessRange: [30, 50] }\n\t})\n\n\tif (opacity < 1) {\n\t\tfor (let i = 3; i < pixels.length; i += 4) {\n\t\t\tpixels[i] = Math.round(pixels[i] * opacity)\n\t\t}\n\t}\n\n\tconst pngBuffer = PNGEncoder.encode(pixels, width, height)\n\treturn `data:image/png;base64,${pngBuffer.toString('base64')}`\n}\n\n/**\n * Generate a circular noise PNG as a data URL.\n * Creates a square image with circular transparency mask.\n */\nexport function generateCircleNoiseDataUrl(options: CircleNoiseOptions): string {\n\tconst { seed, size, opacity = 0.15, colorMode = 'grayscale' } = options\n\n\tconst pixels = generateNoisePixels(size, size, seed, {\n\t\tgridSize: 4,\n\t\toctaves: 3,\n\t\tcolorMode: colorMode === 'grayscale'\n\t\t\t? { type: 'grayscale', range: [30, 70] }\n\t\t\t: { type: 'hsl', hueRange: 40, saturationRange: [30, 50], lightnessRange: [30, 50] }\n\t})\n\n\tconst center = size / 2\n\tconst radius = size / 2\n\n\t// Apply circular mask\n\tfor (let y = 0; y < size; y++) {\n\t\tfor (let x = 0; x < size; x++) {\n\t\t\tconst idx = (y * size + x) * 4\n\t\t\tconst dx = x - center + 0.5\n\t\t\tconst dy = y - center + 0.5\n\t\t\tconst dist = Math.sqrt(dx * dx + dy * dy)\n\n\t\t\tif (dist > radius) {\n\t\t\t\t// Outside circle - fully transparent\n\t\t\t\tpixels[idx + 3] = 0\n\t\t\t} else if (dist > radius - 2) {\n\t\t\t\t// Anti-alias edge\n\t\t\t\tconst edgeOpacity = (radius - dist) / 2\n\t\t\t\tpixels[idx + 3] = Math.round(255 * edgeOpacity * opacity)\n\t\t\t} else {\n\t\t\t\t// Inside circle - apply opacity\n\t\t\t\tpixels[idx + 3] = Math.round(255 * opacity)\n\t\t\t}\n\t\t}\n\t}\n\n\tconst pngBuffer = PNGEncoder.encode(pixels, size, size)\n\treturn `data:image/png;base64,${pngBuffer.toString('base64')}`\n}\n","/**\n * Minimal PNG encoder for noise backgrounds.\n * Uses node:zlib for deflate compression.\n */\n\nimport { deflateSync } from 'node:zlib'\n\nconst PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])\n\nfunction crc32(data: Buffer): number {\n\tlet crc = 0xffffffff\n\tconst table: number[] = []\n\n\t// Build CRC table\n\tfor (let n = 0; n < 256; n++) {\n\t\tlet c = n\n\t\tfor (let k = 0; k < 8; k++) {\n\t\t\tc = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1\n\t\t}\n\t\ttable[n] = c\n\t}\n\n\t// Calculate CRC\n\tfor (let i = 0; i < data.length; i++) {\n\t\tcrc = table[(crc ^ data[i]) & 0xff] ^ (crc >>> 8)\n\t}\n\n\treturn (crc ^ 0xffffffff) >>> 0\n}\n\nfunction createChunk(type: string, data: Buffer): Buffer {\n\tconst length = Buffer.alloc(4)\n\tlength.writeUInt32BE(data.length, 0)\n\n\tconst typeBuffer = Buffer.from(type, 'ascii')\n\tconst crcData = Buffer.concat([typeBuffer, data])\n\tconst crc = Buffer.alloc(4)\n\tcrc.writeUInt32BE(crc32(crcData), 0)\n\n\treturn Buffer.concat([length, typeBuffer, data, crc])\n}\n\nfunction createIHDR(width: number, height: number): Buffer {\n\tconst data = Buffer.alloc(13)\n\tdata.writeUInt32BE(width, 0) // Width\n\tdata.writeUInt32BE(height, 4) // Height\n\tdata.writeUInt8(8, 8) // Bit depth: 8 bits\n\tdata.writeUInt8(2, 9) // Colour type: 2 (RGB)\n\tdata.writeUInt8(0, 10) // Compression method\n\tdata.writeUInt8(0, 11) // Filter method\n\tdata.writeUInt8(0, 12) // Interlace method\n\n\treturn createChunk('IHDR', data)\n}\n\nfunction createIDAT(pixels: Uint8ClampedArray, width: number, height: number): Buffer {\n\t// Apply filter (none filter = 0) per row\n\tconst rawData = Buffer.alloc(height * (width * 3 + 1))\n\n\tlet srcOffset = 0\n\tlet dstOffset = 0\n\n\tfor (let y = 0; y < height; y++) {\n\t\trawData[dstOffset++] = 0 // Filter type: none\n\t\tfor (let x = 0; x < width; x++) {\n\t\t\tconst r = pixels[srcOffset++]\n\t\t\tconst g = pixels[srcOffset++]\n\t\t\tconst b = pixels[srcOffset++]\n\t\t\tsrcOffset++ // Skip alpha\n\t\t\trawData[dstOffset++] = r\n\t\t\trawData[dstOffset++] = g\n\t\t\trawData[dstOffset++] = b\n\t\t}\n\t}\n\n\tconst compressed = deflateSync(rawData)\n\treturn createChunk('IDAT', compressed)\n}\n\nfunction createIEND(): Buffer {\n\treturn createChunk('IEND', Buffer.alloc(0))\n}\n\n/**\n * Encode raw RGBA pixel data as a PNG Buffer.\n */\nexport function encodePNG(pixels: Uint8ClampedArray, width: number, height: number): Buffer {\n\treturn Buffer.concat([\n\t\tPNG_SIGNATURE,\n\t\tcreateIHDR(width, height),\n\t\tcreateIDAT(pixels, width, height),\n\t\tcreateIEND(),\n\t])\n}\n\n/**\n * PNGEncoder namespace for cleaner imports.\n */\nexport const PNGEncoder = {\n\tencode: encodePNG,\n}\n","/**\n * @ewanc26/og types\n */\n\n// ─── Colour Configuration ─────────────────────────────────────────────────────\n\nexport interface OgColorConfig {\n\t/** Background color (very dark). @default '#0f1a15' */\n\tbackground: string\n\t/** Primary text color. @default '#e8f5e9' */\n\ttext: string\n\t/** Secondary/accent text (mint). @default '#86efac' */\n\taccent: string\n}\n\nexport const defaultColors: OgColorConfig = {\n\tbackground: '#0f1a15',\n\ttext: '#e8f5e9',\n\taccent: '#86efac',\n}\n\n// ─── Font Configuration ───────────────────────────────────────────────────────\n\nexport interface OgFontConfig {\n\theading?: string\n\tbody?: string\n}\n\n// ─── Noise Configuration ──────────────────────────────────────────────────────\n\nexport interface OgNoiseConfig {\n\tenabled?: boolean\n\tseed?: string\n\topacity?: number\n\tcolorMode?: 'grayscale' | 'hsl'\n}\n\n// ─── Template Props ────────────────────────────────────────────────────────────\n\nexport interface OgTemplateProps {\n\ttitle: string\n\tdescription?: string\n\tsiteName: string\n\timage?: string\n\tcolors: OgColorConfig\n\tnoiseDataUrl?: string\n\tcircleNoiseDataUrl?: string\n\twidth: number\n\theight: number\n}\n\nexport type OgTemplate = (props: OgTemplateProps) => unknown\n\n// ─── Generation Options ───────────────────────────────────────────────────────\n\nexport interface OgGenerateOptions {\n\ttitle: string\n\tdescription?: string\n\tsiteName: string\n\timage?: string\n\ttemplate?: 'blog' | 'profile' | 'default' | OgTemplate\n\tcolors?: Partial<OgColorConfig>\n\tfonts?: OgFontConfig\n\tnoise?: OgNoiseConfig\n\tnoiseSeed?: string\n\twidth?: number\n\theight?: number\n\tdebugSvg?: boolean\n}\n\n// ─── SvelteKit Endpoint Options ───────────────────────────────────────────────\n\nexport interface OgEndpointOptions {\n\tsiteName: string\n\tdefaultTemplate?: 'blog' | 'profile' | 'default' | OgTemplate\n\tcolors?: Partial<OgColorConfig>\n\tfonts?: OgFontConfig\n\tnoise?: OgNoiseConfig\n\tcacheMaxAge?: number\n\twidth?: number\n\theight?: number\n}\n\n// ─── Internal Types ────────────────────────────────────────────────────────────\n\nexport interface InternalGenerateContext {\n\twidth: number\n\theight: number\n\tfonts: { heading: ArrayBuffer; body: ArrayBuffer }\n\tcolors: OgColorConfig\n\tnoiseDataUrl?: string\n}\n","/**\n * SvelteKit endpoint helpers.\n */\n\nimport { generateOgResponse } from './generate.js'\nimport type { OgEndpointOptions, OgTemplate } from './types.js'\n\n/**\n * Create a SvelteKit GET handler for OG image generation.\n *\n * @example\n * ```ts\n * // src/routes/og/[title]/+server.ts\n * import { createOgEndpoint } from '@ewanc26/og';\n *\n * export const GET = createOgEndpoint({\n * siteName: 'ewancroft.uk',\n * defaultTemplate: 'blog',\n * });\n * ```\n *\n * The endpoint expects query parameters:\n * - `title` (required): Page title\n * - `description`: Optional description\n * - `image`: Optional avatar/logo URL\n * - `seed`: Optional noise seed\n */\nexport function createOgEndpoint(options: OgEndpointOptions) {\n\tconst {\n\t\tsiteName,\n\t\tdefaultTemplate: template = 'default',\n\t\tcolors,\n\t\tfonts,\n\t\tnoise,\n\t\tcacheMaxAge = 3600,\n\t\twidth,\n\t\theight,\n\t} = options\n\n\treturn async ({ url }: { url: URL }) => {\n\t\tconst title = url.searchParams.get('title')\n\t\tconst description = url.searchParams.get('description') ?? undefined\n\t\tconst image = url.searchParams.get('image') ?? undefined\n\t\tconst noiseSeed = url.searchParams.get('seed') ?? undefined\n\n\t\tif (!title) {\n\t\t\treturn new Response('Missing title parameter', { status: 400 })\n\t\t}\n\n\t\ttry {\n\t\t\treturn await generateOgResponse(\n\t\t\t\t{\n\t\t\t\t\ttitle,\n\t\t\t\t\tdescription,\n\t\t\t\t\tsiteName,\n\t\t\t\t\timage,\n\t\t\t\t\ttemplate: template as OgTemplate,\n\t\t\t\t\tcolors,\n\t\t\t\t\tfonts,\n\t\t\t\t\tnoise,\n\t\t\t\t\tnoiseSeed,\n\t\t\t\t\twidth,\n\t\t\t\t\theight,\n\t\t\t\t},\n\t\t\t\tcacheMaxAge\n\t\t\t)\n\t\t} catch (error) {\n\t\t\tconsole.error('Failed to generate OG image:', error)\n\t\t\treturn new Response('Failed to generate image', { status: 500 })\n\t\t}\n\t}\n}\n\n/**\n * Create a typed OG image URL for use in meta tags.\n */\nexport function createOgImageUrl(\n\tbaseUrl: string,\n\tparams: {\n\t\ttitle: string\n\t\tdescription?: string\n\t\timage?: string\n\t\tseed?: string\n\t}\n): string {\n\tconst url = new URL(baseUrl)\n\turl.searchParams.set('title', params.title)\n\tif (params.description) url.searchParams.set('description', params.description)\n\tif (params.image) url.searchParams.set('image', params.image)\n\tif (params.seed) url.searchParams.set('seed', params.seed)\n\treturn url.toString()\n}\n","/**\n * SVG to PNG conversion using @resvg/resvg-js.\n */\n\nimport { Resvg } from '@resvg/resvg-js'\n\nexport interface SvgToPngOptions {\n\t/** Scale to fit width in pixels */\n\tfitWidth?: number\n\t/** Background colour for transparent areas */\n\tbackgroundColor?: string\n}\n\n/**\n * Convert an SVG string to PNG Buffer.\n */\nexport function svgToPng(svg: string, options: SvgToPngOptions = {}): Buffer {\n\tconst opts = {\n\t\tfitTo: options.fitWidth\n\t\t\t? { mode: 'width' as const, value: options.fitWidth }\n\t\t\t: undefined,\n\t\tbackground: options.backgroundColor,\n\t}\n\n\tconst resvg = new Resvg(svg, opts)\n\tconst rendered = resvg.render()\n\n\treturn Buffer.from(rendered.asPng())\n}\n\n/**\n * Convert an SVG string to PNG data URL.\n */\nexport function svgToPngDataUrl(svg: string, options: SvgToPngOptions = {}): string {\n\tconst png = svgToPng(svg, options)\n\treturn `data:image/png;base64,${png.toString('base64')}`\n}\n\n/**\n * Convert an SVG string to PNG Response (for SvelteKit endpoints).\n */\nexport function svgToPngResponse(svg: string, options: SvgToPngOptions = {}, cacheMaxAge = 3600): Response {\n\tconst png = svgToPng(svg, options)\n\n\treturn new Response(png, {\n\t\theaders: {\n\t\t\t'Content-Type': 'image/png',\n\t\t\t'Cache-Control': `public, max-age=${cacheMaxAge}`,\n\t\t},\n\t})\n}\n"],"mappings":";;;;;AAKA,OAAO,YAAY;AACnB,SAAS,aAAa;;;ACAtB,SAAS,gBAAgB;AACzB,SAAS,SAAS,eAAe;AACjC,SAAS,qBAAqB;AAY9B,SAAS,eAAuB;AAE/B,MAAI,OAAO,gBAAgB,eAAe,YAAY,KAAK;AAC1D,WAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AAAA,EAC9C;AAEA,MAAI,OAAO,cAAc,aAAa;AACrC,WAAO;AAAA,EACR;AAEA,SAAO,QAAQ,QAAQ,IAAI,GAAG,+BAA+B;AAC9D;AAKA,SAAS,cAAsB;AAC9B,SAAO,QAAQ,aAAa,GAAG,UAAU;AAC1C;AAKO,IAAM,gBAAgB;AAAA,EAC5B,IAAI,UAAU;AACb,WAAO,QAAQ,YAAY,GAAG,gBAAgB;AAAA,EAC/C;AAAA,EACA,IAAI,OAAO;AACV,WAAO,QAAQ,YAAY,GAAG,mBAAmB;AAAA,EAClD;AACD;AAYA,eAAsB,UAAU,QAA6C;AAC5E,QAAM,cAAc,QAAQ,WAAW,cAAc;AACrD,QAAM,WAAW,QAAQ,QAAQ,cAAc;AAE/C,QAAM,CAAC,SAAS,IAAI,IAAI,MAAM,QAAQ,IAAI;AAAA,IACzC,aAAa,WAAW;AAAA,IACxB,aAAa,QAAQ;AAAA,EACtB,CAAC;AAED,SAAO,EAAE,SAAS,KAAK;AACxB;AAKA,eAAe,aAAa,QAAsC;AAEjE,MAAI,OAAO,WAAW,SAAS,KAAK,OAAO,WAAW,UAAU,GAAG;AAClE,UAAM,WAAW,MAAM,MAAM,MAAM;AACnC,QAAI,CAAC,SAAS,IAAI;AACjB,YAAM,IAAI,MAAM,iCAAiC,MAAM,EAAE;AAAA,IAC1D;AACA,WAAO,SAAS,YAAY;AAAA,EAC7B;AAGA,QAAM,SAAS,MAAM,SAAS,MAAM;AACpC,SAAO,OAAO,OAAO,MAAM,OAAO,YAAY,OAAO,aAAa,OAAO,UAAU;AACpF;AAeO,SAAS,kBAAkB,OAAwC;AACzE,SAAO;AAAA,IACN;AAAA,MACC,MAAM;AAAA,MACN,MAAM,MAAM;AAAA,MACZ,QAAQ;AAAA,MACR,OAAO;AAAA,IACR;AAAA,IACA;AAAA,MACC,MAAM;AAAA,MACN,MAAM,MAAM;AAAA,MACZ,QAAQ;AAAA,MACR,OAAO;AAAA,IACR;AAAA,EACD;AACD;;;ACpHA,SAAS,2BAA2B;;;ACCpC,SAAS,mBAAmB;AAE5B,IAAM,gBAAgB,OAAO,KAAK,CAAC,KAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,EAAI,CAAC;AAElF,SAAS,MAAM,MAAsB;AACpC,MAAI,MAAM;AACV,QAAM,QAAkB,CAAC;AAGzB,WAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC7B,QAAI,IAAI;AACR,aAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC3B,UAAI,IAAI,IAAI,aAAc,MAAM,IAAK,MAAM;AAAA,IAC5C;AACA,UAAM,CAAC,IAAI;AAAA,EACZ;AAGA,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACrC,UAAM,OAAO,MAAM,KAAK,CAAC,KAAK,GAAI,IAAK,QAAQ;AAAA,EAChD;AAEA,UAAQ,MAAM,gBAAgB;AAC/B;AAEA,SAAS,YAAY,MAAc,MAAsB;AACxD,QAAM,SAAS,OAAO,MAAM,CAAC;AAC7B,SAAO,cAAc,KAAK,QAAQ,CAAC;AAEnC,QAAM,aAAa,OAAO,KAAK,MAAM,OAAO;AAC5C,QAAM,UAAU,OAAO,OAAO,CAAC,YAAY,IAAI,CAAC;AAChD,QAAM,MAAM,OAAO,MAAM,CAAC;AAC1B,MAAI,cAAc,MAAM,OAAO,GAAG,CAAC;AAEnC,SAAO,OAAO,OAAO,CAAC,QAAQ,YAAY,MAAM,GAAG,CAAC;AACrD;AAEA,SAAS,WAAW,OAAe,QAAwB;AAC1D,QAAM,OAAO,OAAO,MAAM,EAAE;AAC5B,OAAK,cAAc,OAAO,CAAC;AAC3B,OAAK,cAAc,QAAQ,CAAC;AAC5B,OAAK,WAAW,GAAG,CAAC;AACpB,OAAK,WAAW,GAAG,CAAC;AACpB,OAAK,WAAW,GAAG,EAAE;AACrB,OAAK,WAAW,GAAG,EAAE;AACrB,OAAK,WAAW,GAAG,EAAE;AAErB,SAAO,YAAY,QAAQ,IAAI;AAChC;AAEA,SAAS,WAAW,QAA2B,OAAe,QAAwB;AAErF,QAAM,UAAU,OAAO,MAAM,UAAU,QAAQ,IAAI,EAAE;AAErD,MAAI,YAAY;AAChB,MAAI,YAAY;AAEhB,WAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAChC,YAAQ,WAAW,IAAI;AACvB,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC/B,YAAM,IAAI,OAAO,WAAW;AAC5B,YAAM,IAAI,OAAO,WAAW;AAC5B,YAAM,IAAI,OAAO,WAAW;AAC5B;AACA,cAAQ,WAAW,IAAI;AACvB,cAAQ,WAAW,IAAI;AACvB,cAAQ,WAAW,IAAI;AAAA,IACxB;AAAA,EACD;AAEA,QAAM,aAAa,YAAY,OAAO;AACtC,SAAO,YAAY,QAAQ,UAAU;AACtC;AAEA,SAAS,aAAqB;AAC7B,SAAO,YAAY,QAAQ,OAAO,MAAM,CAAC,CAAC;AAC3C;AAKO,SAAS,UAAU,QAA2B,OAAe,QAAwB;AAC3F,SAAO,OAAO,OAAO;AAAA,IACpB;AAAA,IACA,WAAW,OAAO,MAAM;AAAA,IACxB,WAAW,QAAQ,OAAO,MAAM;AAAA,IAChC,WAAW;AAAA,EACZ,CAAC;AACF;AAKO,IAAM,aAAa;AAAA,EACzB,QAAQ;AACT;;;AD1EO,SAAS,qBAAqB,SAA+B;AACnE,QAAM,EAAE,MAAM,OAAO,QAAQ,UAAU,KAAK,YAAY,YAAY,IAAI;AAExE,QAAM,SAAS,oBAAoB,OAAO,QAAQ,MAAM;AAAA,IACvD,UAAU;AAAA,IACV,SAAS;AAAA,IACT,WAAW,cAAc,cACtB,EAAE,MAAM,aAAa,OAAO,CAAC,IAAI,EAAE,EAAE,IACrC,EAAE,MAAM,OAAO,UAAU,IAAI,iBAAiB,CAAC,IAAI,EAAE,GAAG,gBAAgB,CAAC,IAAI,EAAE,EAAE;AAAA,EACrF,CAAC;AAED,MAAI,UAAU,GAAG;AAChB,aAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,GAAG;AAC1C,aAAO,CAAC,IAAI,KAAK,MAAM,OAAO,CAAC,IAAI,OAAO;AAAA,IAC3C;AAAA,EACD;AAEA,QAAM,YAAY,WAAW,OAAO,QAAQ,OAAO,MAAM;AACzD,SAAO,yBAAyB,UAAU,SAAS,QAAQ,CAAC;AAC7D;AAMO,SAAS,2BAA2B,SAAqC;AAC/E,QAAM,EAAE,MAAM,MAAM,UAAU,MAAM,YAAY,YAAY,IAAI;AAEhE,QAAM,SAAS,oBAAoB,MAAM,MAAM,MAAM;AAAA,IACpD,UAAU;AAAA,IACV,SAAS;AAAA,IACT,WAAW,cAAc,cACtB,EAAE,MAAM,aAAa,OAAO,CAAC,IAAI,EAAE,EAAE,IACrC,EAAE,MAAM,OAAO,UAAU,IAAI,iBAAiB,CAAC,IAAI,EAAE,GAAG,gBAAgB,CAAC,IAAI,EAAE,EAAE;AAAA,EACrF,CAAC;AAED,QAAM,SAAS,OAAO;AACtB,QAAM,SAAS,OAAO;AAGtB,WAAS,IAAI,GAAG,IAAI,MAAM,KAAK;AAC9B,aAAS,IAAI,GAAG,IAAI,MAAM,KAAK;AAC9B,YAAM,OAAO,IAAI,OAAO,KAAK;AAC7B,YAAM,KAAK,IAAI,SAAS;AACxB,YAAM,KAAK,IAAI,SAAS;AACxB,YAAM,OAAO,KAAK,KAAK,KAAK,KAAK,KAAK,EAAE;AAExC,UAAI,OAAO,QAAQ;AAElB,eAAO,MAAM,CAAC,IAAI;AAAA,MACnB,WAAW,OAAO,SAAS,GAAG;AAE7B,cAAM,eAAe,SAAS,QAAQ;AACtC,eAAO,MAAM,CAAC,IAAI,KAAK,MAAM,MAAM,cAAc,OAAO;AAAA,MACzD,OAAO;AAEN,eAAO,MAAM,CAAC,IAAI,KAAK,MAAM,MAAM,OAAO;AAAA,MAC3C;AAAA,IACD;AAAA,EACD;AAEA,QAAM,YAAY,WAAW,OAAO,QAAQ,MAAM,IAAI;AACtD,SAAO,yBAAyB,UAAU,SAAS,QAAQ,CAAC;AAC7D;;;AE1EO,IAAM,gBAA+B;AAAA,EAC3C,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,QAAQ;AACT;;;AJDO,IAAM,WAAW;AACjB,IAAM,YAAY;AAKzB,eAAsB,gBAAgB,SAA6C;AAClF,QAAM;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,OAAO;AAAA,IACP;AAAA,IACA,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,WAAW;AAAA,EACZ,IAAI;AAGJ,QAAM,SAAwB;AAAA,IAC7B,GAAG;AAAA,IACH,GAAG;AAAA,EACJ;AAGA,QAAM,QAAQ,MAAM,UAAU,UAAU;AACxC,QAAM,cAAc,kBAAkB,KAAK;AAG3C,QAAM,eAAe,aAAa,YAAY;AAC9C,QAAM,iBAAiB,aAAa,aAAa,QAAQ;AACzD,QAAM,eAAe,eAClB,qBAAqB;AAAA,IACrB,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,SAAS,aAAa,WAAW;AAAA,IACjC,WAAW,aAAa,aAAa;AAAA,EACtC,CAAC,IACA;AAGH,QAAM,qBAAqB,eACxB,2BAA2B;AAAA,IAC3B,MAAM,GAAG,cAAc;AAAA,IACvB,MAAM;AAAA,IACN,SAAS,aAAa,WAAW;AAAA,IACjC,WAAW,aAAa,aAAa;AAAA,EACtC,CAAC,IACA;AAGH,QAAM,aAAa,YAAY,QAA6C;AAG5E,QAAM,QAAyB;AAAA,IAC9B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD;AAGA,QAAM,UAAU,WAAW,KAAK;AAGhC,QAAM,MAAM,MAAM,OAAO,SAAyC;AAAA,IACjE;AAAA,IACA;AAAA,IACA,OAAO;AAAA,EACR,CAAC;AAGD,MAAI,UAAU;AACb,WAAO,OAAO,KAAK,GAAG;AAAA,EACvB;AAGA,QAAM,QAAQ,IAAI,MAAM,KAAK;AAAA,IAC5B,OAAO;AAAA,MACN,MAAM;AAAA,MACN,OAAO;AAAA,IACR;AAAA,EACD,CAAC;AACD,QAAM,UAAU,MAAM,OAAO;AAE7B,SAAO,OAAO,KAAK,QAAQ,MAAM,CAAC;AACnC;AAKA,eAAsB,uBAAuB,SAA6C;AACzF,QAAM,MAAM,MAAM,gBAAgB,OAAO;AACzC,SAAO,yBAAyB,IAAI,SAAS,QAAQ,CAAC;AACvD;AAKA,eAAsB,mBAAmB,SAA4B,cAAc,MAAyB;AAC3G,QAAM,MAAM,MAAM,gBAAgB,OAAO;AAEzC,SAAO,IAAI,SAAS,KAAK;AAAA,IACxB,SAAS;AAAA,MACR,gBAAgB;AAAA,MAChB,iBAAiB,mBAAmB,WAAW;AAAA,IAChD;AAAA,EACD,CAAC;AACF;;;AK7GO,SAAS,iBAAiB,SAA4B;AAC5D,QAAM;AAAA,IACL;AAAA,IACA,iBAAiB,WAAW;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc;AAAA,IACd;AAAA,IACA;AAAA,EACD,IAAI;AAEJ,SAAO,OAAO,EAAE,IAAI,MAAoB;AACvC,UAAM,QAAQ,IAAI,aAAa,IAAI,OAAO;AAC1C,UAAM,cAAc,IAAI,aAAa,IAAI,aAAa,KAAK;AAC3D,UAAM,QAAQ,IAAI,aAAa,IAAI,OAAO,KAAK;AAC/C,UAAM,YAAY,IAAI,aAAa,IAAI,MAAM,KAAK;AAElD,QAAI,CAAC,OAAO;AACX,aAAO,IAAI,SAAS,2BAA2B,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC/D;AAEA,QAAI;AACH,aAAO,MAAM;AAAA,QACZ;AAAA,UACC;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACD;AAAA,QACA;AAAA,MACD;AAAA,IACD,SAAS,OAAO;AACf,cAAQ,MAAM,gCAAgC,KAAK;AACnD,aAAO,IAAI,SAAS,4BAA4B,EAAE,QAAQ,IAAI,CAAC;AAAA,IAChE;AAAA,EACD;AACD;;;ACnEA,SAAS,SAAAA,cAAa;AAYf,SAAS,SAAS,KAAa,UAA2B,CAAC,GAAW;AAC5E,QAAM,OAAO;AAAA,IACZ,OAAO,QAAQ,WACZ,EAAE,MAAM,SAAkB,OAAO,QAAQ,SAAS,IAClD;AAAA,IACH,YAAY,QAAQ;AAAA,EACrB;AAEA,QAAM,QAAQ,IAAIA,OAAM,KAAK,IAAI;AACjC,QAAM,WAAW,MAAM,OAAO;AAE9B,SAAO,OAAO,KAAK,SAAS,MAAM,CAAC;AACpC;AAKO,SAAS,gBAAgB,KAAa,UAA2B,CAAC,GAAW;AACnF,QAAM,MAAM,SAAS,KAAK,OAAO;AACjC,SAAO,yBAAyB,IAAI,SAAS,QAAQ,CAAC;AACvD;AAKO,SAAS,iBAAiB,KAAa,UAA2B,CAAC,GAAG,cAAc,MAAgB;AAC1G,QAAM,MAAM,SAAS,KAAK,OAAO;AAEjC,SAAO,IAAI,SAAS,KAAK;AAAA,IACxB,SAAS;AAAA,MACR,gBAAgB;AAAA,MAChB,iBAAiB,mBAAmB,WAAW;AAAA,IAChD;AAAA,EACD,CAAC;AACF;","names":["Resvg"]}
|