@vercel/og 0.8.2 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,74 @@
1
+ import { SatoriOptions } from 'satori';
2
+ import { OutgoingHttpHeader } from 'http';
3
+
4
+ declare const apis: {
5
+ twemoji: (code: any) => string;
6
+ openmoji: string;
7
+ blobmoji: string;
8
+ noto: string;
9
+ fluent: (code: any) => string;
10
+ fluentFlat: (code: any) => string;
11
+ };
12
+ type EmojiType = keyof typeof apis;
13
+
14
+ type ImageOptions = {
15
+ /**
16
+ * The width of the image.
17
+ *
18
+ * @type {number}
19
+ * @default 1200
20
+ */
21
+ width?: number;
22
+ /**
23
+ * The height of the image.
24
+ *
25
+ * @type {number}
26
+ * @default 630
27
+ */
28
+ height?: number;
29
+ /**
30
+ * Display debug information on the image.
31
+ *
32
+ * @type {boolean}
33
+ * @default false
34
+ */
35
+ debug?: boolean;
36
+ /**
37
+ * A list of fonts to use.
38
+ *
39
+ * @type {{ data: ArrayBuffer; name: string; weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900; style?: 'normal' | 'italic' }[]}
40
+ * @default Noto Sans Latin Regular.
41
+ */
42
+ fonts?: SatoriOptions['fonts'];
43
+ /**
44
+ * Using a specific Emoji style. Defaults to `twemoji`.
45
+ *
46
+ * @type {EmojiType}
47
+ * @default 'twemoji'
48
+ */
49
+ emoji?: EmojiType;
50
+ };
51
+ type ImageResponseNodeOptions = ImageOptions & {
52
+ status?: number;
53
+ statusText?: string;
54
+ headers?: OutgoingHttpHeader[];
55
+ };
56
+ type ImageResponseOptions = ImageOptions & ConstructorParameters<typeof Response>[1];
57
+ declare module 'react' {
58
+ interface HTMLAttributes<T> {
59
+ /**
60
+ * Specify styles using Tailwind CSS classes. This feature is currently experimental.
61
+ * If `style` prop is also specified, styles generated with `tw` prop will be overridden.
62
+ *
63
+ * Example:
64
+ * - `tw='w-full h-full bg-blue-200'`
65
+ * - `tw='text-9xl'`
66
+ * - `tw='text-[80px]'`
67
+ *
68
+ * @type {string}
69
+ */
70
+ tw?: string;
71
+ }
72
+ }
73
+
74
+ export { ImageResponseOptions as I, ImageResponseNodeOptions as a };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/og",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Generate Open Graph Images dynamically from HTML/CSS without a browser",
5
5
  "type": "module",
6
6
  "main": "./dist/index.node.js",
@@ -44,8 +44,7 @@
44
44
  },
45
45
  "scripts": {
46
46
  "typecheck": "tsc --noEmit",
47
- "build": "pnpm typecheck && tsup && pnpm types && pnpm copy",
48
- "types": "tsc --project tsconfig.json",
47
+ "build": "pnpm typecheck && tsup --dts && pnpm copy",
49
48
  "copy": "node scripts/copy-vendors.js"
50
49
  },
