@vercel/og 0.0.15 → 0.0.16

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/README.md CHANGED
@@ -93,11 +93,24 @@ By default, `@vercel/og` only has the Noto Sans font included. If you need to us
93
93
  - Basic · [_source_](/examples/next/pages/api/vercel.tsx) · [_demo_](https://og-examples.vercel.sh/api/vercel)
94
94
  - Embed SVG Image · [_source_](/examples/next/pages/api/image-svg.tsx) · [_demo_](https://og-examples.vercel.sh/api/image-svg)
95
95
  - Dynamic PNG Image Based on URL Queries · [_source_](/examples/next/pages/api/dynamic-image.tsx) · [_demo_](https://og-examples.vercel.sh/api/dynamic-image?username=vercel)
96
+ - Fetch External Data · [_source_](/examples/next/pages/api/external-data.tsx) · [_demo_](https://og-examples.vercel.sh/api/external-data?username=rauchg)
96
97
  - Custom Font · [_source_](/examples/next/pages/api/custom-font.tsx) · [_demo_](https://og-examples.vercel.sh/api/custom-font)
97
98
  - Emoji · [_source_](/examples/next/pages/api/emoji.tsx) · [_demo_](https://og-examples.vercel.sh/api/emoji)
98
99
  - Languages · [_source_](/examples/next/pages/api/language.tsx) · [_demo_](https://og-examples.vercel.sh/api/language)
99
100
  - Encrypted Token · [_source_](/examples/next/pages/api/encrypted.tsx) · [_demo_](https://og-examples.vercel.sh/encrypted/a)
100
101
 
102
+
103
+ ## Development / Contributing
104
+
105
+ ### Playground
106
+ - `pnpm i` inside the `playground/` directory
107
+ - `pnpm dev` to start the Next.js app
108
+
109
+ ### Package
110
+ - `pnpm i` inside the root directory
111
+ - `pnpm build` to build the library
112
+ - `pnpm types` to generate the types
113
+
101
114
  ## Acknowledgements
102
115
 
103
116
  This project will not be possible without the following projects:
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import b,{init as S}from"satori/wasm";import E from"yoga-wasm-web";import*as i from"@resvg/resvg-wasm";import N from"../vendor/resvg.simd.wasm?module";import T from"../vendor/yoga.wasm?module";var v=String.fromCharCode(8205),j=/\uFE0F/g;function c(t){return y(t.indexOf(v)<0?t.replace(j,""):t)}function y(t){for(var n=[],e=0,o=0,s=0;s<t.length;)e=t.charCodeAt(s++),o?(n.push((65536+(o-55296<<10)+(e-56320)).toString(16)),o=0):55296<=e&&e<=56319?o=e:n.push(e.toString(16));return n.join("-")}var r={twemoji:t=>"https://twemoji.maxcdn.com/v/latest/svg/"+t.toLowerCase()+".svg",openmoji:"https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/",blobmoji:"https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/",noto:"https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/",fluent:t=>"https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/"+t.toLowerCase()+"_color.svg",fluentFlat:t=>"https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/"+t.toLowerCase()+"_flat.svg"};function m(t,n){(!n||!r[n])&&(n="twemoji");let e=r[n];return fetch(typeof e=="function"?e(t):`${e}${t.toUpperCase()}.svg`)}var C=i.initWasm(N),_=E(T).then(t=>S(t)),x=fetch(new URL("../vendor/noto-sans-v27-latin-regular.ttf",import.meta.url)).then(t=>t.arrayBuffer()),p,u,R=((u=(p=globalThis==null?void 0:globalThis.process)==null?void 0:p.env)==null?void 0:u.NODE_ENV)==="development",l={zh:"Noto+Sans+SC",ja:"Noto+Sans+JP",ko:"Noto+Sans+KR",th:"Noto+Sans+Thai",he:"Noto+Sans+Hebrew",ar:"Noto+Sans+Arabic",bn:"Noto+Sans+Bengali",ta:"Noto+Sans+Tamil",te:"Noto+Sans+Telugu",ml:"Noto+Sans+Malayalam",devanagari:"Noto+Sans+Devanagari",unknown:"Noto+Sans"};async function k(t,n){if(!t||!n)return;let e=`https://fonts.googleapis.com/css2?family=${t}&text=${encodeURIComponent(n)}`,s=(await(await fetch(e,{headers:{"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"}})).text()).match(/src: url\((.+)\) format\('(opentype|truetype)'\)/);if(!s)throw new Error("Failed to load font");return fetch(s[1]).then(a=>a.arrayBuffer())}var g=new Map,F=({emoji:t})=>{let n=async(e,o)=>{if(e==="emoji")return"data:image/svg+xml;base64,"+btoa(await(await m(c(o),t)).text());l[e]||(e="unknown");try{let s=await k(l[e],o);if(s)return{name:`satori_${e}_fallback_${o}`,data:s,weight:400,style:"normal"}}catch(s){console.error("Failed to load dynamic font for",o,". Error:",s)}};return async(...e)=>{let o=JSON.stringify(e),s=g.get(o);if(s)return s;let a=await n(...e);return g.set(o,a),a}},f=class{constructor(n,e={}){let o=Object.assign({width:1200,height:630,debug:!1},e),s=new ReadableStream({async start(a){await _,await C;let d=await x,h=await b(n,{width:o.width,height:o.height,debug:o.debug,fonts:o.fonts||[{name:"sans serif",data:d,weight:700,style:"normal"}],loadAdditionalAsset:F({emoji:o.emoji})}),w=new i.Resvg(h,{fitTo:{mode:"width",value:o.width}});a.enqueue(w.render()),a.close()}});return new Response(s,{headers:{"content-type":"image/png","cache-control":R?"no-cache, no-store":"public, immutable, no-transform, max-age=31536000",...o.headers},status:o.status,statusText:o.statusText})}};export{f as ImageResponse};
1
+ import b,{init as S}from"satori/wasm";import E from"yoga-wasm-web";import*as i from"@resvg/resvg-wasm";import N from"../vendor/resvg.simd.wasm?module";import T from"../vendor/yoga.wasm?module";var v=String.fromCharCode(8205),j=/\uFE0F/g;function c(t){return y(t.indexOf(v)<0?t.replace(j,""):t)}function y(t){for(var n=[],e=0,o=0,s=0;s<t.length;)e=t.charCodeAt(s++),o?(n.push((65536+(o-55296<<10)+(e-56320)).toString(16)),o=0):55296<=e&&e<=56319?o=e:n.push(e.toString(16));return n.join("-")}var r={twemoji:t=>"https://twemoji.maxcdn.com/v/latest/svg/"+t.toLowerCase()+".svg",openmoji:"https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/",blobmoji:"https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/",noto:"https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/",fluent:t=>"https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/"+t.toLowerCase()+"_color.svg",fluentFlat:t=>"https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/"+t.toLowerCase()+"_flat.svg"};function m(t,n){(!n||!r[n])&&(n="twemoji");let e=r[n];return fetch(typeof e=="function"?e(t):`${e}${t.toUpperCase()}.svg`)}var C=i.initWasm(N),x=E(T).then(t=>S(t)),_=fetch(new URL("../vendor/noto-sans-v27-latin-regular.ttf",import.meta.url)).then(t=>t.arrayBuffer()),f,u,R=((u=(f=globalThis==null?void 0:globalThis.process)==null?void 0:f.env)==null?void 0:u.NODE_ENV)==="development",l={zh:"Noto+Sans+SC",ja:"Noto+Sans+JP",ko:"Noto+Sans+KR",th:"Noto+Sans+Thai",he:"Noto+Sans+Hebrew",ar:"Noto+Sans+Arabic",bn:"Noto+Sans+Bengali",ta:"Noto+Sans+Tamil",te:"Noto+Sans+Telugu",ml:"Noto+Sans+Malayalam",devanagari:"Noto+Sans+Devanagari",unknown:"Noto+Sans"};async function k(t,n){if(!t||!n)return;let e=`https://fonts.googleapis.com/css2?family=${t}&text=${encodeURIComponent(n)}`,s=(await(await fetch(e,{headers:{"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"}})).text()).match(/src: url\((.+)\) format\('(opentype|truetype)'\)/);if(!s)throw new Error("Failed to load font");return fetch(s[1]).then(a=>a.arrayBuffer())}var g=new Map,F=({emoji:t})=>{let n=async(e,o)=>{if(e==="emoji")return"data:image/svg+xml;base64,"+btoa(await(await m(c(o),t)).text());l[e]||(e="unknown");try{let s=await k(l[e],o);if(s)return{name:`satori_${e}_fallback_${o}`,data:s,weight:400,style:"normal"}}catch(s){console.error("Failed to load dynamic font for",o,". Error:",s)}};return async(...e)=>{let o=JSON.stringify(e),s=g.get(o);if(s)return s;let a=await n(...e);return g.set(o,a),a}},p=class{constructor(n,e={}){let o=Object.assign({width:1200,height:630,debug:!1},e),s=new ReadableStream({async start(a){await x,await C;let d=await _,h=await b(n,{width:o.width,height:o.height,debug:o.debug,fonts:o.fonts||[{name:"sans serif",data:d,weight:700,style:"normal"}],loadAdditionalAsset:F({emoji:o.emoji})}),w=new i.Resvg(h,{fitTo:{mode:"width",value:o.width}});a.enqueue(w.render()),a.close()}});return new Response(s,{headers:{"content-type":"image/png","cache-control":R?"no-cache, no-store":"public, immutable, no-transform, max-age=31536000",...o.headers},status:o.status,statusText:o.statusText})}};export{p as ImageResponse};
2
2
  /*! Copyright Twitter Inc. and other contributors. Licensed under MIT */
3
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/og.ts","../src/emoji/index.ts"],"sourcesContent":["import type { ReactElement } from 'react'\nimport type { SatoriOptions } from 'satori'\n\n// @ts-ignore\nimport satori, { init as initSatori } from 'satori/wasm'\nimport initYoga from 'yoga-wasm-web'\nimport * as resvg from '@resvg/resvg-wasm'\n\n// @ts-ignore\nimport resvg_wasm from '../vendor/resvg.simd.wasm?module'\n// @ts-ignore\nimport yoga_wasm from '../vendor/yoga.wasm?module'\n\nimport { loadEmoji, getIconCode, EmojiType } from './emoji'\n\nconst initializedResvg = resvg.initWasm(resvg_wasm)\nconst initializedYoga = initYoga(yoga_wasm).then((yoga) => initSatori(yoga))\nconst fallbackFont = fetch(\n new URL('../vendor/noto-sans-v27-latin-regular.ttf', import.meta.url)\n).then((res) => res.arrayBuffer())\nconst isDev = globalThis?.process?.env?.NODE_ENV === 'development'\n\ntype ImageResponseOptions = ConstructorParameters<typeof Response>[1] & {\n /**\n * The width of the image.\n *\n * @type {number}\n * @default 1200\n */\n width?: number\n /**\n * The height of the image.\n *\n * @type {number}\n * @default 630\n */\n height?: number\n /**\n * Display debug information on the image.\n *\n * @type {boolean}\n * @default false\n */\n debug?: boolean\n /**\n * A list of fonts to use.\n *\n * @type {{ data: ArrayBuffer; name: string; weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900; style?: 'normal' | 'italic' }[]}\n * @default Noto Sans Latin Regular.\n */\n fonts?: SatoriOptions['fonts']\n /**\n * Using a specific Emoji style. Defaults to `twemoji`.\n *\n * @link https://github.com/vercel/og#emoji\n * @type {EmojiType}\n * @default 'twemoji'\n */\n emoji?: EmojiType\n}\n\n// @TODO: Support font style and weights, and make this option extensible rather\n// than built-in.\n// @TODO: Cover most languages with Noto Sans.\nconst languageFontMap = {\n zh: 'Noto+Sans+SC',\n ja: 'Noto+Sans+JP',\n ko: 'Noto+Sans+KR',\n th: 'Noto+Sans+Thai',\n he: 'Noto+Sans+Hebrew',\n ar: 'Noto+Sans+Arabic',\n bn: 'Noto+Sans+Bengali',\n ta: 'Noto+Sans+Tamil',\n te: 'Noto+Sans+Telugu',\n ml: 'Noto+Sans+Malayalam',\n devanagari: 'Noto+Sans+Devanagari',\n unknown: 'Noto+Sans',\n}\n\nasync function loadGoogleFont(font: string, text: string) {\n if (!font || !text) return\n\n const API = `https://fonts.googleapis.com/css2?family=${font}&text=${encodeURIComponent(\n text\n )}`\n\n const css = await (\n await fetch(API, {\n headers: {\n // Make sure it returns TTF.\n 'User-Agent':\n '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',\n },\n })\n ).text()\n\n const resource = css.match(/src: url\\((.+)\\) format\\('(opentype|truetype)'\\)/)\n if (!resource) throw new Error('Failed to load font')\n\n return fetch(resource[1]).then((res) => res.arrayBuffer())\n}\n\nconst assetCache = new Map<string, any>()\nconst loadDynamicAsset = ({ emoji }: { emoji?: EmojiType }) => {\n const fn = async (code, text) => {\n if (code === 'emoji') {\n // It's an emoji, load the image.\n return (\n `data:image/svg+xml;base64,` +\n btoa(await (await loadEmoji(getIconCode(text), emoji)).text())\n )\n }\n\n // Try to load from Google Fonts.\n if (!languageFontMap[code]) code = 'unknown'\n\n try {\n const data = await loadGoogleFont(languageFontMap[code], text)\n\n if (data) {\n return {\n name: `satori_${code}_fallback_${text}`,\n data,\n weight: 400,\n style: 'normal',\n }\n }\n } catch (e) {\n console.error('Failed to load dynamic font for', text, '. Error:', e)\n }\n }\n\n return async (...args: Parameters<typeof fn>) => {\n const key = JSON.stringify(args)\n const cache = assetCache.get(key)\n if (cache) return cache\n\n const asset = await fn(...args)\n assetCache.set(key, asset)\n return asset\n }\n}\n\nexport class ImageResponse {\n constructor(element: ReactElement, options: ImageResponseOptions = {}) {\n const extendedOptions = Object.assign(\n {\n width: 1200,\n height: 630,\n debug: false,\n },\n options\n )\n\n const result = new ReadableStream({\n async start(controller) {\n await initializedYoga\n await initializedResvg\n const fontData = await fallbackFont\n\n const svg = await satori(element, {\n width: extendedOptions.width,\n height: extendedOptions.height,\n debug: extendedOptions.debug,\n fonts: extendedOptions.fonts || [\n {\n name: 'sans serif',\n data: fontData,\n weight: 700,\n style: 'normal',\n },\n ],\n loadAdditionalAsset: loadDynamicAsset({\n emoji: extendedOptions.emoji,\n }),\n })\n\n const resvgJS = new resvg.Resvg(svg, {\n fitTo: {\n mode: 'width',\n value: extendedOptions.width,\n },\n })\n\n controller.enqueue(resvgJS.render())\n controller.close()\n },\n })\n\n return new Response(result, {\n headers: {\n 'content-type': 'image/png',\n 'cache-control': isDev\n ? 'no-cache, no-store'\n : 'public, immutable, no-transform, max-age=31536000',\n ...extendedOptions.headers,\n },\n status: extendedOptions.status,\n statusText: extendedOptions.statusText,\n })\n }\n}\n","/**\n * Modified version of https://unpkg.com/twemoji@13.1.0/dist/twemoji.esm.js.\n */\n\n/*! Copyright Twitter Inc. and other contributors. Licensed under MIT */\n\nconst U200D = String.fromCharCode(8205)\nconst UFE0Fg = /\\uFE0F/g\n\nexport function getIconCode(char: string) {\n return toCodePoint(char.indexOf(U200D) < 0 ? char.replace(UFE0Fg, '') : char)\n}\n\nfunction toCodePoint(unicodeSurrogates: string) {\n var r: string[] = [],\n c = 0,\n p = 0,\n i = 0\n while (i < unicodeSurrogates.length) {\n c = unicodeSurrogates.charCodeAt(i++)\n if (p) {\n r.push((65536 + ((p - 55296) << 10) + (c - 56320)).toString(16))\n p = 0\n } else if (55296 <= c && c <= 56319) {\n p = c\n } else {\n r.push(c.toString(16))\n }\n }\n return r.join('-')\n}\n\nconst apis = {\n twemoji: (code) =>\n 'https://twemoji.maxcdn.com/v/latest/svg/' + code.toLowerCase() + '.svg',\n openmoji: 'https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/',\n blobmoji: 'https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/',\n noto: 'https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/',\n fluent: (code) =>\n 'https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/' +\n code.toLowerCase() +\n '_color.svg',\n fluentFlat: (code) =>\n 'https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/' +\n code.toLowerCase() +\n '_flat.svg',\n}\n\nexport type EmojiType = keyof typeof apis\n\nexport function loadEmoji(code: string, type?: EmojiType) {\n // https://github.com/svgmoji/svgmoji\n if (!type || !apis[type]) {\n type = 'twemoji'\n }\n const api = apis[type]\n if (typeof api === 'function') {\n return fetch(api(code))\n }\n return fetch(`${api}${code.toUpperCase()}.svg`)\n}\n"],"mappings":"AAIA,sCACA,6BACA,oCAGA,gDAEA,0CCPA,AAEA,GAAM,GAAQ,OAAO,aAAa,IAAI,EAChC,EAAS,UAER,WAAqB,EAAc,CACxC,MAAO,GAAY,EAAK,QAAQ,CAAK,EAAI,EAAI,EAAK,QAAQ,EAAQ,EAAE,EAAI,CAAI,CAC9E,CAEA,WAAqB,EAA2B,CAK9C,OAJI,GAAc,CAAC,EACjB,EAAI,EACJ,EAAI,EACJ,EAAI,EACC,EAAI,EAAkB,QAC3B,EAAI,EAAkB,WAAW,GAAG,EACpC,AAAI,EACF,GAAE,KAAM,OAAU,GAAI,OAAU,IAAO,GAAI,QAAQ,SAAS,EAAE,CAAC,EAC/D,EAAI,GACC,AAAI,OAAS,GAAK,GAAK,MAC5B,EAAI,EAEJ,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,EAGzB,MAAO,GAAE,KAAK,GAAG,CACnB,CAEA,GAAM,GAAO,CACX,QAAS,AAAC,GACR,2CAA6C,EAAK,YAAY,EAAI,OACpE,SAAU,4DACV,SAAU,wDACV,KAAM,0EACN,OAAQ,AAAC,GACP,qEACA,EAAK,YAAY,EACjB,aACF,WAAY,AAAC,GACX,qEACA,EAAK,YAAY,EACjB,WACJ,EAIO,WAAmB,EAAc,EAAkB,CAExD,AAAI,EAAC,GAAQ,CAAC,EAAK,KACjB,GAAO,WAET,GAAM,GAAM,EAAK,GACjB,MAAI,AACK,OADL,MAAO,IAAQ,WACJ,EAAI,CAAI,EAEV,GAAG,IAAM,EAAK,YAAY,OAFf,CAG1B,CD7CA,GAAM,GAAmB,AAAM,WAAS,CAAU,EAC5C,EAAkB,EAAS,CAAS,EAAE,KAAK,AAAC,GAAS,EAAW,CAAI,CAAC,EACrE,EAAe,MACnB,GAAI,KAAI,4CAA6C,YAAY,GAAG,CACtE,EAAE,KAAK,AAAC,GAAQ,EAAI,YAAY,CAAC,EAnBjC,IAoBM,EAAQ,0CAAY,UAAZ,cAAqB,MAArB,cAA0B,YAAa,cA4C/C,EAAkB,CACtB,GAAI,eACJ,GAAI,eACJ,GAAI,eACJ,GAAI,iBACJ,GAAI,mBACJ,GAAI,mBACJ,GAAI,oBACJ,GAAI,kBACJ,GAAI,mBACJ,GAAI,sBACJ,WAAY,uBACZ,QAAS,WACX,EAEA,iBAA8B,EAAc,EAAc,CACxD,GAAI,CAAC,GAAQ,CAAC,EAAM,OAEpB,GAAM,GAAM,4CAA4C,UAAa,mBACnE,CACF,IAYM,EAAW,AAVL,MACV,MAAM,OAAM,EAAK,CACf,QAAS,CAEP,aACE,iIACJ,CACF,CAAC,GACD,KAAK,GAEc,MAAM,kDAAkD,EAC7E,GAAI,CAAC,EAAU,KAAM,IAAI,OAAM,qBAAqB,EAEpD,MAAO,OAAM,EAAS,EAAE,EAAE,KAAK,AAAC,GAAQ,EAAI,YAAY,CAAC,CAC3D,CAEA,GAAM,GAAa,GAAI,KACjB,EAAmB,CAAC,CAAE,WAAmC,CAC7D,GAAM,GAAK,MAAO,EAAM,IAAS,CAC/B,GAAI,IAAS,QAEX,MACE,6BACA,KAAK,KAAO,MAAM,GAAU,EAAY,CAAI,EAAG,CAAK,GAAG,KAAK,CAAC,EAKjE,AAAK,EAAgB,IAAO,GAAO,WAEnC,GAAI,CACF,GAAM,GAAO,KAAM,GAAe,EAAgB,GAAO,CAAI,EAE7D,GAAI,EACF,MAAO,CACL,KAAM,UAAU,cAAiB,IACjC,OACA,OAAQ,IACR,MAAO,QACT,CAEJ,OAAS,EAAP,CACA,QAAQ,MAAM,kCAAmC,EAAM,WAAY,CAAC,CACtE,CACF,EAEA,MAAO,UAAU,IAAgC,CAC/C,GAAM,GAAM,KAAK,UAAU,CAAI,EACzB,EAAQ,EAAW,IAAI,CAAG,EAChC,GAAI,EAAO,MAAO,GAElB,GAAM,GAAQ,KAAM,GAAG,GAAG,CAAI,EAC9B,SAAW,IAAI,EAAK,CAAK,EAClB,CACT,CACF,EAEa,EAAN,KAAoB,CACzB,YAAY,EAAuB,EAAgC,CAAC,EAAG,CACrE,GAAM,GAAkB,OAAO,OAC7B,CACE,MAAO,KACP,OAAQ,IACR,MAAO,EACT,EACA,CACF,EAEM,EAAS,GAAI,gBAAe,CAChC,KAAM,OAAM,EAAY,CACtB,KAAM,GACN,KAAM,GACN,GAAM,GAAW,KAAM,GAEjB,EAAM,KAAM,GAAO,EAAS,CAChC,MAAO,EAAgB,MACvB,OAAQ,EAAgB,OACxB,MAAO,EAAgB,MACvB,MAAO,EAAgB,OAAS,CAC9B,CACE,KAAM,aACN,KAAM,EACN,OAAQ,IACR,MAAO,QACT,CACF,EACA,oBAAqB,EAAiB,CACpC,MAAO,EAAgB,KACzB,CAAC,CACH,CAAC,EAEK,EAAU,GAAU,SAAM,EAAK,CACnC,MAAO,CACL,KAAM,QACN,MAAO,EAAgB,KACzB,CACF,CAAC,EAED,EAAW,QAAQ,EAAQ,OAAO,CAAC,EACnC,EAAW,MAAM,CACnB,CACF,CAAC,EAED,MAAO,IAAI,UAAS,EAAQ,CAC1B,QAAS,CACP,eAAgB,YAChB,gBAAiB,EACb,qBACA,oDACJ,GAAG,EAAgB,OACrB,EACA,OAAQ,EAAgB,OACxB,WAAY,EAAgB,UAC9B,CAAC,CACH,CACF","names":[]}
1
+ {"version":3,"sources":["../src/og.ts","../src/emoji/index.ts"],"sourcesContent":["import type { ReactElement } from 'react'\nimport type { SatoriOptions } from 'satori'\n\n// @ts-ignore\nimport satori, { init as initSatori } from 'satori/wasm'\nimport initYoga from 'yoga-wasm-web'\nimport * as resvg from '@resvg/resvg-wasm'\n\n// @ts-ignore\nimport resvg_wasm from '../vendor/resvg.simd.wasm?module'\n// @ts-ignore\nimport yoga_wasm from '../vendor/yoga.wasm?module'\n\nimport { loadEmoji, getIconCode, EmojiType } from './emoji'\n\nconst initializedResvg = resvg.initWasm(resvg_wasm)\nconst initializedYoga = initYoga(yoga_wasm).then((yoga) => initSatori(yoga))\nconst fallbackFont = fetch(\n new URL('../vendor/noto-sans-v27-latin-regular.ttf', import.meta.url)\n).then((res) => res.arrayBuffer())\nconst isDev = globalThis?.process?.env?.NODE_ENV === 'development'\n\nexport type ImageResponseOptions = ConstructorParameters<typeof Response>[1] & {\n /**\n * The width of the image.\n *\n * @type {number}\n * @default 1200\n */\n width?: number\n /**\n * The height of the image.\n *\n * @type {number}\n * @default 630\n */\n height?: number\n /**\n * Display debug information on the image.\n *\n * @type {boolean}\n * @default false\n */\n debug?: boolean\n /**\n * A list of fonts to use.\n *\n * @type {{ data: ArrayBuffer; name: string; weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900; style?: 'normal' | 'italic' }[]}\n * @default Noto Sans Latin Regular.\n */\n fonts?: SatoriOptions['fonts']\n /**\n * Using a specific Emoji style. Defaults to `twemoji`.\n *\n * @link https://github.com/vercel/og#emoji\n * @type {EmojiType}\n * @default 'twemoji'\n */\n emoji?: EmojiType\n}\n\n// @TODO: Support font style and weights, and make this option extensible rather\n// than built-in.\n// @TODO: Cover most languages with Noto Sans.\nconst languageFontMap = {\n zh: 'Noto+Sans+SC',\n ja: 'Noto+Sans+JP',\n ko: 'Noto+Sans+KR',\n th: 'Noto+Sans+Thai',\n he: 'Noto+Sans+Hebrew',\n ar: 'Noto+Sans+Arabic',\n bn: 'Noto+Sans+Bengali',\n ta: 'Noto+Sans+Tamil',\n te: 'Noto+Sans+Telugu',\n ml: 'Noto+Sans+Malayalam',\n devanagari: 'Noto+Sans+Devanagari',\n unknown: 'Noto+Sans',\n}\n\nasync function loadGoogleFont(font: string, text: string) {\n if (!font || !text) return\n\n const API = `https://fonts.googleapis.com/css2?family=${font}&text=${encodeURIComponent(\n text\n )}`\n\n const css = await (\n await fetch(API, {\n headers: {\n // Make sure it returns TTF.\n 'User-Agent':\n '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',\n },\n })\n ).text()\n\n const resource = css.match(/src: url\\((.+)\\) format\\('(opentype|truetype)'\\)/)\n if (!resource) throw new Error('Failed to load font')\n\n return fetch(resource[1]).then((res) => res.arrayBuffer())\n}\n\nconst assetCache = new Map<string, any>()\nconst loadDynamicAsset = ({ emoji }: { emoji?: EmojiType }) => {\n const fn = async (code, text) => {\n if (code === 'emoji') {\n // It's an emoji, load the image.\n return (\n `data:image/svg+xml;base64,` +\n btoa(await (await loadEmoji(getIconCode(text), emoji)).text())\n )\n }\n\n // Try to load from Google Fonts.\n if (!languageFontMap[code]) code = 'unknown'\n\n try {\n const data = await loadGoogleFont(languageFontMap[code], text)\n\n if (data) {\n return {\n name: `satori_${code}_fallback_${text}`,\n data,\n weight: 400,\n style: 'normal',\n }\n }\n } catch (e) {\n console.error('Failed to load dynamic font for', text, '. Error:', e)\n }\n }\n\n return async (...args: Parameters<typeof fn>) => {\n const key = JSON.stringify(args)\n const cache = assetCache.get(key)\n if (cache) return cache\n\n const asset = await fn(...args)\n assetCache.set(key, asset)\n return asset\n }\n}\n\nexport class ImageResponse {\n constructor(element: ReactElement, options: ImageResponseOptions = {}) {\n const extendedOptions = Object.assign(\n {\n width: 1200,\n height: 630,\n debug: false,\n },\n options\n )\n\n const result = new ReadableStream({\n async start(controller) {\n await initializedYoga\n await initializedResvg\n const fontData = await fallbackFont\n\n const svg = await satori(element, {\n width: extendedOptions.width,\n height: extendedOptions.height,\n debug: extendedOptions.debug,\n fonts: extendedOptions.fonts || [\n {\n name: 'sans serif',\n data: fontData,\n weight: 700,\n style: 'normal',\n },\n ],\n loadAdditionalAsset: loadDynamicAsset({\n emoji: extendedOptions.emoji,\n }),\n })\n\n const resvgJS = new resvg.Resvg(svg, {\n fitTo: {\n mode: 'width',\n value: extendedOptions.width,\n },\n })\n\n controller.enqueue(resvgJS.render())\n controller.close()\n },\n })\n\n return new Response(result, {\n headers: {\n 'content-type': 'image/png',\n 'cache-control': isDev\n ? 'no-cache, no-store'\n : 'public, immutable, no-transform, max-age=31536000',\n ...extendedOptions.headers,\n },\n status: extendedOptions.status,\n statusText: extendedOptions.statusText,\n })\n }\n}\n","/**\n * Modified version of https://unpkg.com/twemoji@13.1.0/dist/twemoji.esm.js.\n */\n\n/*! Copyright Twitter Inc. and other contributors. Licensed under MIT */\n\nconst U200D = String.fromCharCode(8205)\nconst UFE0Fg = /\\uFE0F/g\n\nexport function getIconCode(char: string) {\n return toCodePoint(char.indexOf(U200D) < 0 ? char.replace(UFE0Fg, '') : char)\n}\n\nfunction toCodePoint(unicodeSurrogates: string) {\n var r: string[] = [],\n c = 0,\n p = 0,\n i = 0\n while (i < unicodeSurrogates.length) {\n c = unicodeSurrogates.charCodeAt(i++)\n if (p) {\n r.push((65536 + ((p - 55296) << 10) + (c - 56320)).toString(16))\n p = 0\n } else if (55296 <= c && c <= 56319) {\n p = c\n } else {\n r.push(c.toString(16))\n }\n }\n return r.join('-')\n}\n\nconst apis = {\n twemoji: (code) =>\n 'https://twemoji.maxcdn.com/v/latest/svg/' + code.toLowerCase() + '.svg',\n openmoji: 'https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/',\n blobmoji: 'https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/',\n noto: 'https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/',\n fluent: (code) =>\n 'https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/' +\n code.toLowerCase() +\n '_color.svg',\n fluentFlat: (code) =>\n 'https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/' +\n code.toLowerCase() +\n '_flat.svg',\n}\n\nexport type EmojiType = keyof typeof apis\n\nexport function loadEmoji(code: string, type?: EmojiType) {\n // https://github.com/svgmoji/svgmoji\n if (!type || !apis[type]) {\n type = 'twemoji'\n }\n const api = apis[type]\n if (typeof api === 'function') {\n return fetch(api(code))\n }\n return fetch(`${api}${code.toUpperCase()}.svg`)\n}\n"],"mappings":"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","names":[]}
package/dist/og.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { ReactElement } from 'react';
2
2
  import type { SatoriOptions } from 'satori';
3
3
  import { EmojiType } from './emoji';
4
- declare type ImageResponseOptions = ConstructorParameters<typeof Response>[1] & {
4
+ export declare type ImageResponseOptions = ConstructorParameters<typeof Response>[1] & {
5
5
  /**
6
6
  * The width of the image.
7
7
  *
@@ -42,4 +42,3 @@ declare type ImageResponseOptions = ConstructorParameters<typeof Response>[1] &
42
42
  export declare class ImageResponse {
43
43
  constructor(element: ReactElement, options?: ImageResponseOptions);
44
44
  }
45
- export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/og",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "Generate Open Graph Images dynamically from HTML/CSS without a browser",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",