@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/dist/index.cjs ADDED
@@ -0,0 +1,663 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ BUNDLED_FONTS: () => BUNDLED_FONTS,
34
+ OG_HEIGHT: () => OG_HEIGHT,
35
+ OG_WIDTH: () => OG_WIDTH,
36
+ createOgEndpoint: () => createOgEndpoint,
37
+ createSatoriFonts: () => createSatoriFonts,
38
+ defaultColors: () => defaultColors,
39
+ generateCircleNoiseDataUrl: () => generateCircleNoiseDataUrl,
40
+ generateNoiseDataUrl: () => generateNoiseDataUrl,
41
+ generateOgImage: () => generateOgImage,
42
+ generateOgImageDataUrl: () => generateOgImageDataUrl,
43
+ generateOgResponse: () => generateOgResponse,
44
+ loadFonts: () => loadFonts,
45
+ svgToPng: () => svgToPng,
46
+ svgToPngDataUrl: () => svgToPngDataUrl,
47
+ svgToPngResponse: () => svgToPngResponse
48
+ });
49
+ module.exports = __toCommonJS(index_exports);
50
+
51
+ // src/generate.ts
52
+ var import_satori = __toESM(require("satori"), 1);
53
+ var import_resvg_js = require("@resvg/resvg-js");
54
+
55
+ // src/fonts.ts
56
+ var import_promises = require("fs/promises");
57
+ var import_node_path = require("path");
58
+ var import_node_url = require("url");
59
+ var import_meta = {};
60
+ function getModuleDir() {
61
+ if (typeof import_meta !== "undefined" && import_meta.url) {
62
+ return (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
63
+ }
64
+ if (typeof __dirname !== "undefined") {
65
+ return __dirname;
66
+ }
67
+ return (0, import_node_path.resolve)(process.cwd(), "node_modules/@ewanc26/og/dist");
68
+ }
69
+ function getFontsDir() {
70
+ return (0, import_node_path.resolve)(getModuleDir(), "../fonts");
71
+ }
72
+ var BUNDLED_FONTS = {
73
+ get heading() {
74
+ return (0, import_node_path.resolve)(getFontsDir(), "Inter-Bold.ttf");
75
+ },
76
+ get body() {
77
+ return (0, import_node_path.resolve)(getFontsDir(), "Inter-Regular.ttf");
78
+ }
79
+ };
80
+ async function loadFonts(config) {
81
+ const headingPath = config?.heading ?? BUNDLED_FONTS.heading;
82
+ const bodyPath = config?.body ?? BUNDLED_FONTS.body;
83
+ const [heading, body] = await Promise.all([
84
+ loadFontFile(headingPath),
85
+ loadFontFile(bodyPath)
86
+ ]);
87
+ return { heading, body };
88
+ }
89
+ async function loadFontFile(source) {
90
+ if (source.startsWith("http://") || source.startsWith("https://")) {
91
+ const response = await fetch(source);
92
+ if (!response.ok) {
93
+ throw new Error(`Failed to load font from URL: ${source}`);
94
+ }
95
+ return response.arrayBuffer();
96
+ }
97
+ const buffer = await (0, import_promises.readFile)(source);
98
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
99
+ }
100
+ function createSatoriFonts(fonts) {
101
+ return [
102
+ {
103
+ name: "Inter",
104
+ data: fonts.heading,
105
+ weight: 700,
106
+ style: "normal"
107
+ },
108
+ {
109
+ name: "Inter",
110
+ data: fonts.body,
111
+ weight: 400,
112
+ style: "normal"
113
+ }
114
+ ];
115
+ }
116
+
117
+ // src/noise.ts
118
+ var import_noise = require("@ewanc26/noise");
119
+
120
+ // src/png-encoder.ts
121
+ var import_node_zlib = require("zlib");
122
+ var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
123
+ function crc32(data) {
124
+ let crc = 4294967295;
125
+ const table = [];
126
+ for (let n = 0; n < 256; n++) {
127
+ let c = n;
128
+ for (let k = 0; k < 8; k++) {
129
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
130
+ }
131
+ table[n] = c;
132
+ }
133
+ for (let i = 0; i < data.length; i++) {
134
+ crc = table[(crc ^ data[i]) & 255] ^ crc >>> 8;
135
+ }
136
+ return (crc ^ 4294967295) >>> 0;
137
+ }
138
+ function createChunk(type, data) {
139
+ const length = Buffer.alloc(4);
140
+ length.writeUInt32BE(data.length, 0);
141
+ const typeBuffer = Buffer.from(type, "ascii");
142
+ const crcData = Buffer.concat([typeBuffer, data]);
143
+ const crc = Buffer.alloc(4);
144
+ crc.writeUInt32BE(crc32(crcData), 0);
145
+ return Buffer.concat([length, typeBuffer, data, crc]);
146
+ }
147
+ function createIHDR(width, height) {
148
+ const data = Buffer.alloc(13);
149
+ data.writeUInt32BE(width, 0);
150
+ data.writeUInt32BE(height, 4);
151
+ data.writeUInt8(8, 8);
152
+ data.writeUInt8(2, 9);
153
+ data.writeUInt8(0, 10);
154
+ data.writeUInt8(0, 11);
155
+ data.writeUInt8(0, 12);
156
+ return createChunk("IHDR", data);
157
+ }
158
+ function createIDAT(pixels, width, height) {
159
+ const rawData = Buffer.alloc(height * (width * 3 + 1));
160
+ let srcOffset = 0;
161
+ let dstOffset = 0;
162
+ for (let y = 0; y < height; y++) {
163
+ rawData[dstOffset++] = 0;
164
+ for (let x = 0; x < width; x++) {
165
+ const r = pixels[srcOffset++];
166
+ const g = pixels[srcOffset++];
167
+ const b = pixels[srcOffset++];
168
+ srcOffset++;
169
+ rawData[dstOffset++] = r;
170
+ rawData[dstOffset++] = g;
171
+ rawData[dstOffset++] = b;
172
+ }
173
+ }
174
+ const compressed = (0, import_node_zlib.deflateSync)(rawData);
175
+ return createChunk("IDAT", compressed);
176
+ }
177
+ function createIEND() {
178
+ return createChunk("IEND", Buffer.alloc(0));
179
+ }
180
+ function encodePNG(pixels, width, height) {
181
+ return Buffer.concat([
182
+ PNG_SIGNATURE,
183
+ createIHDR(width, height),
184
+ createIDAT(pixels, width, height),
185
+ createIEND()
186
+ ]);
187
+ }
188
+ var PNGEncoder = {
189
+ encode: encodePNG
190
+ };
191
+
192
+ // src/noise.ts
193
+ function generateNoiseDataUrl(options) {
194
+ const { seed, width, height, opacity = 0.4, colorMode = "grayscale" } = options;
195
+ const pixels = (0, import_noise.generateNoisePixels)(width, height, seed, {
196
+ gridSize: 4,
197
+ octaves: 3,
198
+ colorMode: colorMode === "grayscale" ? { type: "grayscale", range: [20, 60] } : { type: "hsl", hueRange: 40, saturationRange: [30, 50], lightnessRange: [30, 50] }
199
+ });
200
+ if (opacity < 1) {
201
+ for (let i = 3; i < pixels.length; i += 4) {
202
+ pixels[i] = Math.round(pixels[i] * opacity);
203
+ }
204
+ }
205
+ const pngBuffer = PNGEncoder.encode(pixels, width, height);
206
+ return `data:image/png;base64,${pngBuffer.toString("base64")}`;
207
+ }
208
+ function generateCircleNoiseDataUrl(options) {
209
+ const { seed, size, opacity = 0.15, colorMode = "grayscale" } = options;
210
+ const pixels = (0, import_noise.generateNoisePixels)(size, size, seed, {
211
+ gridSize: 4,
212
+ octaves: 3,
213
+ colorMode: colorMode === "grayscale" ? { type: "grayscale", range: [30, 70] } : { type: "hsl", hueRange: 40, saturationRange: [30, 50], lightnessRange: [30, 50] }
214
+ });
215
+ const center = size / 2;
216
+ const radius = size / 2;
217
+ for (let y = 0; y < size; y++) {
218
+ for (let x = 0; x < size; x++) {
219
+ const idx = (y * size + x) * 4;
220
+ const dx = x - center + 0.5;
221
+ const dy = y - center + 0.5;
222
+ const dist = Math.sqrt(dx * dx + dy * dy);
223
+ if (dist > radius) {
224
+ pixels[idx + 3] = 0;
225
+ } else if (dist > radius - 2) {
226
+ const edgeOpacity = (radius - dist) / 2;
227
+ pixels[idx + 3] = Math.round(255 * edgeOpacity * opacity);
228
+ } else {
229
+ pixels[idx + 3] = Math.round(255 * opacity);
230
+ }
231
+ }
232
+ }
233
+ const pngBuffer = PNGEncoder.encode(pixels, size, size);
234
+ return `data:image/png;base64,${pngBuffer.toString("base64")}`;
235
+ }
236
+
237
+ // src/templates/blog.ts
238
+ function blogTemplate({
239
+ title,
240
+ description,
241
+ siteName,
242
+ colors,
243
+ width,
244
+ height
245
+ }) {
246
+ return {
247
+ type: "div",
248
+ props: {
249
+ style: {
250
+ display: "flex",
251
+ flexDirection: "column",
252
+ alignItems: "center",
253
+ justifyContent: "center",
254
+ width,
255
+ height,
256
+ backgroundColor: colors.background
257
+ },
258
+ children: [
259
+ {
260
+ type: "h1",
261
+ props: {
262
+ style: {
263
+ fontSize: 64,
264
+ fontWeight: 700,
265
+ color: colors.text,
266
+ letterSpacing: "-0.02em",
267
+ margin: 0,
268
+ textAlign: "center",
269
+ lineHeight: 1.1,
270
+ maxWidth: 1e3
271
+ },
272
+ children: title
273
+ }
274
+ },
275
+ description ? {
276
+ type: "p",
277
+ props: {
278
+ style: {
279
+ fontSize: 28,
280
+ fontWeight: 400,
281
+ color: colors.accent,
282
+ marginTop: 28,
283
+ marginBottom: 0,
284
+ textAlign: "center",
285
+ lineHeight: 1.4,
286
+ maxWidth: 900
287
+ },
288
+ children: description
289
+ }
290
+ } : null,
291
+ {
292
+ type: "p",
293
+ props: {
294
+ style: {
295
+ fontSize: 24,
296
+ fontWeight: 400,
297
+ color: colors.accent,
298
+ marginTop: 56,
299
+ marginBottom: 0,
300
+ textAlign: "center",
301
+ opacity: 0.7
302
+ },
303
+ children: siteName
304
+ }
305
+ }
306
+ ].filter(Boolean)
307
+ }
308
+ };
309
+ }
310
+
311
+ // src/templates/profile.ts
312
+ function profileTemplate({
313
+ title,
314
+ description,
315
+ siteName,
316
+ image,
317
+ colors,
318
+ width,
319
+ height
320
+ }) {
321
+ const children = [];
322
+ if (image) {
323
+ children.push({
324
+ type: "img",
325
+ props: {
326
+ src: image,
327
+ width: 120,
328
+ height: 120,
329
+ style: {
330
+ borderRadius: "50%",
331
+ marginBottom: 32,
332
+ objectFit: "cover"
333
+ }
334
+ }
335
+ });
336
+ }
337
+ children.push({
338
+ type: "h1",
339
+ props: {
340
+ style: {
341
+ fontSize: 56,
342
+ fontWeight: 700,
343
+ color: colors.text,
344
+ letterSpacing: "-0.02em",
345
+ margin: 0,
346
+ textAlign: "center",
347
+ lineHeight: 1.1,
348
+ maxWidth: 900
349
+ },
350
+ children: title
351
+ }
352
+ });
353
+ if (description) {
354
+ children.push({
355
+ type: "p",
356
+ props: {
357
+ style: {
358
+ fontSize: 26,
359
+ fontWeight: 400,
360
+ color: colors.accent,
361
+ marginTop: 20,
362
+ marginBottom: 0,
363
+ textAlign: "center",
364
+ lineHeight: 1.4,
365
+ maxWidth: 700
366
+ },
367
+ children: description
368
+ }
369
+ });
370
+ }
371
+ children.push({
372
+ type: "p",
373
+ props: {
374
+ style: {
375
+ fontSize: 24,
376
+ fontWeight: 400,
377
+ color: colors.accent,
378
+ marginTop: 48,
379
+ marginBottom: 0,
380
+ textAlign: "center",
381
+ opacity: 0.7
382
+ },
383
+ children: siteName
384
+ }
385
+ });
386
+ return {
387
+ type: "div",
388
+ props: {
389
+ style: {
390
+ display: "flex",
391
+ flexDirection: "column",
392
+ alignItems: "center",
393
+ justifyContent: "center",
394
+ width,
395
+ height,
396
+ backgroundColor: colors.background
397
+ },
398
+ children
399
+ }
400
+ };
401
+ }
402
+
403
+ // src/templates/default.ts
404
+ function defaultTemplate({
405
+ title,
406
+ description,
407
+ siteName,
408
+ colors,
409
+ width,
410
+ height
411
+ }) {
412
+ return {
413
+ type: "div",
414
+ props: {
415
+ style: {
416
+ display: "flex",
417
+ flexDirection: "column",
418
+ alignItems: "center",
419
+ justifyContent: "center",
420
+ width,
421
+ height,
422
+ backgroundColor: colors.background
423
+ },
424
+ children: [
425
+ {
426
+ type: "h1",
427
+ props: {
428
+ style: {
429
+ fontSize: 72,
430
+ fontWeight: 700,
431
+ color: colors.text,
432
+ letterSpacing: "-0.02em",
433
+ margin: 0,
434
+ textAlign: "center"
435
+ },
436
+ children: title
437
+ }
438
+ },
439
+ description ? {
440
+ type: "p",
441
+ props: {
442
+ style: {
443
+ fontSize: 32,
444
+ fontWeight: 400,
445
+ color: colors.accent,
446
+ marginTop: 24,
447
+ marginBottom: 0,
448
+ textAlign: "center",
449
+ maxWidth: 900
450
+ },
451
+ children: description
452
+ }
453
+ } : null,
454
+ {
455
+ type: "p",
456
+ props: {
457
+ style: {
458
+ fontSize: 28,
459
+ fontWeight: 400,
460
+ color: colors.accent,
461
+ marginTop: 64,
462
+ marginBottom: 0,
463
+ textAlign: "center",
464
+ opacity: 0.7
465
+ },
466
+ children: siteName
467
+ }
468
+ }
469
+ ].filter(Boolean)
470
+ }
471
+ };
472
+ }
473
+
474
+ // src/templates/index.ts
475
+ var templates = {
476
+ blog: blogTemplate,
477
+ profile: profileTemplate,
478
+ default: defaultTemplate
479
+ };
480
+ function getTemplate(name) {
481
+ if (typeof name === "function") {
482
+ return name;
483
+ }
484
+ return templates[name];
485
+ }
486
+
487
+ // src/types.ts
488
+ var defaultColors = {
489
+ background: "#0f1a15",
490
+ text: "#e8f5e9",
491
+ accent: "#86efac"
492
+ };
493
+
494
+ // src/generate.ts
495
+ var OG_WIDTH = 1200;
496
+ var OG_HEIGHT = 630;
497
+ async function generateOgImage(options) {
498
+ const {
499
+ title,
500
+ description,
501
+ siteName,
502
+ image,
503
+ template = "blog",
504
+ colors: colorOverrides,
505
+ fonts: fontConfig,
506
+ noise: noiseConfig,
507
+ noiseSeed,
508
+ width = OG_WIDTH,
509
+ height = OG_HEIGHT,
510
+ debugSvg = false
511
+ } = options;
512
+ const colors = {
513
+ ...defaultColors,
514
+ ...colorOverrides
515
+ };
516
+ const fonts = await loadFonts(fontConfig);
517
+ const satoriFonts = createSatoriFonts(fonts);
518
+ const noiseEnabled = noiseConfig?.enabled !== false;
519
+ const noiseSeedValue = noiseSeed || noiseConfig?.seed || title;
520
+ const noiseDataUrl = noiseEnabled ? generateNoiseDataUrl({
521
+ seed: noiseSeedValue,
522
+ width,
523
+ height,
524
+ opacity: noiseConfig?.opacity ?? 0.4,
525
+ colorMode: noiseConfig?.colorMode ?? "grayscale"
526
+ }) : void 0;
527
+ const circleNoiseDataUrl = noiseEnabled ? generateCircleNoiseDataUrl({
528
+ seed: `${noiseSeedValue}-circle`,
529
+ size: 200,
530
+ opacity: noiseConfig?.opacity ?? 0.15,
531
+ colorMode: noiseConfig?.colorMode ?? "grayscale"
532
+ }) : void 0;
533
+ const templateFn = getTemplate(template);
534
+ const props = {
535
+ title,
536
+ description,
537
+ siteName,
538
+ image,
539
+ colors,
540
+ noiseDataUrl,
541
+ circleNoiseDataUrl,
542
+ width,
543
+ height
544
+ };
545
+ const element = templateFn(props);
546
+ const svg = await (0, import_satori.default)(element, {
547
+ width,
548
+ height,
549
+ fonts: satoriFonts
550
+ });
551
+ if (debugSvg) {
552
+ return Buffer.from(svg);
553
+ }
554
+ const resvg = new import_resvg_js.Resvg(svg, {
555
+ fitTo: {
556
+ mode: "width",
557
+ value: width
558
+ }
559
+ });
560
+ const pngData = resvg.render();
561
+ return Buffer.from(pngData.asPng());
562
+ }
563
+ async function generateOgImageDataUrl(options) {
564
+ const png = await generateOgImage(options);
565
+ return `data:image/png;base64,${png.toString("base64")}`;
566
+ }
567
+ async function generateOgResponse(options, cacheMaxAge = 3600) {
568
+ const png = await generateOgImage(options);
569
+ return new Response(png, {
570
+ headers: {
571
+ "Content-Type": "image/png",
572
+ "Cache-Control": `public, max-age=${cacheMaxAge}`
573
+ }
574
+ });
575
+ }
576
+
577
+ // src/endpoint.ts
578
+ function createOgEndpoint(options) {
579
+ const {
580
+ siteName,
581
+ defaultTemplate: template = "default",
582
+ colors,
583
+ fonts,
584
+ noise,
585
+ cacheMaxAge = 3600,
586
+ width,
587
+ height
588
+ } = options;
589
+ return async ({ url }) => {
590
+ const title = url.searchParams.get("title");
591
+ const description = url.searchParams.get("description") ?? void 0;
592
+ const image = url.searchParams.get("image") ?? void 0;
593
+ const noiseSeed = url.searchParams.get("seed") ?? void 0;
594
+ if (!title) {
595
+ return new Response("Missing title parameter", { status: 400 });
596
+ }
597
+ try {
598
+ return await generateOgResponse(
599
+ {
600
+ title,
601
+ description,
602
+ siteName,
603
+ image,
604
+ template,
605
+ colors,
606
+ fonts,
607
+ noise,
608
+ noiseSeed,
609
+ width,
610
+ height
611
+ },
612
+ cacheMaxAge
613
+ );
614
+ } catch (error) {
615
+ console.error("Failed to generate OG image:", error);
616
+ return new Response("Failed to generate image", { status: 500 });
617
+ }
618
+ };
619
+ }
620
+
621
+ // src/svg.ts
622
+ var import_resvg_js2 = require("@resvg/resvg-js");
623
+ function svgToPng(svg, options = {}) {
624
+ const opts = {
625
+ fitTo: options.fitWidth ? { mode: "width", value: options.fitWidth } : void 0,
626
+ background: options.backgroundColor
627
+ };
628
+ const resvg = new import_resvg_js2.Resvg(svg, opts);
629
+ const rendered = resvg.render();
630
+ return Buffer.from(rendered.asPng());
631
+ }
632
+ function svgToPngDataUrl(svg, options = {}) {
633
+ const png = svgToPng(svg, options);
634
+ return `data:image/png;base64,${png.toString("base64")}`;
635
+ }
636
+ function svgToPngResponse(svg, options = {}, cacheMaxAge = 3600) {
637
+ const png = svgToPng(svg, options);
638
+ return new Response(png, {
639
+ headers: {
640
+ "Content-Type": "image/png",
641
+ "Cache-Control": `public, max-age=${cacheMaxAge}`
642
+ }
643
+ });
644
+ }
645
+ // Annotate the CommonJS export names for ESM import in node:
646
+ 0 && (module.exports = {
647
+ BUNDLED_FONTS,
648
+ OG_HEIGHT,
649
+ OG_WIDTH,
650
+ createOgEndpoint,
651
+ createSatoriFonts,
652
+ defaultColors,
653
+ generateCircleNoiseDataUrl,
654
+ generateNoiseDataUrl,
655
+ generateOgImage,
656
+ generateOgImageDataUrl,
657
+ generateOgResponse,
658
+ loadFonts,
659
+ svgToPng,
660
+ svgToPngDataUrl,
661
+ svgToPngResponse
662
+ });
663
+ //# sourceMappingURL=index.cjs.map