51
50
  "keywords": [
@@ -1,15 +0,0 @@
1
- /**
2
- * Modified version of https://unpkg.com/twemoji@13.1.0/dist/twemoji.esm.js.
3
- */
4
- export declare function getIconCode(char: string): string;
5
- declare const apis: {
6
- twemoji: (code: any) => string;
7
- openmoji: string;
8
- blobmoji: string;
9
- noto: string;
10
- fluent: (code: any) => string;
11
- fluentFlat: (code: any) => string;
12
- };
13
- export type EmojiType = keyof typeof apis;
14
- export declare function loadEmoji(code: string, type?: EmojiType): Promise<Response>;
15
- export {};
@@ -1,49 +0,0 @@
1
- /**
2
- * Modified version of https://unpkg.com/twemoji@13.1.0/dist/twemoji.esm.js.
3
- */
4
- /*! Copyright Twitter Inc. and other contributors. Licensed under MIT */
5
- const U200D = String.fromCharCode(8205);
6
- const UFE0Fg = /\uFE0F/g;
7
- export function getIconCode(char) {
8
- return toCodePoint(char.indexOf(U200D) < 0 ? char.replace(UFE0Fg, '') : char);
9
- }
10
- function toCodePoint(unicodeSurrogates) {
11
- var r = [], c = 0, p = 0, i = 0;
12
- while (i < unicodeSurrogates.length) {
13
- c = unicodeSurrogates.charCodeAt(i++);
14
- if (p) {
15
- r.push((65536 + ((p - 55296) << 10) + (c - 56320)).toString(16));
16
- p = 0;
17
- }
18
- else if (55296 <= c && c <= 56319) {
19
- p = c;
20
- }
21
- else {
22
- r.push(c.toString(16));
23
- }
24
- }
25
- return r.join('-');
26
- }
27
- const apis = {
28
- twemoji: (code) => 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/' + code.toLowerCase() + '.svg',
29
- openmoji: 'https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/',
30
- blobmoji: 'https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/',
31
- noto: 'https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/',
32
- fluent: (code) => 'https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/' +
33
- code.toLowerCase() +
34
- '_color.svg',
35
- fluentFlat: (code) => 'https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/' +
36
- code.toLowerCase() +
37
- '_flat.svg',
38
- };
39
- export function loadEmoji(code, type) {
40
- // https://github.com/svgmoji/svgmoji
41
- if (!type || !apis[type]) {
42
- type = 'twemoji';
43
- }
44
- const api = apis[type];
45
- if (typeof api === 'function') {
46
- return fetch(api(code));
47
- }
48
- return fetch(`${api}${code.toUpperCase()}.svg`);
49
- }
@@ -1,28 +0,0 @@
1
- export declare class FontDetector {
2
- private rangesByLang;
3
- detect(text: string, fonts: string[]): Promise<{
4
- [lang: string]: string;
5
- }>;
6
- private detectSegment;
7
- private load;
8
- private addDetectors;
9
- }
10
- export declare const languageFontMap: {
11
- 'ja-JP': string;
12
- 'ko-KR': string;
13
- 'zh-CN': string;
14
- 'zh-TW': string;
15
- 'zh-HK': string;
16
- 'th-TH': string;
17
- 'bn-IN': string;
18
- 'ar-AR': string;
19
- 'ta-IN': string;
20
- 'ml-IN': string;
21
- 'he-IL': string;
22
- 'te-IN': string;
23
- devanagari: string;
24
- kannada: string;
25
- symbol: string[];
26
- math: string;
27
- unknown: string;
28
- };
@@ -1,103 +0,0 @@
1
- export class FontDetector {
2
- constructor() {
3
- this.rangesByLang = {};
4
- }
5
- async detect(text, fonts) {
6
- await this.load(fonts);
7
- const result = {};
8
- for (const segment of text) {
9
- const lang = this.detectSegment(segment, fonts);
10
- if (lang) {
11
- result[lang] = result[lang] || '';
12
- result[lang] += segment;
13
- }
14
- }
15
- return result;
16
- }
17
- detectSegment(segment, fonts) {
18
- for (const font of fonts) {
19
- const range = this.rangesByLang[font];
20
- if (range && checkSegmentInRange(segment, range)) {
21
- return font;
22
- }
23
- }
24
- return null;
25
- }
26
- async load(fonts) {
27
- let params = '';
28
- const existingLang = Object.keys(this.rangesByLang);
29
- const langNeedsToLoad = fonts.filter((font) => !existingLang.includes(font));
30
- if (langNeedsToLoad.length === 0) {
31
- return;
32
- }
33
- for (const font of langNeedsToLoad) {
34
- params += `family=${font}&`;
35
- }
36
- params += 'display=swap';
37
- const API = `https://fonts.googleapis.com/css2?${params}`;
38
- const fontFace = await (await fetch(API, {
39
- headers: {
40
- // Make sure it returns TTF.
41
- 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
42
- },
43
- })).text();
44
- this.addDetectors(fontFace);
45
- }
46
- addDetectors(input) {
47
- const regex = /font-family:\s*'(.+?)';.+?unicode-range:\s*(.+?);/gms;
48
- const matches = input.matchAll(regex);
49
- for (const [, _lang, range] of matches) {
50
- const lang = _lang.replaceAll(' ', '+');
51
- if (!this.rangesByLang[lang]) {
52
- this.rangesByLang[lang] = [];
53
- }
54
- this.rangesByLang[lang].push(...convert(range));
55
- }
56
- }
57
- }
58
- function convert(input) {
59
- return input.split(', ').map((range) => {
60
- range = range.replaceAll('U+', '');
61
- const [start, end] = range.split('-').map((hex) => parseInt(hex, 16));
62
- if (isNaN(end)) {
63
- return start;
64
- }
65
- return [start, end];
66
- });
67
- }
68
- function checkSegmentInRange(segment, range) {
69
- const codePoint = segment.codePointAt(0);
70
- if (!codePoint)
71
- return false;
72
- return range.some((val) => {
73
- if (typeof val === 'number') {
74
- return codePoint === val;
75
- }
76
- else {
77
- const [start, end] = val;
78
- return start <= codePoint && codePoint <= end;
79
- }
80
- });
81
- }
82
- // @TODO: Support font style and weights, and make this option extensible rather
83
- // than built-in.
84
- // @TODO: Cover most languages with Noto Sans.
85
- export const languageFontMap = {
86
- 'ja-JP': 'Noto+Sans+JP',
87
- 'ko-KR': 'Noto+Sans+KR',
88
- 'zh-CN': 'Noto+Sans+SC',
89
- 'zh-TW': 'Noto+Sans+TC',
90
- 'zh-HK': 'Noto+Sans+HK',
91
- 'th-TH': 'Noto+Sans+Thai',
92
- 'bn-IN': 'Noto+Sans+Bengali',
93
- 'ar-AR': 'Noto+Sans+Arabic',
94
- 'ta-IN': 'Noto+Sans+Tamil',
95
- 'ml-IN': 'Noto+Sans+Malayalam',
96
- 'he-IL': 'Noto+Sans+Hebrew',
97
- 'te-IN': 'Noto+Sans+Telugu',
98
- devanagari: 'Noto+Sans+Devanagari',
99
- kannada: 'Noto+Sans+Kannada',
100
- symbol: ['Noto+Sans+Symbols', 'Noto+Sans+Symbols+2'],
101
- math: 'Noto+Sans+Math',
102
- unknown: 'Noto+Sans',
103
- };
package/dist/og.d.ts DELETED
@@ -1 +0,0 @@
1
- export default function render(satori: any, resvg: typeof import('@resvg/resvg-wasm'), opts: any, defaultFonts: any, element: any): Promise<Uint8Array>;
package/dist/og.js DELETED
@@ -1,91 +0,0 @@
1
- import { loadEmoji, getIconCode } from './emoji';
2
- import { FontDetector, languageFontMap } from './language';
3
- async function loadGoogleFont(font, text) {
4
- if (!font || !text)
5
- return;
6
- const API = `https://fonts.googleapis.com/css2?family=${font}&text=${encodeURIComponent(text)}`;
7
- const css = await (await fetch(API, {
8
- headers: {
9
- // Make sure it returns TTF.
10
- 'User-Agent': 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1',
11
- },
12
- })).text();
13
- const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/);
14
- if (!resource)
15
- throw new Error('Failed to download dynamic font');
16
- const res = await fetch(resource[1]);
17
- if (!res.ok) {
18
- throw new Error('Failed to download dynamic font. Status: ' + res.status);
19
- }
20
- return res.arrayBuffer();
21
- }
22
- const detector = new FontDetector();
23
- const assetCache = new Map();
24
- const loadDynamicAsset = ({ emoji }) => {
25
- const fn = async (code, text) => {
26
- if (code === 'emoji') {
27
- // It's an emoji, load the image.
28
- return (`data:image/svg+xml;base64,` +
29
- btoa(await (await loadEmoji(getIconCode(text), emoji)).text()));
30
- }
31
- const codes = code.split('|');
32
- // Try to load from Google Fonts.
33
- const names = codes
34
- .map((code) => languageFontMap[code])
35
- .filter(Boolean)
36
- .flat();
37
- if (names.length === 0)
38
- return [];
39
- try {
40
- const textByFont = await detector.detect(text, names);
41
- const fonts = Object.keys(textByFont);
42
- const fontData = await Promise.all(fonts.map((font) => loadGoogleFont(font, textByFont[font])));
43
- return fontData.map((data, index) => ({
44
- name: `satori_${codes[index]}_fallback_${text}`,
45
- data,
46
- weight: 400,
47
- style: 'normal',
48
- lang: codes[index] === 'unknown' ? undefined : codes[index],
49
- }));
50
- }
51
- catch (e) {
52
- console.error('Failed to load dynamic font for', text, '. Error:', e);
53
- }
54
- };
55
- return async (...args) => {
56
- const key = JSON.stringify(args);
57
- const cache = assetCache.get(key);
58
- if (cache)
59
- return cache;
60
- const asset = await fn(...args);
61
- assetCache.set(key, asset);
62
- return asset;
63
- };
64
- };
65
- export default async function render(satori, resvg, opts, defaultFonts, element) {
66
- const options = Object.assign({
67
- width: 1200,
68
- height: 630,
69
- debug: false,
70
- }, opts);
71
- const svg = await satori(element, {
72
- width: options.width,
73
- height: options.height,
74
- debug: options.debug,
75
- fonts: options.fonts || defaultFonts,
76
- loadAdditionalAsset: loadDynamicAsset({
77
- emoji: options.emoji,
78
- }),
79
- });
80
- const resvgJS = new resvg.Resvg(svg, {
81
- fitTo: {
82
- mode: 'width',
83
- value: options.width,
84
- },
85
- });
86
- const pngData = resvgJS.render();
87
- const pngBuffer = pngData.asPng();
88
- pngData.free();
89
- resvgJS.free();
90
- return pngBuffer;
91
- }
package/dist/types.d.ts DELETED
@@ -1,118 +0,0 @@
1
- /// <reference types="node" />
2
- /// <reference types="react" />
3
- /// <reference types="node" />
4
- /// <reference types="node" />
5
- import type { SatoriOptions } from 'satori';
6
- import type { EmojiType } from './emoji';
7
- import type { OutgoingHttpHeader } from 'http';
8
- type ImageOptions = {
9
- /**
10
- * The width of the image.
11
- *
12
- * @type {number}
13
- * @default 1200
14
- */
15
- width?: number;
16
- /**
17
- * The height of the image.
18
- *
19
- * @type {number}
20
- * @default 630
21
- */
22
- height?: number;
23
- /**
24
- * Display debug information on the image.
25
- *
26
- * @type {boolean}
27
- * @default false
28
- */
29
- debug?: boolean;
30
- /**
31
- * A list of fonts to use.
32
- *
33
- * @type {{ data: ArrayBuffer; name: string; weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900; style?: 'normal' | 'italic' }[]}
34
- * @default Noto Sans Latin Regular.
35
- */
36
- fonts?: SatoriOptions['fonts'];
37
- /**
38
- * Using a specific Emoji style. Defaults to `twemoji`.
39
- *
40
- * @type {EmojiType}
41
- * @default 'twemoji'
42
- */
43
- emoji?: EmojiType;
44
- };
45
- export type ImageResponseNodeOptions = ImageOptions & {
46
- status?: number;
47
- statusText?: string;
48
- headers?: OutgoingHttpHeader[];
49
- };
50
- export type ImageResponseOptions = ImageOptions & ConstructorParameters<typeof Response>[1];
51
- export interface FigmaImageResponseProps {
52
- /**
53
- * Link to the Figma template frame.
54
- *
55
- * You can get the URL in Figma by right-clicking a frame and selecting "Copy link".
56
- * @example https://www.figma.com/file/QjGNQixWnhu300e1Xzdl2y/OG-Images?type=design&node-id=11356-2443&mode=design&t=yLROd7ro8mP1PxMY-4
57
- */
58
- url: string;
59
- /**
60
- * A mapping between Figma layer name and the value you want to replace it with.
61
- *
62
- * @example Sets Figma text layer named "Title" to "How to create OG Images"
63
- * ```js
64
- * { "Title": "How to create OG Images" }
65
- * ```
66
- *
67
- * @example Sets multiple Figma text layers and provides custom styles
68
- * ```js
69
- * {
70
- * "Title": { value: "How to create OG Images", props: { color: "red", centerHorizontally: true } },
71
- * "Description": { value: "A short story", props: { centerHorizontally: true } },
72
- * }
73
- * ```
74
- *
75
- * `centerHorizontally` centers text layer horizontally.
76
- */
77
- template: Record<string, FigmaComplexTemplate | string>;
78
- /**
79
- * The font names must match the font names in Figma.
80
- */
81
- fonts?: FontOptions[];
82
- /**
83
- * The same as {@link ImageResponseOptions} except `width` and `height`. `width` and `height` are automatically set from the Figma frame's size.
84
- */
85
- imageResponseOptions?: Omit<ImageResponseOptions, 'width' | 'height'>;
86
- }
87
- export interface FigmaComplexTemplate {
88
- value: string;
89
- props?: {
90
- centerHorizontally?: boolean;
91
- } & React.CSSProperties;
92
- }
93
- type Weight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
94
- type Style = 'normal' | 'italic';
95
- interface FontOptions {
96
- data: Buffer | ArrayBuffer;
97
- name: string;
98
- weight?: Weight;
99
- style?: Style;
100
- lang?: string;
101
- }
102
- declare module 'react' {
103
- interface HTMLAttributes<T> {
104
- /**
105
- * Specify styles using Tailwind CSS classes. This feature is currently experimental.
106
- * If `style` prop is also specified, styles generated with `tw` prop will be overridden.
107
- *
108
- * Example:
109
- * - `tw='w-full h-full bg-blue-200'`
110
- * - `tw='text-9xl'`
111
- * - `tw='text-[80px]'`
112
- *
113
- * @type {string}
114
- */
115
- tw?: string;
116
- }
117
- }
118
- export {};
package/dist/types.js DELETED
@@ -1 +0,0 @@
1
- export {};