@ogify/core 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 +21 -0
- package/README.md +350 -0
- package/dist/index.d.mts +400 -0
- package/dist/index.d.ts +400 -0
- package/dist/index.js +492 -0
- package/dist/index.mjs +481 -0
- package/package.json +39 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var satori = require('satori');
|
|
4
|
+
var satoriHtml = require('satori-html');
|
|
5
|
+
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var satori__default = /*#__PURE__*/_interopDefault(satori);
|
|
9
|
+
|
|
10
|
+
// src/template.ts
|
|
11
|
+
|
|
12
|
+
// src/utils/google-font-detector.ts
|
|
13
|
+
var USER_AGENTS = {
|
|
14
|
+
ttf: "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20",
|
|
15
|
+
woff: "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0"
|
|
16
|
+
};
|
|
17
|
+
var utils = {
|
|
18
|
+
/**
|
|
19
|
+
* Converts a CSS unicode-range string to an array of code points and ranges.
|
|
20
|
+
*
|
|
21
|
+
* @param input - CSS unicode-range value (e.g., "U+0041, U+0061-007A")
|
|
22
|
+
* @returns Array of code points and ranges
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* toUnicodeRange("U+0041, U+0061-007A, U+20AC")
|
|
26
|
+
* // Returns: [65, [97, 122], 8364]
|
|
27
|
+
* // Represents: 'A', 'a-z', '€'
|
|
28
|
+
*/
|
|
29
|
+
toUnicodeRange: (input) => {
|
|
30
|
+
return input.split(", ").map((range) => {
|
|
31
|
+
range = range.replaceAll("U+", "");
|
|
32
|
+
const [start, end] = range.split("-").map((hex) => parseInt(hex, 16));
|
|
33
|
+
if (isNaN(end)) {
|
|
34
|
+
return start;
|
|
35
|
+
}
|
|
36
|
+
return [start, end];
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
/**
|
|
40
|
+
* Checks if a character's code point falls within a Unicode range.
|
|
41
|
+
*
|
|
42
|
+
* @param segment - The character to check
|
|
43
|
+
* @param range - Array of code points and ranges to check against
|
|
44
|
+
* @returns true if the character is in the range, false otherwise
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* checkSegmentInRange('A', [65, [97, 122]]) // true (65 is in the range)
|
|
48
|
+
* checkSegmentInRange('a', [65, [97, 122]]) // true (97 is in [97, 122])
|
|
49
|
+
* checkSegmentInRange('€', [65, [97, 122]]) // false (8364 is not in the range)
|
|
50
|
+
*/
|
|
51
|
+
checkSegmentInRange: (segment, range) => {
|
|
52
|
+
if (range.length === 0) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const codePoint = segment.codePointAt(0);
|
|
56
|
+
if (!codePoint) return false;
|
|
57
|
+
return range.some((val) => {
|
|
58
|
+
if (typeof val === "number") {
|
|
59
|
+
return codePoint === val;
|
|
60
|
+
} else {
|
|
61
|
+
const [start, end] = val;
|
|
62
|
+
return start <= codePoint && codePoint <= end;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var GoogleFontDetector = class {
|
|
68
|
+
/**
|
|
69
|
+
* Maps font URLs to their Unicode ranges.
|
|
70
|
+
* Key: Font file URL
|
|
71
|
+
* Value: Array of Unicode code points/ranges that the font supports
|
|
72
|
+
*/
|
|
73
|
+
rangesByUrl = {};
|
|
74
|
+
/** The font configuration to detect */
|
|
75
|
+
font;
|
|
76
|
+
constructor(font) {
|
|
77
|
+
this.font = font;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Detects which Google Font URLs are needed to render the given text.
|
|
81
|
+
*
|
|
82
|
+
* This method:
|
|
83
|
+
* 1. Loads the font CSS from Google Fonts (if not already loaded)
|
|
84
|
+
* 2. Splits the text into individual characters
|
|
85
|
+
* 3. Finds the font URL for each character based on Unicode ranges
|
|
86
|
+
* 4. Returns unique URLs (no duplicates)
|
|
87
|
+
*
|
|
88
|
+
* @param text - The text to analyze
|
|
89
|
+
* @returns Promise resolving to an array of unique font file URLs
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* const urls = await detector.detect('Hello 世界');
|
|
93
|
+
* // Returns: [
|
|
94
|
+
* // 'https://fonts.gstatic.com/.../latin.woff2',
|
|
95
|
+
* // 'https://fonts.gstatic.com/.../chinese.woff2'
|
|
96
|
+
* // ]
|
|
97
|
+
*/
|
|
98
|
+
async detect(text) {
|
|
99
|
+
await this.load();
|
|
100
|
+
const detectedUrls = text.split("").map((segment) => {
|
|
101
|
+
return this.detectSegment(segment);
|
|
102
|
+
}).filter((url) => url !== null);
|
|
103
|
+
const uniqueUrls = [...new Set(detectedUrls)];
|
|
104
|
+
return uniqueUrls;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Finds the font URL that supports rendering a specific character.
|
|
108
|
+
*
|
|
109
|
+
* @param segment - A single character to check
|
|
110
|
+
* @returns The font URL that supports this character, or null if not found
|
|
111
|
+
*/
|
|
112
|
+
detectSegment(segment) {
|
|
113
|
+
for (const url of Object.keys(this.rangesByUrl)) {
|
|
114
|
+
const range = this.rangesByUrl[url];
|
|
115
|
+
if (range.length === 0 || utils.checkSegmentInRange(segment, range)) {
|
|
116
|
+
return url;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Fetches and parses the Google Fonts CSS to extract font URLs and Unicode ranges.
|
|
123
|
+
*
|
|
124
|
+
* This method:
|
|
125
|
+
* 1. Constructs the Google Fonts API URL with the font family, weight, and style
|
|
126
|
+
* 2. Fetches the CSS with an appropriate User-Agent to get the desired format
|
|
127
|
+
* 3. Parses each @font-face rule to extract URLs and unicode-range values
|
|
128
|
+
* 4. Stores the mapping in `rangesByUrl` for later character matching
|
|
129
|
+
*/
|
|
130
|
+
async load() {
|
|
131
|
+
const { name, style, weight, format = "woff" } = this.font;
|
|
132
|
+
const apiUrl = `https://fonts.googleapis.com/css2?display=swap&family=${name.split(" ").join("+")}:${style === "italic" ? "ital," : ""}wght@${style === "italic" ? "1," : ""}${weight || 400}`;
|
|
133
|
+
const content = await (await fetch(apiUrl, {
|
|
134
|
+
headers: {
|
|
135
|
+
"User-Agent": format === "ttf" ? USER_AGENTS.ttf : USER_AGENTS.woff
|
|
136
|
+
}
|
|
137
|
+
})).text();
|
|
138
|
+
content.split("@font-face").forEach((fontFace) => {
|
|
139
|
+
this.extractUrlAndRange(fontFace);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Extracts the font URL and Unicode range from a @font-face CSS rule.
|
|
144
|
+
*
|
|
145
|
+
* @param input - A @font-face CSS rule as a string
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* Input:
|
|
149
|
+
* ```
|
|
150
|
+
* {
|
|
151
|
+
* font-family: 'Inter';
|
|
152
|
+
* src: url(https://fonts.gstatic.com/.../inter.woff2) format('woff2');
|
|
153
|
+
* unicode-range: U+0041-005A, U+0061-007A;
|
|
154
|
+
* }
|
|
155
|
+
* ```
|
|
156
|
+
*
|
|
157
|
+
* Stores: rangesByUrl['https://fonts.gstatic.com/.../inter.woff2'] = [[65, 90], [97, 122]]
|
|
158
|
+
*/
|
|
159
|
+
extractUrlAndRange(input) {
|
|
160
|
+
if (input && input.includes("url") && input.includes("format")) {
|
|
161
|
+
const [, url] = input.match(/url\((.*?)\)/) || [];
|
|
162
|
+
const [, format] = input.match(/format\(['"](.+?)['"]\)/) || [];
|
|
163
|
+
const [, unicodeRange] = input.match(/unicode-range:\s*(.*?);/) || [];
|
|
164
|
+
if (!["woff", "woff2", "truetype"].includes(format)) {
|
|
165
|
+
console.warn(`[Warning] Unsupported font format: ${format}`);
|
|
166
|
+
}
|
|
167
|
+
if (url) {
|
|
168
|
+
if (unicodeRange) {
|
|
169
|
+
this.rangesByUrl[url] = [...utils.toUnicodeRange(unicodeRange)];
|
|
170
|
+
} else {
|
|
171
|
+
this.rangesByUrl[url] = [];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// src/utils/emoji-loader.ts
|
|
179
|
+
var ZERO_WIDTH_JOINER = String.fromCharCode(8205);
|
|
180
|
+
var VARIATION_SELECTOR_REGEX = /\uFE0F/g;
|
|
181
|
+
function getIconCode(char) {
|
|
182
|
+
const normalizedChar = char.indexOf(ZERO_WIDTH_JOINER) < 0 ? char.replace(VARIATION_SELECTOR_REGEX, "") : char;
|
|
183
|
+
return toCodePoint(normalizedChar);
|
|
184
|
+
}
|
|
185
|
+
function toCodePoint(unicodeSurrogates) {
|
|
186
|
+
const codePoints = [];
|
|
187
|
+
let currentChar = 0;
|
|
188
|
+
let highSurrogate = 0;
|
|
189
|
+
let index = 0;
|
|
190
|
+
while (index < unicodeSurrogates.length) {
|
|
191
|
+
currentChar = unicodeSurrogates.charCodeAt(index++);
|
|
192
|
+
if (highSurrogate) {
|
|
193
|
+
const codePoint = 65536 + (highSurrogate - 55296 << 10) + (currentChar - 56320);
|
|
194
|
+
codePoints.push(codePoint.toString(16));
|
|
195
|
+
highSurrogate = 0;
|
|
196
|
+
} else if (currentChar >= 55296 && currentChar <= 56319) {
|
|
197
|
+
highSurrogate = currentChar;
|
|
198
|
+
} else {
|
|
199
|
+
codePoints.push(currentChar.toString(16));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return codePoints.join("-");
|
|
203
|
+
}
|
|
204
|
+
var apis = {
|
|
205
|
+
twemoji: (code) => `https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/${code.toLowerCase()}.svg`,
|
|
206
|
+
openmoji: "https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/",
|
|
207
|
+
blobmoji: "https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/",
|
|
208
|
+
noto: "https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/",
|
|
209
|
+
fluent: (code) => `https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/${code.toLowerCase()}_color.svg`,
|
|
210
|
+
fluentFlat: (code) => `https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/${code.toLowerCase()}_flat.svg`
|
|
211
|
+
};
|
|
212
|
+
var cache = {};
|
|
213
|
+
async function loadEmoji(type, text) {
|
|
214
|
+
const code = getIconCode(text);
|
|
215
|
+
const cacheKey = `${type}:${code}`;
|
|
216
|
+
if (cacheKey in cache) {
|
|
217
|
+
return cache[cacheKey];
|
|
218
|
+
}
|
|
219
|
+
if (!type || !apis[type]) {
|
|
220
|
+
type = "noto";
|
|
221
|
+
}
|
|
222
|
+
const api = apis[type];
|
|
223
|
+
const baseUrl = typeof api === "function" ? api(code) : api;
|
|
224
|
+
const fullUrl = typeof api === "function" ? baseUrl : `${baseUrl}${code.toUpperCase()}.svg`;
|
|
225
|
+
const emojiPromise = fetch(fullUrl).then((response) => response.text()).then((svgContent) => `data:image/svg+xml;base64,${btoa(svgContent)}`);
|
|
226
|
+
cache[cacheKey] = emojiPromise;
|
|
227
|
+
return emojiPromise;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/utils/fetcher.ts
|
|
231
|
+
var cache2 = {};
|
|
232
|
+
var loadFontFromUrl = async (url) => {
|
|
233
|
+
if (url in cache2) {
|
|
234
|
+
return cache2[url];
|
|
235
|
+
}
|
|
236
|
+
const fontData = fetch(url).then((response) => response.arrayBuffer());
|
|
237
|
+
cache2[url] = fontData;
|
|
238
|
+
return fontData;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// src/utils/additional-asset-loader.ts
|
|
242
|
+
var loadAdditionalAsset = async (options) => {
|
|
243
|
+
const { code, segment, fonts, emojiProvider } = options;
|
|
244
|
+
if (code === "emoji") {
|
|
245
|
+
return await loadEmoji(emojiProvider, segment);
|
|
246
|
+
}
|
|
247
|
+
const fallbackFonts = [];
|
|
248
|
+
await Promise.all(
|
|
249
|
+
fonts.map(async (fontConfig) => {
|
|
250
|
+
const detector = new GoogleFontDetector(fontConfig);
|
|
251
|
+
const detectedFontUrls = await detector.detect(segment);
|
|
252
|
+
const fontDataArray = await Promise.all(
|
|
253
|
+
detectedFontUrls.map(async (fontUrl) => {
|
|
254
|
+
return await loadFontFromUrl(fontUrl);
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
fontDataArray.forEach((fontData, index) => {
|
|
258
|
+
fallbackFonts.push({
|
|
259
|
+
// Generate unique name: e.g., "Inter-Fallback-0", "Inter-Fallback-1"
|
|
260
|
+
name: `${fontConfig.name}-Fallback-${index}`,
|
|
261
|
+
// The downloaded font binary data
|
|
262
|
+
data: fontData,
|
|
263
|
+
// Inherit style from the original font config (default: 'normal')
|
|
264
|
+
style: fontConfig.style || "normal",
|
|
265
|
+
// Inherit weight from the original font config (default: 400)
|
|
266
|
+
weight: fontConfig.weight || 400
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
})
|
|
270
|
+
);
|
|
271
|
+
return fallbackFonts;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// src/utils/font-loader.ts
|
|
275
|
+
var loadFonts = async (fonts) => {
|
|
276
|
+
const loadedFonts = await Promise.all(fonts.map((font) => loadFont(font)));
|
|
277
|
+
return loadedFonts.filter((font) => font !== null);
|
|
278
|
+
};
|
|
279
|
+
var loadFont = async (font) => {
|
|
280
|
+
if (font.data) {
|
|
281
|
+
return {
|
|
282
|
+
name: font.name,
|
|
283
|
+
data: font.data,
|
|
284
|
+
style: font.style || "normal",
|
|
285
|
+
weight: font.weight || 400
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (font.url) {
|
|
289
|
+
const buffer = await loadFontFromUrl(font.url);
|
|
290
|
+
return {
|
|
291
|
+
name: font.name,
|
|
292
|
+
data: Buffer.from(buffer),
|
|
293
|
+
style: font.style || "normal",
|
|
294
|
+
weight: font.weight || 400
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const detector = new GoogleFontDetector(font);
|
|
298
|
+
const detectedFonts = await detector.detect("a");
|
|
299
|
+
if (detectedFonts.length === 0) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
name: font.name,
|
|
304
|
+
data: await loadFontFromUrl(detectedFonts[0]),
|
|
305
|
+
style: font.style || "normal",
|
|
306
|
+
weight: font.weight || 400
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// src/template.ts
|
|
311
|
+
var DEFAULT_WIDTH = 1200;
|
|
312
|
+
var DEFAULT_HEIGHT = 630;
|
|
313
|
+
async function renderTemplate(template, params, options) {
|
|
314
|
+
const width = options?.width || DEFAULT_WIDTH;
|
|
315
|
+
const height = options?.height || DEFAULT_HEIGHT;
|
|
316
|
+
const satoriFonts = await loadFonts(template.fonts);
|
|
317
|
+
const htmlString = template.renderer({
|
|
318
|
+
params,
|
|
319
|
+
width,
|
|
320
|
+
height
|
|
321
|
+
});
|
|
322
|
+
const element = satoriHtml.html(htmlString);
|
|
323
|
+
const svg = await satori__default.default(element, {
|
|
324
|
+
// Image dimensions (customizable via options parameter)
|
|
325
|
+
width,
|
|
326
|
+
height,
|
|
327
|
+
// Loaded fonts for text rendering
|
|
328
|
+
fonts: satoriFonts,
|
|
329
|
+
// Embed fonts in the SVG for portability
|
|
330
|
+
embedFont: true,
|
|
331
|
+
// Dynamic asset loader for emojis and fallback fonts
|
|
332
|
+
// This is called when Satori encounters characters that need special handling
|
|
333
|
+
loadAdditionalAsset: async (code, segment) => {
|
|
334
|
+
return loadAdditionalAsset({
|
|
335
|
+
code,
|
|
336
|
+
// Asset type ('emoji' or other)
|
|
337
|
+
segment,
|
|
338
|
+
// The character(s) to load
|
|
339
|
+
fonts: template.fonts,
|
|
340
|
+
// Available fonts for fallback detection
|
|
341
|
+
emojiProvider: template.emojiProvider || "noto"
|
|
342
|
+
// Emoji provider (default: noto)
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
const { renderAsync } = await import('@resvg/resvg-js');
|
|
347
|
+
const pngData = await renderAsync(svg, {
|
|
348
|
+
fitTo: {
|
|
349
|
+
mode: "width",
|
|
350
|
+
// Scale based on width, maintain aspect ratio
|
|
351
|
+
value: width
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
return Buffer.from(pngData.asPng());
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/renderer.ts
|
|
358
|
+
function validateTemplate(config) {
|
|
359
|
+
if (!config.id) {
|
|
360
|
+
throw new Error("Template must have an id");
|
|
361
|
+
}
|
|
362
|
+
if (!config.name) {
|
|
363
|
+
throw new Error("Template must have a name");
|
|
364
|
+
}
|
|
365
|
+
if (typeof config.renderer !== "function") {
|
|
366
|
+
throw new Error("Template must have a renderer function");
|
|
367
|
+
}
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
function defineTemplate(config) {
|
|
371
|
+
validateTemplate(config);
|
|
372
|
+
return config;
|
|
373
|
+
}
|
|
374
|
+
var TemplateRenderer = class {
|
|
375
|
+
/** Configuration including global settings and lifecycle hooks */
|
|
376
|
+
config;
|
|
377
|
+
/**
|
|
378
|
+
* Internal registry mapping template IDs to template definitions.
|
|
379
|
+
*
|
|
380
|
+
* Using a Map provides:
|
|
381
|
+
* - O(1) lookup performance
|
|
382
|
+
* - Guaranteed insertion order
|
|
383
|
+
* - Better memory efficiency than objects
|
|
384
|
+
*/
|
|
385
|
+
templates = /* @__PURE__ */ new Map();
|
|
386
|
+
/**
|
|
387
|
+
* Creates a new TemplateRenderer instance.
|
|
388
|
+
*
|
|
389
|
+
* @param config - Handler configuration with templates and global settings
|
|
390
|
+
*/
|
|
391
|
+
constructor(config) {
|
|
392
|
+
this.config = config;
|
|
393
|
+
this.registerTemplates(config.templates);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Registers multiple templates into the internal registry.
|
|
397
|
+
*
|
|
398
|
+
* Templates are indexed by their ID for fast lookup.
|
|
399
|
+
* If a template with the same ID already exists, it will be overwritten.
|
|
400
|
+
*
|
|
401
|
+
* @param templates - Array of template definitions to register
|
|
402
|
+
*/
|
|
403
|
+
registerTemplates(templates) {
|
|
404
|
+
for (const template of templates) {
|
|
405
|
+
this.templates.set(template.id, template);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Retrieves a template by its unique ID.
|
|
410
|
+
*
|
|
411
|
+
* This is useful for:
|
|
412
|
+
* - Checking if a template exists before rendering
|
|
413
|
+
* - Inspecting template configuration
|
|
414
|
+
* - Building template selection UIs
|
|
415
|
+
*
|
|
416
|
+
* @param id - The template ID to look up
|
|
417
|
+
* @returns The template definition, or undefined if not found
|
|
418
|
+
*/
|
|
419
|
+
getTemplate(id) {
|
|
420
|
+
return this.templates.get(id);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Gets all registered template IDs.
|
|
424
|
+
*
|
|
425
|
+
* Useful for:
|
|
426
|
+
* - Building template selection dropdowns
|
|
427
|
+
* - Listing available templates in documentation
|
|
428
|
+
* - Debugging template registration
|
|
429
|
+
*
|
|
430
|
+
* @returns Array of template IDs
|
|
431
|
+
*/
|
|
432
|
+
getTemplateIds() {
|
|
433
|
+
return Array.from(this.templates.keys());
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Renders a template to a PNG image buffer.
|
|
437
|
+
*
|
|
438
|
+
* This is the main method for generating OG images. It:
|
|
439
|
+
* 1. Looks up the template by ID
|
|
440
|
+
* 2. Merges default params with user params
|
|
441
|
+
* 3. Calls lifecycle hooks (if configured)
|
|
442
|
+
* 4. Delegates to the core rendering engine
|
|
443
|
+
*
|
|
444
|
+
* **Parameter Merging:**
|
|
445
|
+
* User-provided parameters take precedence over default parameters.
|
|
446
|
+
*
|
|
447
|
+
* **Custom Dimensions:**
|
|
448
|
+
* You can override the default 1200x630 dimensions by providing custom width/height.
|
|
449
|
+
* This is useful for platform-specific requirements (e.g., Twitter, Instagram).
|
|
450
|
+
*
|
|
451
|
+
* **Error Handling:**
|
|
452
|
+
* - Throws if template is not found
|
|
453
|
+
* - Throws if rendering fails (font loading, HTML generation, etc.)
|
|
454
|
+
*
|
|
455
|
+
* @param templateId - ID of the template to render
|
|
456
|
+
* @param params - Parameters to pass to the template
|
|
457
|
+
* @param options - Optional rendering options
|
|
458
|
+
* @param options.width - Custom image width in pixels (default: 1200)
|
|
459
|
+
* @param options.height - Custom image height in pixels (default: 630)
|
|
460
|
+
* @returns Promise resolving to a PNG image buffer
|
|
461
|
+
* @throws Error if template is not found or rendering fails
|
|
462
|
+
*/
|
|
463
|
+
async renderToImage(templateId, params, options) {
|
|
464
|
+
const template = this.getTemplate(templateId);
|
|
465
|
+
if (!template) {
|
|
466
|
+
throw new Error(`Template '${templateId}' not found`);
|
|
467
|
+
}
|
|
468
|
+
const mergedParams = {
|
|
469
|
+
...this.config.defaultParams,
|
|
470
|
+
...params
|
|
471
|
+
};
|
|
472
|
+
if (this.config.beforeRender) {
|
|
473
|
+
await this.config.beforeRender(templateId, mergedParams);
|
|
474
|
+
}
|
|
475
|
+
const imageBuffer = await renderTemplate(template, mergedParams, options);
|
|
476
|
+
if (this.config.afterRender) {
|
|
477
|
+
await this.config.afterRender(templateId, mergedParams, imageBuffer);
|
|
478
|
+
}
|
|
479
|
+
return imageBuffer;
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
function createTemplateRenderer(config) {
|
|
483
|
+
return new TemplateRenderer(config);
|
|
484
|
+
}
|
|
485
|
+
/*! Copyright Twitter Inc. and other contributors. Licensed under MIT */
|
|
486
|
+
|
|
487
|
+
exports.TemplateRenderer = TemplateRenderer;
|
|
488
|
+
exports.createTemplateRenderer = createTemplateRenderer;
|
|
489
|
+
exports.defineTemplate = defineTemplate;
|
|
490
|
+
exports.loadFontFromUrl = loadFontFromUrl;
|
|
491
|
+
exports.renderTemplate = renderTemplate;
|
|
492
|
+
exports.validateTemplate = validateTemplate;
|