@vercel/og 0.8.1 → 0.8.2

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,103 @@
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.js ADDED
@@ -0,0 +1,91 @@
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.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/og",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
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",
@@ -43,7 +43,8 @@
43
43
  "./package.json": "./package.json"
44
44
  },
45
45
  "scripts": {
46
- "build": "tsup && pnpm types && pnpm copy",
46
+ "typecheck": "tsc --noEmit",
47
+ "build": "pnpm typecheck && tsup && pnpm types && pnpm copy",
47
48
  "types": "tsc --project tsconfig.json",
48
49
  "copy": "node scripts/copy-vendors.js"
49
50
  },