@azuro-org/images-generator 0.0.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/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ 16.15.1
package/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ nodejs 16.15.1
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ > This readme is for Developers only.
2
+
3
+ ## Current packages
4
+
5
+ [@azuro-protocol/nft-image-generator](https://www.npmjs.com/package/@azuro-protocol/nft-image-generator)<br />
6
+ [@azuro-protocol/bet-og-image-generator](https://www.npmjs.com/package/@azuro-protocol/bet-og-image-generator)
7
+
8
+
9
+ ## Usage
10
+
11
+ ### Types Declaration
12
+
13
+ ```typescript
14
+ import { generateImage } from '@azuro-org/image-generator';
15
+ import template, { type Props } from '@azuro-org/image-generator/templates/bet-nft';
16
+
17
+ const props: Props = {
18
+ type: 'match',
19
+ sport: 'soccer',
20
+ league: 'Leinster Senior League Senior Division',
21
+ team1: {
22
+ img: 'https://content.bookieratings.net/images/fq/tx/fqtxnf_20181001112329_100x100.png',
23
+ name: 'Nizhny Novgorod'
24
+ },
25
+ team2: {
26
+ img: 'https://content.bookieratings.net/images/fq/tx/fqtxnf_20181001112329_100x100.png',
27
+ name: 'Lokomotiv Moscow'
28
+ },
29
+ date: '21.03.2022 8:00 UTC',
30
+ betAmount: '100 USDC',
31
+ outcome: 'Total Under(2.5)',
32
+ betOdds: '2.88',
33
+ currentOdds: '1.88'
34
+ }
35
+
36
+ // get image buffer
37
+ const buffer = generateImage({
38
+ template,
39
+ props,
40
+ })
41
+
42
+ // create image file
43
+ generateImage({
44
+ template,
45
+ props,
46
+ output: './dist',
47
+ })
48
+ ```
49
+
50
+ ## Options
51
+
52
+ ```
53
+ type PuppeteerOptions = Parameters<typeof puppeteer.launch>[0]
54
+
55
+ type PuppeteerInitialOptions = {
56
+ headless: boolean
57
+ devtools: boolean
58
+ args: string[]
59
+ }
60
+
61
+ generateImage({
62
+ output?: string // output filepath
63
+ filename?: string // default "image"
64
+ props: any
65
+ modifyPuppeteerOptions?(options: PuppeteerInitialOptions): PuppeteerOptions
66
+ })
67
+ ```
68
+
69
+
70
+ # Contributing
71
+
72
+ ## Add new template
73
+
74
+ 1. Copy `templates/_template` to `templates/{your_template_name}`.
75
+ 3. Use `index.html` for HTML. Write CSS in `index.html` file.
76
+ 4. Create `templates/{your_template_name}/images` folder for images if required.
77
+
78
+
79
+ ## Setup generator
80
+
81
+ Edit `{your_template_name}/index.ts` file:
82
+
83
+ ```typescript
84
+ import { type Template, getFile, downloadImage, createGenerator } from '../../utils'
85
+
86
+ export type Props = {
87
+ team1ImageSrc: string
88
+ team2ImageSrc: string
89
+ date: string
90
+ }
91
+
92
+ const template = {
93
+ width: 800,
94
+ height: 400,
95
+ type: 'jpeg',
96
+ html: async (props: Props) => {
97
+ const { team1ImageSrc, team2ImageSrc, date } = props
98
+
99
+ let html = getFile('./index.html')
100
+ let css = getFile('./index.css')
101
+
102
+ const team1Img = await downloadImage(team1ImageSrc)
103
+ const team2Img = await downloadImage(team2ImageSrc)
104
+
105
+ return html
106
+ .replace('.style{}', css)
107
+ .replace('{image1}', team1Img)
108
+ .replace('{image2}', team2Img)
109
+ .replace('{date}', date)
110
+ },
111
+ }
112
+
113
+ export default template
114
+ ```
115
+
116
+
117
+ ## `createGenerator` options
118
+
119
+ `type: 'png' | 'jpeg'`<br /><br />
120
+ `headless: Boolean` - use true to see compiled html in browser<br /><br />
121
+ `scaleFactor: 1 | 2` - use 2 if you need to generate x2 sized image
122
+
123
+
124
+ ## Publish
125
+
126
+ Publish npm package with `npm run publish`. For access to `@azuro-org` scope ask Pavel Ivanov or Stas Onatskiy.
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@azuro-org/images-generator",
3
+ "version": "0.0.1",
4
+ "license": "ISC",
5
+ "engines": {
6
+ "node": "=16.15.1",
7
+ "npm": "=8.11.0"
8
+ },
9
+ "typings": "./index.d.ts",
10
+ "scripts": {
11
+ "dev": "rollup -c ./rollup.config.js -w",
12
+ "build": "rimraf ./dist && rimraf ./lib && rollup -c ./rollup.config.js --compact",
13
+ "build-and-test": "npm run build && npm run test",
14
+ "prepublish": "npm run build"
15
+ },
16
+ "dependencies": {
17
+ "axios": "^0.26.1",
18
+ "builtin-modules": "^3.2.0",
19
+ "dayjs": "^1.11.7",
20
+ "puppeteer": "^19.3.0"
21
+ },
22
+ "devDependencies": {
23
+ "@babel/core": "^7.17.0",
24
+ "@babel/plugin-proposal-object-rest-spread": "^7.16.7",
25
+ "@babel/plugin-transform-destructuring": "^7.16.7",
26
+ "@babel/plugin-transform-runtime": "^7.17.0",
27
+ "@babel/preset-env": "^7.16.11",
28
+ "@babel/preset-react": "^7.16.7",
29
+ "@babel/preset-typescript": "^7.16.7",
30
+ "@rollup/plugin-babel": "^6.0.3",
31
+ "@rollup/plugin-commonjs": "^23.0.3",
32
+ "@rollup/plugin-json": "^4.1.0",
33
+ "@rollup/plugin-node-resolve": "^15.0.1",
34
+ "@types/node": "^17.0.21",
35
+ "glob": "^8.1.0",
36
+ "rimraf": "^3.0.2",
37
+ "rollup": "^2.67.0",
38
+ "rollup-plugin-babel": "^4.4.0",
39
+ "rollup-plugin-copy": "^3.4.0",
40
+ "rollup-plugin-typescript2": "^0.34.1",
41
+ "tslib": "^2.4.1",
42
+ "typescript": "^4.6.2"
43
+ }
44
+ }
@@ -0,0 +1,107 @@
1
+ import glob from 'glob'
2
+ import path from 'node:path'
3
+ import { nodeResolve } from '@rollup/plugin-node-resolve'
4
+ import builtins from 'builtin-modules/static'
5
+ import commonjs from '@rollup/plugin-commonjs'
6
+ import typescript from 'rollup-plugin-typescript2'
7
+ import babel from 'rollup-plugin-babel'
8
+ import json from '@rollup/plugin-json'
9
+ import copy from 'rollup-plugin-copy'
10
+
11
+
12
+ const templateFolders = (
13
+ glob.sync('src/templates/*')
14
+ .filter((folder) => !folder.includes('_template'))
15
+ .map((folder) => path.relative('src', folder))
16
+ )
17
+
18
+ const TARGETS_TO_COPY = [
19
+ 'index.html',
20
+ 'images',
21
+ ]
22
+
23
+ const main = {
24
+ input: './src/index.ts',
25
+ output: [
26
+ {
27
+ file: './lib/index.js',
28
+ format: 'cjs',
29
+ exports: 'named',
30
+ },
31
+ {
32
+ file: './dist/index.es.js',
33
+ format: 'es',
34
+ exports: 'named',
35
+ },
36
+ ],
37
+ external: [
38
+ ...builtins,
39
+ 'puppeteer',
40
+ ],
41
+ plugins: [
42
+ nodeResolve(),
43
+ commonjs(),
44
+ json(),
45
+ babel({
46
+ exclude: 'node_modules/**',
47
+ }),
48
+ typescript({
49
+ tsconfigOverride: {
50
+ include: [
51
+ 'src/index.ts',
52
+ ],
53
+ },
54
+ clean: true,
55
+ }),
56
+ copy({
57
+ targets: templateFolders.map((folder) => (
58
+ TARGETS_TO_COPY.map((fileName) => (
59
+ [ 'dist', 'lib' ].map((dest) => ({
60
+ src: path.join('src', folder, fileName),
61
+ dest: path.join(dest, folder),
62
+ })).flat()
63
+ )).flat()
64
+ )).flat(),
65
+ })
66
+ ],
67
+ }
68
+
69
+ const templates = {
70
+ input: Object.fromEntries(
71
+ templateFolders.map((file) => [
72
+ path.join(file, 'index'),
73
+ path.resolve(`src/${file}/index.ts`),
74
+ ])
75
+ ),
76
+ output: [
77
+ {
78
+ dir: 'lib',
79
+ format: 'cjs',
80
+ exports: 'named',
81
+ },
82
+ {
83
+ dir: 'dist',
84
+ format: 'es',
85
+ exports: 'named',
86
+ },
87
+ ],
88
+ external: [
89
+ ...builtins,
90
+ ],
91
+ plugins: [
92
+ nodeResolve(),
93
+ commonjs(),
94
+ json(),
95
+ babel({
96
+ exclude: 'node_modules/**',
97
+ }),
98
+ typescript({
99
+ clean: true,
100
+ }),
101
+ ],
102
+ }
103
+
104
+ export default [
105
+ main,
106
+ templates,
107
+ ]
File without changes
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default as generateImage } from './utils/generateImage'
@@ -0,0 +1,13 @@
1
+ html {
2
+
3
+ }
4
+
5
+ html, body {
6
+ margin: 0;
7
+ padding: 0;
8
+ width: fit-content;
9
+ }
10
+
11
+ * {
12
+ box-sizing: border-box;
13
+ }
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>Document</title>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
8
+ <style>.style{}</style>
9
+ </head>
10
+ <body>
11
+ <!-- Write your HTML here -->
12
+ </body>
13
+ </html>
@@ -0,0 +1,22 @@
1
+ import { type Template, getFile, getBase64Image, downloadImage } from '../../utils'
2
+
3
+
4
+ export type Props = {
5
+
6
+ }
7
+
8
+ const template: Template = {
9
+ width: 1000,
10
+ height: 1000,
11
+ type: 'png',
12
+ html: (props: Props) => {
13
+ const { } = props
14
+
15
+ let html = getFile('./index.html')
16
+ let css = getFile('./index.css')
17
+
18
+ return html.replace('.style{}', css)
19
+ }
20
+ }
21
+
22
+ export default template
@@ -0,0 +1,325 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
8
+ <style>
9
+ html {
10
+ font-family: 'Inter', sans-serif;
11
+ font-weight: 500;
12
+ }
13
+
14
+ html, body {
15
+ margin: 0;
16
+ width: fit-content;
17
+ }
18
+
19
+ * {
20
+ box-sizing: border-box;
21
+ }
22
+
23
+ .flex {
24
+ display: flex;
25
+ }
26
+
27
+ .flex-wrap {
28
+ flex-wrap: wrap;
29
+ }
30
+
31
+ .flex-1 {
32
+ flex: 1 1 0;
33
+ }
34
+
35
+ .flex-auto {
36
+ flex: 1 1 auto;
37
+ }
38
+
39
+ .flex-none {
40
+ flex: none;
41
+ }
42
+
43
+
44
+ .items-center {
45
+ align-items: center;
46
+ }
47
+
48
+ .items-start {
49
+ align-items: flex-start;
50
+ }
51
+
52
+ .items-end {
53
+ align-items: flex-end;
54
+ }
55
+
56
+ .items-baseline {
57
+ align-items: baseline;
58
+ }
59
+
60
+ .items-stretch {
61
+ align-items: stretch;
62
+ }
63
+
64
+ .self-auto {
65
+ align-self: auto;
66
+ }
67
+
68
+ .self-start {
69
+ align-self: flex-start;
70
+ }
71
+
72
+ .self-end {
73
+ align-self: flex-end;
74
+ }
75
+
76
+ .self-center {
77
+ align-self: center;
78
+ }
79
+
80
+ .self-stretch {
81
+ align-self: stretch;
82
+ }
83
+
84
+ .justify-around {
85
+ justify-content: space-around;
86
+ }
87
+
88
+ .justify-between {
89
+ justify-content: space-between;
90
+ }
91
+
92
+ .justify-center {
93
+ justify-content: center;
94
+ }
95
+
96
+ .justify-start {
97
+ justify-content: flex-start;
98
+ }
99
+
100
+ .justify-end {
101
+ justify-content: flex-end;
102
+ }
103
+
104
+ .flex-row {
105
+ flex-direction: row;
106
+ }
107
+
108
+ .flex-col {
109
+ flex-direction: column;
110
+ }
111
+
112
+ .flex-col-reverse {
113
+ flex-direction: column-reverse;
114
+ }
115
+
116
+ .text-upper {
117
+ text-transform: uppercase;
118
+ }
119
+
120
+ .text-center {
121
+ text-align: center;
122
+ }
123
+
124
+ .text-600 {
125
+ font-weight: 600;
126
+ }
127
+
128
+ .text-400 {
129
+ font-weight: 400;
130
+ }
131
+
132
+ .card {
133
+ width: 510px;
134
+ position: relative;
135
+ padding: 24px 16px 16px;
136
+ border: 1px solid rgba(0, 0, 0, 0.1);
137
+ border-radius: 16px;
138
+ background: #FFFFFF;
139
+ }
140
+
141
+ .logo {
142
+ position: relative;
143
+ z-index: 1;
144
+ margin-bottom: 40px;
145
+ }
146
+
147
+ .shadow {
148
+ position: absolute;
149
+ left: 0;
150
+ width: 100%;
151
+ top: 0;
152
+ }
153
+
154
+ .sport {
155
+ margin-bottom: 8px;
156
+ font-size: 16px;
157
+ line-height: 24px;
158
+ }
159
+
160
+ .league {
161
+ font-size: 18px;
162
+ line-height: 27px
163
+ }
164
+
165
+ .sport::before, .sport::after {
166
+ content: '';
167
+ display: block;
168
+ border-radius: 50%;
169
+ width: 4px;
170
+ height: 4px;
171
+ margin: 0 8px;
172
+ background: #000000;
173
+ }
174
+
175
+ .teams {
176
+ margin: 24px 0 16px;
177
+ }
178
+
179
+ .team {
180
+ width: 100%;
181
+ background: #FAFAFA;
182
+ border-radius: 12px;
183
+ padding: 16px;
184
+ }
185
+
186
+ .team img {
187
+ display: block;
188
+ width: 72px;
189
+ height: 72px;
190
+ margin: 0 auto;
191
+ }
192
+
193
+ .date {
194
+ opacity: 0.6;
195
+ font-size: 16px;
196
+ line-height: 24px;
197
+ }
198
+
199
+ .game {
200
+ font-size: 24px;
201
+ line-height: 36px;
202
+ }
203
+
204
+ .table {
205
+ border: 1px solid transparent;
206
+ border-radius: 12px;
207
+ overflow: hidden;
208
+ margin-top: 16px;
209
+ background: linear-gradient(180deg, rgba(250, 250, 250, 0) 0%, #FAFAFA 100%);
210
+ }
211
+
212
+ .table__head {
213
+ font-size: 14px;
214
+ line-height: 21px;
215
+ padding: 8px;
216
+ text-transform: capitalize;
217
+ }
218
+
219
+ .table__content {
220
+ padding: 24px
221
+ }
222
+
223
+ .table__item {
224
+ font-size: 20px;
225
+ line-height: 30px;
226
+ margin-top: 4px;
227
+ }
228
+
229
+ .table__item:first-child {
230
+ margin-top: 0;
231
+ }
232
+
233
+ .table_match {
234
+ border-color: #007FFF;
235
+ }
236
+
237
+ .table_match .table__head {
238
+ color: #007FFF;
239
+ background: #E0EEFE;
240
+ }
241
+
242
+ .table_claim {
243
+ border-color: #FFA000;
244
+ }
245
+
246
+ .table_claim .table__head {
247
+ color: #FFA000;
248
+ background: #FFF8E1;
249
+ }
250
+
251
+ .table_claimed {
252
+ border-color: #4CAF50;
253
+ }
254
+
255
+ .table_claimed .table__head {
256
+ color: #4CAF50;
257
+ background: #E8F5E9;
258
+ }
259
+
260
+ .table_lose {
261
+ border-color: #A3A3A3;
262
+ }
263
+
264
+ .table_lose .table__head {
265
+ color: #A3A3A3;
266
+ background: rgba(163, 163, 163, 0.12);
267
+ }
268
+
269
+ .table_canceled {
270
+ border-color: #FF1717;
271
+ }
272
+
273
+ .table_canceled .table__head {
274
+ color: #FF1717;
275
+ background: rgba(255, 23, 23, 0.12);
276
+ }
277
+ </style>
278
+ </head>
279
+ <body>
280
+ <div class="card" id="card">
281
+ <img src="{shadow}" alt="" class="shadow">
282
+ <div class="logo text-center">
283
+ <img src="{logo}" alt="">
284
+ </div>
285
+ <div id="sport" class="sport flex items-center justify-center text-upper text-600">{sport}</div>
286
+ <div id="league" class="league text-400 text-center">{league}</div>
287
+ <div class="teams flex">
288
+ <div class="team text-center">
289
+ <img id="team1-img" src="{image1}" alt="">
290
+ </div>
291
+ <img src="{separator}" alt="">
292
+ <div class="team text-center">
293
+ <img id="team2-img" src="{image2}" alt="">
294
+ </div>
295
+ </div>
296
+ <div id="date" class="date text-center text-400">
297
+ {date}
298
+ </div>
299
+ <div id="teams" class="game text-center text-600">
300
+ {game}
301
+ </div>
302
+ <div id="table" class="table table_{tableType}">
303
+ <div class="table__head text-center">{tableHead}</div>
304
+ <div class="table__content">
305
+ <div class="table__item flex items-center justify-between">
306
+ <div class="table__item__name text-400">Bet Amount:</div>
307
+ <div class="table__item__value text-600">{betAmount}</div>
308
+ </div>
309
+ <div class="table__item flex items-center justify-between">
310
+ <div class="table__item__name text-400">Outcome:</div>
311
+ <div class="table__item__value text-600">{outcome}</div>
312
+ </div>
313
+ <div class="table__item flex items-center justify-between">
314
+ <div class="table__item__name text-400">Bet odds:</div>
315
+ <div class="table__item__value text-600">{betOdds}</div>
316
+ </div>
317
+ <div class="table__item flex items-center justify-between">
318
+ <div class="table__item__name text-400">Current odds:</div>
319
+ <div class="table__item__value text-600">{currentOdds}</div>
320
+ </div>
321
+ </div>
322
+ </div>
323
+ </div>
324
+ </body>
325
+ </html>
@@ -0,0 +1,69 @@
1
+ import { type Template, getFile, getBase64Image, downloadImage } from '../../utils'
2
+
3
+
4
+ const matchType = {
5
+ 'match': 'Waiting for match',
6
+ 'claim': 'Waiting for claim',
7
+ 'claimed': 'Claimed',
8
+ 'lose': 'Lose',
9
+ 'canceled': 'Canceled match'
10
+ } as const
11
+
12
+ type MatchType = keyof typeof matchType
13
+
14
+ type Team = {
15
+ img: string
16
+ name: string
17
+ }
18
+
19
+ export type Props = {
20
+ type: MatchType
21
+ sport: string
22
+ league: string
23
+ team1: Team
24
+ team2: Team
25
+ date: string
26
+ betAmount: string
27
+ outcome: string
28
+ betOdds: string
29
+ currentOdds: string
30
+ }
31
+
32
+ const template: Template = {
33
+ width: 510,
34
+ height: 510,
35
+ type: 'png',
36
+ html: async (props: Props) => {
37
+ const { type, sport, league, team1, team2, date, betAmount, outcome, betOdds, currentOdds } = props
38
+
39
+ let html = getFile('./index.html')
40
+ let css = getFile('./index.css')
41
+
42
+ const shadow = getBase64Image('./images/shadow.png')
43
+ const logo = getBase64Image('./images/logo.png')
44
+ const separator = getBase64Image('./images/separator.png')
45
+
46
+ const team1Img = await downloadImage(team1.img)
47
+ const team2Img = await downloadImage(team2.img)
48
+
49
+ return html
50
+ .replace('.style{}', css)
51
+ .replace('{sport}', sport)
52
+ .replace('{league}', league)
53
+ .replace('{image1}', team1Img)
54
+ .replace('{image2}', team2Img)
55
+ .replace('{date}', date)
56
+ .replace('{game}', `${team1.name} - ${team2.name}`)
57
+ .replace('{tableType}', type)
58
+ .replace('{tableHead}', matchType[type])
59
+ .replace('{betAmount}', betAmount)
60
+ .replace('{outcome}', outcome)
61
+ .replace('{betOdds}', betOdds)
62
+ .replace('{currentOdds}', currentOdds)
63
+ .replace('{separator}', separator)
64
+ .replace('{shadow}', shadow)
65
+ .replace('{logo}', logo)
66
+ },
67
+ }
68
+
69
+ export default template
@@ -0,0 +1,176 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
7
+ <style>
8
+ @font-face {
9
+ font-weight: 700;
10
+ font-family: 'Fivo Sans Modern';
11
+ font-style: normal;
12
+ src: url('/fonts/fivo-sans-modern/bold.woff2') format('woff2'),
13
+ url('/fonts/fivo-sans-modern/bold.woff') format('woff');
14
+ font-display: swap;
15
+ unicode-range: U+000-5FF; /* Latin glyphs */
16
+ }
17
+
18
+ html {
19
+ font-family: 'Inter', sans-serif;
20
+ font-weight: 500;
21
+ }
22
+
23
+ html, body {
24
+ width: 600px;
25
+ height: 315px;
26
+ margin: 0;
27
+ padding: 0;
28
+ }
29
+
30
+ * {
31
+ box-sizing: border-box;
32
+ }
33
+
34
+ .opengraph {
35
+ width: 600px;
36
+ height: 315px;
37
+ padding: 30px 16px 0 23px;
38
+ background-size: cover;
39
+ overflow: hidden;
40
+ }
41
+
42
+ .logo {
43
+ display: block;
44
+ height: 24px;
45
+ margin-bottom: 36px;
46
+ }
47
+
48
+ .content {
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: space-between;
52
+ }
53
+
54
+ .title {
55
+ padding-right: 40px;
56
+ color: #fff;
57
+ font-size: 30px;
58
+ line-height: 32px;
59
+ font-weight: 700;
60
+ font-family: 'Fivo Sans Modern', sans-serif;
61
+ }
62
+
63
+ .card {
64
+ flex: none;
65
+ width: 280px;
66
+ padding: 12px 4px 8px;
67
+ font-weight: 500;
68
+ background: #1F1F24;
69
+ border: 1px solid #333338;
70
+ box-shadow: 0 4px 22px rgba(0, 0, 0, 0.2);
71
+ border-radius: 9px;
72
+ }
73
+
74
+ .cardTitle {
75
+ padding: 0 10px;
76
+ margin-bottom: 11px;
77
+ color: #8A8A98;
78
+ text-align: center;
79
+ font-size: 13px;
80
+ line-height: 16px;
81
+ overflow: hidden;
82
+ text-overflow: ellipsis;
83
+ white-space: nowrap;
84
+ }
85
+
86
+ .teams {
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ }
91
+
92
+ .teamImage {
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ width: 68px;
97
+ height: 68px;
98
+ background: #27272B;
99
+ border-radius: 50%;
100
+ }
101
+
102
+ .teamImage img {
103
+ display: block;
104
+ width: 40px;
105
+ }
106
+
107
+ .dates {
108
+ padding: 0 24px;
109
+ text-align: center;
110
+ }
111
+
112
+ .date {
113
+ color: #8A8A98;
114
+ font-size: 13px;
115
+ line-height: 16px;
116
+ }
117
+
118
+ .time {
119
+ margin-top: 2px;
120
+ color: #fff;
121
+ font-size: 13px;
122
+ line-height: 16px;
123
+ }
124
+
125
+ .teamNames {
126
+ display: grid;
127
+ grid-template-columns: 1fr 1fr;
128
+ grid-gap: 44px;
129
+ }
130
+
131
+ .teamName {
132
+ display: flex;
133
+ justify-content: center;
134
+ align-items: center;
135
+ min-height: 32px;
136
+ margin-top: 4px;
137
+ padding: 0 6px;
138
+ color: #fff;
139
+ text-align: center;
140
+ font-size: 13px;
141
+ line-height: 16px;
142
+ }
143
+ </style>
144
+ </head>
145
+ <body>
146
+ <div class="opengraph" style="background-image: url({bgImage});">
147
+ <img class="logo" src="{logoImage}" alt="" />
148
+ <div class="content">
149
+ <div class="title">{title}</div>
150
+ <div class="card">
151
+ <div class="cardTitle">{country}&nbsp;&middot;&nbsp;{league}</div>
152
+ <div class="teams">
153
+ <div class="teamImage">
154
+ <img src="{team1Image}" alt="" />
155
+ </div>
156
+ <div class="dates">
157
+ <div class="date">{date}</div>
158
+ <div class="time">{time}</div>
159
+ </div>
160
+ <div class="teamImage">
161
+ <img src="{team2Image}" alt="" />
162
+ </div>
163
+ </div>
164
+ <div class="teamNames">
165
+ <div class="teamName">
166
+ <span>{team1Name}</span>
167
+ </div>
168
+ <div class="teamName">
169
+ <span>{team2Name}</span>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </body>
176
+ </html>
@@ -0,0 +1,57 @@
1
+ import dayjs from 'dayjs'
2
+
3
+ import { type Template, getFile, getBase64Image } from '../../utils'
4
+
5
+
6
+ export type Props = {
7
+ title: string
8
+ game: {
9
+ country: string
10
+ league: string
11
+ participants: {
12
+ name: string
13
+ image: string
14
+ }[]
15
+ startsAt: number
16
+ }
17
+ }
18
+
19
+ const template: Template = {
20
+ width: 600,
21
+ height: 315,
22
+ type: 'jpeg',
23
+ scaleFactor: 2,
24
+ html: async (props: Props) => {
25
+ const { title, game } = props
26
+ const { country, league, participants, startsAt } = game
27
+
28
+ const bgImage = getBase64Image('./images/bg.jpg')
29
+ const logoImage = getBase64Image('./images/logo.png')
30
+
31
+ const team1Image = participants[0].image
32
+ const team1Name = participants[0].name
33
+ const team2Image = participants[1].image
34
+ const team2Name = participants[1].name
35
+
36
+ const dateTime = dayjs(startsAt)
37
+ const date = dateTime.format('DD MMM')
38
+ const time = dateTime.format('HH:mm')
39
+
40
+ let html = getFile('./index.html')
41
+
42
+ return html
43
+ .replace('{bgImage}', bgImage)
44
+ .replace('{logoImage}', logoImage)
45
+ .replace('{title}', title)
46
+ .replace('{country}', country)
47
+ .replace('{league}', league)
48
+ .replace('{team1Image}', team1Image)
49
+ .replace('{team1Name}', team1Name)
50
+ .replace('{team2Image}', team2Image)
51
+ .replace('{team2Name}', team2Name)
52
+ .replace('{date}', date)
53
+ .replace('{time}', time)
54
+ }
55
+ }
56
+
57
+ export default template
@@ -0,0 +1,89 @@
1
+ import puppeteer from 'puppeteer'
2
+
3
+ import { type Template } from './types'
4
+
5
+
6
+ type PuppeteerOptions = Parameters<typeof puppeteer.launch>[0]
7
+
8
+ type PuppeteerInitialOptions = {
9
+ headless: boolean
10
+ devtools: boolean
11
+ args: string[]
12
+ }
13
+
14
+ type GenerateImageResult<T> = T extends { output: string } ? void : (string | Buffer)
15
+
16
+ type GenerateImageProps = {
17
+ template: Template
18
+ output?: string // output filepath
19
+ filename?: string
20
+ props: any
21
+ modifyPuppeteerOptions?(options: PuppeteerInitialOptions): PuppeteerOptions
22
+ }
23
+
24
+ export default async function generateImage<T extends GenerateImageProps>(props: T): Promise<GenerateImageResult<T> | undefined> {
25
+ const {
26
+ output,
27
+ filename = 'image',
28
+ template,
29
+ props: htmlProps,
30
+ modifyPuppeteerOptions,
31
+ } = props
32
+
33
+ const {
34
+ headless = true,
35
+ width,
36
+ height,
37
+ type,
38
+ scaleFactor = 1,
39
+ html: getHtml,
40
+ } = template
41
+
42
+ const html = await getHtml(htmlProps)
43
+
44
+ let launchOptions: PuppeteerOptions = {
45
+ headless,
46
+ devtools: false,
47
+ args: [
48
+ '--no-sandbox',
49
+ '--disable-gpu',
50
+ '--disable-accelerated-video-decode',
51
+ // '--allow-file-access-from-files',
52
+ ],
53
+ }
54
+
55
+ if (typeof modifyPuppeteerOptions === 'function') {
56
+ launchOptions = modifyPuppeteerOptions(launchOptions as PuppeteerInitialOptions)
57
+ }
58
+
59
+ const browser = await puppeteer.launch(launchOptions)
60
+
61
+ const page = await browser.newPage()
62
+
63
+ await page.setViewport({ width, height, deviceScaleFactor: scaleFactor })
64
+ await page.setContent(html)
65
+
66
+ const content = await page.$('body')
67
+
68
+ // dont' change this condition!
69
+ if (headless === false) {
70
+ await new Promise(() => {})
71
+ }
72
+
73
+ if (output) {
74
+ const filePath = `${output.replace(/\/$/, '')}/${filename.replace(/\..+$/, '')}.${type}`
75
+
76
+ await content!.screenshot({ path: filePath })
77
+ await page.close()
78
+ await browser.close()
79
+ }
80
+ else {
81
+ const imageBuffer = await content!.screenshot({ omitBackground: true, type })
82
+
83
+ await page.close()
84
+ await browser.close()
85
+
86
+ // @ts-ignore
87
+ return imageBuffer
88
+ }
89
+ }
@@ -0,0 +1,37 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import axios from 'axios'
4
+
5
+
6
+ export const getPath = (filePath: string) => {
7
+ return path.join(__dirname, filePath)
8
+ }
9
+
10
+ export const getFile = (filePath: string) => {
11
+ return fs.readFileSync(getPath(filePath), 'utf8')
12
+ }
13
+
14
+ export const getBase64Image = (filePath: string) => {
15
+ return `data:image/png;base64,${fs.readFileSync(getPath(filePath)).toString('base64')}`
16
+ }
17
+
18
+ export const downloadImage = async (url: string) => {
19
+ // empty pixel
20
+ let base64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='
21
+
22
+ try {
23
+ const response = await axios.get(url, { responseType: 'arraybuffer' })
24
+ const buffer = Buffer.from(response.data, 'utf-8')
25
+
26
+ base64 = buffer.toString('base64')
27
+ }
28
+ catch (err) {
29
+ console.error(err)
30
+ // empty pixel
31
+
32
+ }
33
+
34
+ return `data:image/png;base64,${base64}`
35
+ }
36
+
37
+ export { type Template } from './types'
@@ -0,0 +1,8 @@
1
+ export type Template = {
2
+ headless?: boolean
3
+ width: number
4
+ height: number
5
+ type: 'png' | 'jpeg'
6
+ scaleFactor?: 1 | 2
7
+ html: (props: any) => string | Promise<string>
8
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "rootDir": "src",
4
+ "target": "es5",
5
+ "module": "esnext",
6
+ "moduleResolution": "node",
7
+ "lib": [ "es2017", "es7", "es6", "dom" ],
8
+ "strict": true,
9
+ "declaration": true,
10
+ "esModuleInterop": true
11
+ },
12
+ "include": [
13
+ "src"
14
+ ],
15
+ "exclude": [
16
+ "node_modules",
17
+ "dist",
18
+ "lib"
19
+ ]
20
+ }