From 11bac255e7352feb1f4210f8de529f6cc58112f6 Mon Sep 17 00:00:00 2001
From: arelive
Date: Tue, 6 Jun 2023 18:51:15 +0300
Subject: [PATCH 01/60] drawing quote with Playwright
---
package-lock.json | 75 +++++++++++++++++++++++++++++++++++++++--
package.json | 2 ++
utils/quote-generate.js | 64 ++++++++++++++++++++++++++---------
utils/render.js | 11 ++++++
utils/views.js | 11 ++++++
views/quote.hbs | 52 ++++++++++++++++++++++++++++
6 files changed, 197 insertions(+), 18 deletions(-)
create mode 100644 utils/render.js
create mode 100644 utils/views.js
create mode 100644 views/quote.hbs
diff --git a/package-lock.json b/package-lock.json
index fb0b5cf..8703e1b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,18 @@
{
"name": "quote-api",
- "version": "0.13.2",
+ "version": "0.13.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "quote-api",
- "version": "0.13.2",
+ "version": "0.13.17",
"license": "MIT",
"dependencies": {
"canvas": "2.11.0",
"dotenv": "^7.0.0",
"emoji-db": "^14.0.1",
+ "handlebars": "^4.7.7",
"jimp": "^0.16.1",
"jsdom": "^16.5.3",
"koa": "^2.11.0",
@@ -24,6 +25,7 @@
"lottie-web": "^5.7.8",
"lru-cache": "^5.1.1",
"object-sizeof": "^1.6.0",
+ "playwright": "^1.34.3",
"runes": "^0.4.3",
"sharp": "^0.31.3",
"smartcrop-sharp": "^2.0.7",
@@ -2129,6 +2131,26 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/handlebars": {
+ "version": "4.7.7",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
+ "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==",
+ "dependencies": {
+ "minimist": "^1.2.5",
+ "neo-async": "^2.6.0",
+ "source-map": "^0.6.1",
+ "wordwrap": "^1.0.0"
+ },
+ "bin": {
+ "handlebars": "bin/handlebars"
+ },
+ "engines": {
+ "node": ">=0.4.7"
+ },
+ "optionalDependencies": {
+ "uglify-js": "^3.1.4"
+ }
+ },
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@@ -3121,6 +3143,11 @@
"node": ">= 0.6"
}
},
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
+ },
"node_modules/node-abi": {
"version": "3.34.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.34.0.tgz",
@@ -3466,6 +3493,32 @@
"pixelmatch": "bin/pixelmatch"
}
},
+ "node_modules/playwright": {
+ "version": "1.34.3",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.34.3.tgz",
+ "integrity": "sha512-UOOVE4ZbGfGkP1KVqWTdXOmm8Pw2pBhfbmlqKMkpiRCQjL5W+J+xRQXpgutFr0iM4pWl8g0GyyASMsqjQfFohw==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "playwright-core": "1.34.3"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.34.3",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.3.tgz",
+ "integrity": "sha512-2pWd6G7OHKemc5x1r1rp8aQcpvDh7goMBZlJv6Co5vCNLVcQJdhxRL09SGaY6HcyHH9aT4tiynZabMofVasBYw==",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/pngjs": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
@@ -3996,7 +4049,6 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "optional": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4320,6 +4372,18 @@
"resolved": "https://registry.npmjs.org/typegram/-/typegram-3.7.0.tgz",
"integrity": "sha512-IafMO+GRi5H8CtWSNihuD56Bjpmj/ISbg6G8jdTkNxldrym+FOPlo/fxtaPs/LyWnS0l1Bm18MUDwOikZSKmJw=="
},
+ "node_modules/uglify-js": {
+ "version": "3.17.4",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
+ "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
+ "optional": true,
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/unbox-primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
@@ -4492,6 +4556,11 @@
"node": ">=0.10.0"
}
},
+ "node_modules/wordwrap": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
diff --git a/package.json b/package.json
index ae3afb5..ab43bf7 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"canvas": "2.11.0",
"dotenv": "^7.0.0",
"emoji-db": "^14.0.1",
+ "handlebars": "^4.7.7",
"jimp": "^0.16.1",
"jsdom": "^16.5.3",
"koa": "^2.11.0",
@@ -34,6 +35,7 @@
"lottie-web": "^5.7.8",
"lru-cache": "^5.1.1",
"object-sizeof": "^1.6.0",
+ "playwright": "^1.34.3",
"runes": "^0.4.3",
"sharp": "^0.31.3",
"smartcrop-sharp": "^2.0.7",
diff --git a/utils/quote-generate.js b/utils/quote-generate.js
index 52dbfa3..4562006 100644
--- a/utils/quote-generate.js
+++ b/utils/quote-generate.js
@@ -1,5 +1,5 @@
const fs = require('fs')
-const { createCanvas, registerFont } = require('canvas')
+const { createCanvas, registerFont, Image } = require('canvas')
const EmojiDbLib = require('emoji-db')
const { loadImage } = require('canvas')
const loadImageFromUrl = require('./image-load-url')
@@ -11,6 +11,9 @@ const lottie = require('lottie-node')
const zlib = require('zlib')
const { Telegram } = require('telegraf')
+const render = require('./render')
+const { quote: view } = require('./views')
+
const emojiDb = new EmojiDbLib({ useDefaultDb: true })
function loadFont () {
@@ -35,6 +38,7 @@ loadFont()
const emojiImageByBrand = require('./emoji-image')
const LRU = require('lru-cache')
+const page = require('./render')
const avatarCache = new LRU({
max: 20,
@@ -692,11 +696,11 @@ class QuoteGenerate {
const avatarX = 0
const avatarY = 0
- canvasCtx.beginPath()
- canvasCtx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2, true)
- canvasCtx.clip()
- canvasCtx.closePath()
- canvasCtx.restore()
+ // canvasCtx.beginPath()
+ // canvasCtx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2, true)
+ // canvasCtx.clip()
+ // canvasCtx.closePath()
+ // canvasCtx.restore()
canvasCtx.drawImage(avatarImage, avatarX, avatarY, avatarSize, avatarSize)
return canvas
@@ -940,12 +944,8 @@ class QuoteGenerate {
'#FF7FD5' // pink
]
- console.log(Math.abs(message.from.id) % 7)
-
// user name color
- let nameIndex = 1
- if (message.from.id) nameIndex = Math.abs(message.from.id) % 7
-
+ const nameIndex = message.from.id ? Math.abs(message.from.id) % 7 : 1
const nameColorArray = backStyle === 'light' ? nameColorLight : nameColorDark
let nameColor = nameColorArray[nameIndex]
@@ -958,6 +958,42 @@ class QuoteGenerate {
nameColor = colorContrast.adjustContrast(this.colorLuminance(backgroundColorTwo, 0.55), nameColor)
}
+
+ const avatarImage = await this.drawAvatar(message.from)
+
+ const content = view({
+ width,
+ height,
+ scale,
+ theme: backStyle,
+ nameColor,
+ name: message?.from?.name ?? '',
+ emojiStatus: message?.from?.emoji_status ?? '',
+ avatarURL: avatarImage.toDataURL(),
+ text: message.text ?? '',
+ })
+
+ const page = await render(content)
+ const image = await page.locator('#quote').screenshot()
+
+ // convert to canvas for the generator
+ const canvas = createCanvas(width, height)
+ const canvasCtx = canvas.getContext('2d')
+ const img = new Image()
+
+ return new Promise((resolve, reject) => {
+ img.onload = () => {
+ canvasCtx.drawImage(img, 0, 0, width, height)
+ resolve(canvas)
+ }
+ img.onerror = console.error
+ img.src = image
+ })
+
+
+ // deprecated
+ // TODO: completely replace, test, remove
+
const nameSize = 22 * scale
let nameCanvas
@@ -996,10 +1032,8 @@ class QuoteGenerate {
)
}
- let fontSize = 24 * scale
-
- let textColor = '#fff'
- if (backStyle === 'light') textColor = '#000'
+ const fontSize = 24 * scale
+ const textColor = backStyle == 'light' ? '#000' : '#fff'
let textCanvas
if (message.text) {
diff --git a/utils/render.js b/utils/render.js
new file mode 100644
index 0000000..3fbe2e9
--- /dev/null
+++ b/utils/render.js
@@ -0,0 +1,11 @@
+const { webkit } = require('playwright')
+
+const promise = webkit.launch()
+ .then(browser => browser.newContext())
+ .then(context => context.newPage())
+
+module.exports = async (content) => {
+ const page = await promise
+ await page.setContent(content)
+ return page
+}
diff --git a/utils/views.js b/utils/views.js
new file mode 100644
index 0000000..3c515d3
--- /dev/null
+++ b/utils/views.js
@@ -0,0 +1,11 @@
+const path = require('path')
+const fs = require('fs')
+const Handlebars = require('handlebars')
+
+const quote = Handlebars.compile(
+ fs.readFileSync(path.resolve('./views/quote.hbs')).toString('utf-8')
+)
+
+module.exports = {
+ quote
+}
diff --git a/views/quote.hbs b/views/quote.hbs
new file mode 100644
index 0000000..eca43d3
--- /dev/null
+++ b/views/quote.hbs
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+ {{name}}
+ {{emojiStatus}}
+
+ {{text}}
+
+
+
From 95575105d89d3a53e0919331e011cf7f6d3022e5 Mon Sep 17 00:00:00 2001
From: arelive
Date: Tue, 6 Jun 2023 18:51:15 +0300
Subject: [PATCH 02/60] drawing quote with Playwright
---
package-lock.json | 75 +++++++++++++++++++++++++++++++++++++++--
package.json | 2 ++
utils/quote-generate.js | 63 +++++++++++++++++++++++++---------
utils/render.js | 11 ++++++
utils/views.js | 11 ++++++
views/quote.hbs | 52 ++++++++++++++++++++++++++++
6 files changed, 196 insertions(+), 18 deletions(-)
create mode 100644 utils/render.js
create mode 100644 utils/views.js
create mode 100644 views/quote.hbs
diff --git a/package-lock.json b/package-lock.json
index fb0b5cf..8703e1b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,18 @@
{
"name": "quote-api",
- "version": "0.13.2",
+ "version": "0.13.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "quote-api",
- "version": "0.13.2",
+ "version": "0.13.17",
"license": "MIT",
"dependencies": {
"canvas": "2.11.0",
"dotenv": "^7.0.0",
"emoji-db": "^14.0.1",
+ "handlebars": "^4.7.7",
"jimp": "^0.16.1",
"jsdom": "^16.5.3",
"koa": "^2.11.0",
@@ -24,6 +25,7 @@
"lottie-web": "^5.7.8",
"lru-cache": "^5.1.1",
"object-sizeof": "^1.6.0",
+ "playwright": "^1.34.3",
"runes": "^0.4.3",
"sharp": "^0.31.3",
"smartcrop-sharp": "^2.0.7",
@@ -2129,6 +2131,26 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/handlebars": {
+ "version": "4.7.7",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
+ "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==",
+ "dependencies": {
+ "minimist": "^1.2.5",
+ "neo-async": "^2.6.0",
+ "source-map": "^0.6.1",
+ "wordwrap": "^1.0.0"
+ },
+ "bin": {
+ "handlebars": "bin/handlebars"
+ },
+ "engines": {
+ "node": ">=0.4.7"
+ },
+ "optionalDependencies": {
+ "uglify-js": "^3.1.4"
+ }
+ },
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@@ -3121,6 +3143,11 @@
"node": ">= 0.6"
}
},
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
+ },
"node_modules/node-abi": {
"version": "3.34.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.34.0.tgz",
@@ -3466,6 +3493,32 @@
"pixelmatch": "bin/pixelmatch"
}
},
+ "node_modules/playwright": {
+ "version": "1.34.3",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.34.3.tgz",
+ "integrity": "sha512-UOOVE4ZbGfGkP1KVqWTdXOmm8Pw2pBhfbmlqKMkpiRCQjL5W+J+xRQXpgutFr0iM4pWl8g0GyyASMsqjQfFohw==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "playwright-core": "1.34.3"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.34.3",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.3.tgz",
+ "integrity": "sha512-2pWd6G7OHKemc5x1r1rp8aQcpvDh7goMBZlJv6Co5vCNLVcQJdhxRL09SGaY6HcyHH9aT4tiynZabMofVasBYw==",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/pngjs": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
@@ -3996,7 +4049,6 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "optional": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4320,6 +4372,18 @@
"resolved": "https://registry.npmjs.org/typegram/-/typegram-3.7.0.tgz",
"integrity": "sha512-IafMO+GRi5H8CtWSNihuD56Bjpmj/ISbg6G8jdTkNxldrym+FOPlo/fxtaPs/LyWnS0l1Bm18MUDwOikZSKmJw=="
},
+ "node_modules/uglify-js": {
+ "version": "3.17.4",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
+ "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
+ "optional": true,
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/unbox-primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
@@ -4492,6 +4556,11 @@
"node": ">=0.10.0"
}
},
+ "node_modules/wordwrap": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
diff --git a/package.json b/package.json
index ae3afb5..ab43bf7 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"canvas": "2.11.0",
"dotenv": "^7.0.0",
"emoji-db": "^14.0.1",
+ "handlebars": "^4.7.7",
"jimp": "^0.16.1",
"jsdom": "^16.5.3",
"koa": "^2.11.0",
@@ -34,6 +35,7 @@
"lottie-web": "^5.7.8",
"lru-cache": "^5.1.1",
"object-sizeof": "^1.6.0",
+ "playwright": "^1.34.3",
"runes": "^0.4.3",
"sharp": "^0.31.3",
"smartcrop-sharp": "^2.0.7",
diff --git a/utils/quote-generate.js b/utils/quote-generate.js
index 52dbfa3..317e41a 100644
--- a/utils/quote-generate.js
+++ b/utils/quote-generate.js
@@ -1,5 +1,5 @@
const fs = require('fs')
-const { createCanvas, registerFont } = require('canvas')
+const { createCanvas, registerFont, Image } = require('canvas')
const EmojiDbLib = require('emoji-db')
const { loadImage } = require('canvas')
const loadImageFromUrl = require('./image-load-url')
@@ -11,6 +11,9 @@ const lottie = require('lottie-node')
const zlib = require('zlib')
const { Telegram } = require('telegraf')
+const render = require('./render')
+const { quote: view } = require('./views')
+
const emojiDb = new EmojiDbLib({ useDefaultDb: true })
function loadFont () {
@@ -692,11 +695,11 @@ class QuoteGenerate {
const avatarX = 0
const avatarY = 0
- canvasCtx.beginPath()
- canvasCtx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2, true)
- canvasCtx.clip()
- canvasCtx.closePath()
- canvasCtx.restore()
+ // canvasCtx.beginPath()
+ // canvasCtx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2, true)
+ // canvasCtx.clip()
+ // canvasCtx.closePath()
+ // canvasCtx.restore()
canvasCtx.drawImage(avatarImage, avatarX, avatarY, avatarSize, avatarSize)
return canvas
@@ -940,12 +943,8 @@ class QuoteGenerate {
'#FF7FD5' // pink
]
- console.log(Math.abs(message.from.id) % 7)
-
// user name color
- let nameIndex = 1
- if (message.from.id) nameIndex = Math.abs(message.from.id) % 7
-
+ const nameIndex = message.from.id ? Math.abs(message.from.id) % 7 : 1
const nameColorArray = backStyle === 'light' ? nameColorLight : nameColorDark
let nameColor = nameColorArray[nameIndex]
@@ -958,6 +957,42 @@ class QuoteGenerate {
nameColor = colorContrast.adjustContrast(this.colorLuminance(backgroundColorTwo, 0.55), nameColor)
}
+
+ const avatarImage = await this.drawAvatar(message.from)
+
+ const content = view({
+ width,
+ height,
+ scale,
+ theme: backStyle,
+ nameColor,
+ name: message?.from?.name ?? '',
+ emojiStatus: message?.from?.emoji_status ?? '',
+ avatarURL: avatarImage.toDataURL(),
+ text: message.text ?? '',
+ })
+
+ const page = await render(content)
+ const image = await page.locator('#quote').screenshot()
+
+ // convert to canvas for the generator
+ const canvas = createCanvas(width, height)
+ const canvasCtx = canvas.getContext('2d')
+ const img = new Image()
+
+ return new Promise((resolve, reject) => {
+ img.onload = () => {
+ canvasCtx.drawImage(img, 0, 0, width, height)
+ resolve(canvas)
+ }
+ img.onerror = console.error
+ img.src = image
+ })
+
+
+ // deprecated
+ // TODO: completely replace, test, remove
+
const nameSize = 22 * scale
let nameCanvas
@@ -996,10 +1031,8 @@ class QuoteGenerate {
)
}
- let fontSize = 24 * scale
-
- let textColor = '#fff'
- if (backStyle === 'light') textColor = '#000'
+ const fontSize = 24 * scale
+ const textColor = backStyle == 'light' ? '#000' : '#fff'
let textCanvas
if (message.text) {
diff --git a/utils/render.js b/utils/render.js
new file mode 100644
index 0000000..3fbe2e9
--- /dev/null
+++ b/utils/render.js
@@ -0,0 +1,11 @@
+const { webkit } = require('playwright')
+
+const promise = webkit.launch()
+ .then(browser => browser.newContext())
+ .then(context => context.newPage())
+
+module.exports = async (content) => {
+ const page = await promise
+ await page.setContent(content)
+ return page
+}
diff --git a/utils/views.js b/utils/views.js
new file mode 100644
index 0000000..3c515d3
--- /dev/null
+++ b/utils/views.js
@@ -0,0 +1,11 @@
+const path = require('path')
+const fs = require('fs')
+const Handlebars = require('handlebars')
+
+const quote = Handlebars.compile(
+ fs.readFileSync(path.resolve('./views/quote.hbs')).toString('utf-8')
+)
+
+module.exports = {
+ quote
+}
diff --git a/views/quote.hbs b/views/quote.hbs
new file mode 100644
index 0000000..1c6a1b9
--- /dev/null
+++ b/views/quote.hbs
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+ {{name}}
+ {{emojiStatus}}
+
+ {{text}}
+
+
+
From 09722a4bb44b3d4293eb378699164e90b8a5d945 Mon Sep 17 00:00:00 2001
From: arelive
Date: Wed, 7 Jun 2023 13:04:42 +0300
Subject: [PATCH 03/60] optimize generation; add stress test
---
methods/generate.js | 9 +++--
package-lock.json | 84 +++++++++++++++++++++++++++++++++++++++--
package.json | 7 +++-
test/.gitignore | 1 +
test/stress.js | 59 +++++++++++++++++++++++++++++
utils/quote-generate.js | 5 +--
6 files changed, 154 insertions(+), 11 deletions(-)
create mode 100644 test/.gitignore
create mode 100644 test/stress.js
diff --git a/methods/generate.js b/methods/generate.js
index bd27063..780e5e8 100644
--- a/methods/generate.js
+++ b/methods/generate.js
@@ -3,6 +3,7 @@ const {
} = require('../utils')
const { createCanvas, loadImage } = require('canvas')
const sharp = require('sharp')
+const { Telegram } = require('telegraf')
const normalizeColor = (color) => {
const canvas = createCanvas(0, 0)
@@ -46,14 +47,16 @@ const imageAlpha = (image, alpha) => {
return canvas
}
+let telegramBot
+
module.exports = async (parm) => {
// console.log(JSON.stringify(parm, null, 2))
if (!parm) return { error: 'query_empty' }
if (!parm.messages || parm.messages.length < 1) return { error: 'messages_empty' }
- let botToken = parm.botToken || process.env.BOT_TOKEN
-
- const quoteGenerate = new QuoteGenerate(botToken)
+ const botToken = parm.botToken || process.env.BOT_TOKEN
+ telegramBot = telegramBot || new Telegram(botToken)
+ const quoteGenerate = new QuoteGenerate(telegramBot)
const quoteImages = []
diff --git a/package-lock.json b/package-lock.json
index fb0b5cf..f95e42d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "quote-api",
- "version": "0.13.2",
+ "version": "0.13.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "quote-api",
- "version": "0.13.2",
+ "version": "0.13.17",
"license": "MIT",
"dependencies": {
"canvas": "2.11.0",
@@ -30,12 +30,14 @@
"telegraf": "^3.38.0"
},
"devDependencies": {
+ "axios": "^1.4.0",
"eslint": "^8.6.0",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-import": "^2.17.3",
"eslint-plugin-node": "^9.1.0",
"eslint-plugin-promise": "^4.1.1",
- "eslint-plugin-standard": "^4.0.0"
+ "eslint-plugin-standard": "^4.0.0",
+ "lorem-ipsum": "^2.0.8"
}
},
"node_modules/@babel/runtime": {
@@ -739,6 +741,31 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
+ "node_modules/axios": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
+ "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
+ "dev": true,
+ "dependencies": {
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axios/node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dev": true,
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -980,6 +1007,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/commander": {
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
+ "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.20.0 || >=14"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1959,6 +1995,26 @@
"integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==",
"dev": true
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.2",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+ "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/form-data": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
@@ -2909,6 +2965,22 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
+ "node_modules/lorem-ipsum": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/lorem-ipsum/-/lorem-ipsum-2.0.8.tgz",
+ "integrity": "sha512-5RIwHuCb979RASgCJH0VKERn9cQo/+NcAi2BMe9ddj+gp7hujl6BI+qdOG4nVsLDpwWEJwTVYXNKP6BGgbcoGA==",
+ "dev": true,
+ "dependencies": {
+ "commander": "^9.3.0"
+ },
+ "bin": {
+ "lorem-ipsum": "dist/bin/lorem-ipsum.bin.js"
+ },
+ "engines": {
+ "node": ">= 8.x",
+ "npm": ">= 5.x"
+ }
+ },
"node_modules/lottie-node": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lottie-node/-/lottie-node-2.0.0.tgz",
@@ -3581,6 +3653,12 @@
"node": ">=0.4.0"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true
+ },
"node_modules/psl": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
diff --git a/package.json b/package.json
index ae3afb5..614f8d9 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,8 @@
"scripts": {
"start": "node index.js",
"lint": "node_modules/.bin/eslint --ext js .",
- "lint:fix": "node_modules/.bin/eslint --fix --ext js ."
+ "lint:fix": "node_modules/.bin/eslint --fix --ext js .",
+ "test": "node test/stress.js 1000"
},
"repository": {
"type": "git",
@@ -40,11 +41,13 @@
"telegraf": "^3.38.0"
},
"devDependencies": {
+ "axios": "^1.4.0",
"eslint": "^8.6.0",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-import": "^2.17.3",
"eslint-plugin-node": "^9.1.0",
"eslint-plugin-promise": "^4.1.1",
- "eslint-plugin-standard": "^4.0.0"
+ "eslint-plugin-standard": "^4.0.0",
+ "lorem-ipsum": "^2.0.8"
}
}
diff --git a/test/.gitignore b/test/.gitignore
new file mode 100644
index 0000000..e33609d
--- /dev/null
+++ b/test/.gitignore
@@ -0,0 +1 @@
+*.png
diff --git a/test/stress.js b/test/stress.js
new file mode 100644
index 0000000..489ff43
--- /dev/null
+++ b/test/stress.js
@@ -0,0 +1,59 @@
+const axios = require('axios')
+const fs = require('fs')
+const LoremIpsum = require('lorem-ipsum').LoremIpsum
+const path = require('path')
+
+require('dotenv').config({ path: './.env' })
+require('../app')
+
+const lorem = new LoremIpsum({
+ sentencesPerParagraph: {
+ max: 8,
+ min: 1
+ },
+ wordsPerSentence: {
+ max: 16,
+ min: 4
+ }
+})
+
+const nQuotes = parseInt(process.argv[2])
+
+;(async () => {
+ for (let i = 0; i < nQuotes; i++) {
+ const text = lorem.generateParagraphs(1)
+ const username = lorem.generateWords(2)
+ const avatar = 'https://telegra.ph/file/59952c903fdfb10b752b3.jpg'
+
+ const json = {
+ type: 'quote',
+ format: 'png',
+ backgroundColor: '#FFFFFF',
+ width: 512,
+ height: 768,
+ scale: 2,
+ messages: [
+ {
+ entities: [],
+ avatar: true,
+ from: {
+ id: 1,
+ name: username,
+ photo: {
+ url: avatar
+ }
+ },
+ text: text,
+ replyMessage: {}
+ }
+ ]
+ }
+
+ await axios.post('http://localhost:3000/generate', json, {
+ headers: { 'Content-Type': 'application/json' }
+ }).then(res => {
+ const buffer = Buffer.from(res.data.result.image, 'base64')
+ fs.writeFile(path.resolve(`./test/${i}.png`), buffer, (err) => err && console.error(err))
+ }).catch(console.error)
+ }
+})()
diff --git a/utils/quote-generate.js b/utils/quote-generate.js
index 52dbfa3..2dd7dd7 100644
--- a/utils/quote-generate.js
+++ b/utils/quote-generate.js
@@ -9,7 +9,6 @@ const smartcrop = require('smartcrop-sharp')
const runes = require('runes')
const lottie = require('lottie-node')
const zlib = require('zlib')
-const { Telegram } = require('telegraf')
const emojiDb = new EmojiDbLib({ useDefaultDb: true })
@@ -109,8 +108,8 @@ class ColorContrast {
class QuoteGenerate {
- constructor (botToken) {
- this.telegram = new Telegram(botToken)
+ constructor (telegramBot) {
+ this.telegram = telegramBot
}
async avatarImageLatters (letters, color) {
From 982320268b89331cb50f74d94c7f517d89ce9ae2 Mon Sep 17 00:00:00 2001
From: arelive
Date: Wed, 7 Jun 2023 15:20:01 +0300
Subject: [PATCH 04/60] add async test using direct method
---
package-lock.json | 7 ++++
package.json | 4 ++-
test/async.js | 71 +++++++++++++++++++++++++++++++++++++++++
test/stress.js | 6 +++-
utils/quote-generate.js | 2 --
5 files changed, 86 insertions(+), 4 deletions(-)
create mode 100644 test/async.js
diff --git a/package-lock.json b/package-lock.json
index f95e42d..d137681 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,6 +30,7 @@
"telegraf": "^3.38.0"
},
"devDependencies": {
+ "async": "^3.2.4",
"axios": "^1.4.0",
"eslint": "^8.6.0",
"eslint-config-standard": "^12.0.0",
@@ -736,6 +737,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/async": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
+ "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
+ "dev": true
+ },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
diff --git a/package.json b/package.json
index 614f8d9..2067a90 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,8 @@
"start": "node index.js",
"lint": "node_modules/.bin/eslint --ext js .",
"lint:fix": "node_modules/.bin/eslint --fix --ext js .",
- "test": "node test/stress.js 1000"
+ "test": "node test/stress.js 1000",
+ "test:async": "node test/async.js 1000 50"
},
"repository": {
"type": "git",
@@ -41,6 +42,7 @@
"telegraf": "^3.38.0"
},
"devDependencies": {
+ "async": "^3.2.4",
"axios": "^1.4.0",
"eslint": "^8.6.0",
"eslint-config-standard": "^12.0.0",
diff --git a/test/async.js b/test/async.js
new file mode 100644
index 0000000..1ca0a34
--- /dev/null
+++ b/test/async.js
@@ -0,0 +1,71 @@
+const async = require('async')
+const path = require('path')
+const fs = require('fs')
+const LoremIpsum = require('lorem-ipsum').LoremIpsum
+
+require('dotenv').config({ path: './.env' })
+require('../app')
+
+const generate = require('../methods/generate')
+
+const lorem = new LoremIpsum({
+ sentencesPerParagraph: {
+ max: 8,
+ min: 1
+ },
+ wordsPerSentence: {
+ max: 16,
+ min: 4
+ }
+})
+
+const nQuotes = parseInt(process.argv[2])
+const nCallsLimit = parseInt(process.argv[3])
+
+const queue = async.queue(
+ (json, cb) => generate(json)
+ .then(cb)
+ .catch(console.error),
+ nCallsLimit
+)
+
+for (let i = 0; i < nQuotes; i++) {
+ const text = lorem.generateParagraphs(1)
+ const username = lorem.generateWords(2)
+ const avatar = 'https://telegra.ph/file/59952c903fdfb10b752b3.jpg'
+
+ const json = {
+ botToken: process.env.BOT_TOKEN,
+ type: 'quote',
+ format: 'png',
+ backgroundColor: '#FFFFFF',
+ width: 512,
+ height: 768,
+ scale: 2,
+ messages: [
+ {
+ entities: [],
+ avatar: true,
+ from: {
+ id: 1,
+ name: username,
+ photo: {
+ url: avatar
+ }
+ },
+ text: text,
+ replyMessage: {}
+ }
+ ]
+ }
+
+ console.time(i)
+ queue.push(json, res => {
+ const buffer = Buffer.from(res.image, 'base64')
+ fs.writeFile(
+ path.resolve(`./test/${i}.png`), buffer,
+ err => err && console.error(err)
+ )
+ console.timeLog(i)
+ })
+}
diff --git a/test/stress.js b/test/stress.js
index 489ff43..fc10cdd 100644
--- a/test/stress.js
+++ b/test/stress.js
@@ -26,6 +26,7 @@ const nQuotes = parseInt(process.argv[2])
const avatar = 'https://telegra.ph/file/59952c903fdfb10b752b3.jpg'
const json = {
+ botToken: process.env.BOT_TOKEN,
type: 'quote',
format: 'png',
backgroundColor: '#FFFFFF',
@@ -53,7 +54,10 @@ const nQuotes = parseInt(process.argv[2])
headers: { 'Content-Type': 'application/json' }
}).then(res => {
const buffer = Buffer.from(res.data.result.image, 'base64')
- fs.writeFile(path.resolve(`./test/${i}.png`), buffer, (err) => err && console.error(err))
+ fs.writeFile(
+ path.resolve(`./test/${i}.png`), buffer,
+ err => err && console.error(err)
+ )
}).catch(console.error)
}
})()
diff --git a/utils/quote-generate.js b/utils/quote-generate.js
index 2dd7dd7..e67fc63 100644
--- a/utils/quote-generate.js
+++ b/utils/quote-generate.js
@@ -939,8 +939,6 @@ class QuoteGenerate {
'#FF7FD5' // pink
]
- console.log(Math.abs(message.from.id) % 7)
-
// user name color
let nameIndex = 1
if (message.from.id) nameIndex = Math.abs(message.from.id) % 7
From b615b2adc585512c4b5876db7302fbc84a9524f3 Mon Sep 17 00:00:00 2001
From: arelive
Date: Wed, 7 Jun 2023 21:00:55 +0300
Subject: [PATCH 05/60] draw full quote with browser
---
methods/generate.js | 116 ++++++++++++++++++++-------------
utils/draw-avatar.js | 126 ++++++++++++++++++++++++++++++++++++
utils/get-view.js | 11 ++++
utils/quote-generate.js | 138 +++-------------------------------------
utils/render.js | 9 ++-
utils/telegram.js | 5 ++
utils/views.js | 11 ----
views/message.hbs | 52 +++++++++++++++
views/quote.hbs | 123 ++++++++++++++++++++++++++++-------
9 files changed, 384 insertions(+), 207 deletions(-)
create mode 100644 utils/draw-avatar.js
create mode 100644 utils/get-view.js
create mode 100644 utils/telegram.js
delete mode 100644 utils/views.js
create mode 100644 views/message.hbs
diff --git a/methods/generate.js b/methods/generate.js
index bd27063..f2f0638 100644
--- a/methods/generate.js
+++ b/methods/generate.js
@@ -1,9 +1,12 @@
-const {
- QuoteGenerate
-} = require('../utils')
+const path = require('path')
+const fs = require('fs')
const { createCanvas, loadImage } = require('canvas')
const sharp = require('sharp')
+const render = require('../utils/render')
+const getView = require('../utils/get-view')
+const drawAvatar = require('../utils/draw-avatar')
+
const normalizeColor = (color) => {
const canvas = createCanvas(0, 0)
const canvasCtx = canvas.getContext('2d')
@@ -46,20 +49,18 @@ const imageAlpha = (image, alpha) => {
return canvas
}
+const buildContent = getView('quote')
+// FIXME: doest work
+const bgImageURL = fs.readFileSync(
+ path.resolve('./assets/pattern_02.png')
+).toString('base64')
+
module.exports = async (parm) => {
- // console.log(JSON.stringify(parm, null, 2))
if (!parm) return { error: 'query_empty' }
- if (!parm.messages || parm.messages.length < 1) return { error: 'messages_empty' }
-
- let botToken = parm.botToken || process.env.BOT_TOKEN
-
- const quoteGenerate = new QuoteGenerate(botToken)
-
- const quoteImages = []
+ if (!parm.messages || !parm.messages.length) return { error: 'messages_empty' }
let backgroundColor = parm.backgroundColor || '//#292232'
- let backgroundColorOne
- let backgroundColorTwo
+ let backgroundColorOne, backgroundColorTwo
const backgroundColorSplit = backgroundColor.split('/')
@@ -76,9 +77,65 @@ module.exports = async (parm) => {
backgroundColorTwo = backgroundColor
}
- for (const key in parm.messages) {
- const message = parm.messages[key]
+ const scale = parm.scale ? Math.min(parseFloat(parm.scale), 20) : 2
+ const nameColors = ['red', 'orange', 'purple', 'green', 'sea', 'blue', 'pink']
+
+ const messages = await Promise.all(parm.messages
+ .filter(message => message)
+ .map(async message => {
+ const avatarURL = await drawAvatar(message.from)
+ return {
+ from: {
+ name: message.from?.name ?? '',
+ color: nameColors[message.from?.id ? Math.abs(message.from.id) % 7 : 1],
+ emoji_status: message.from?.emojiStatus ?? '',
+ avatar: { url: avatarURL.toDataURL() }
+ },
+ text: message.text ?? ''
+ }
+ })
+ )
+
+ if (!messages.length) {
+ return { error: 'empty_messages' }
+ }
+
+ const content = buildContent({
+ scale,
+ width: parm.width * scale,
+ height: parm.height * scale,
+ theme: 'light',
+ background: {
+ image: { url: bgImageURL, },
+ color1: colorLuminance(backgroundColorTwo, 0.15),
+ color2: colorLuminance(backgroundColorOne, 0.15)
+ },
+ messages,
+ })
+ fs.writeFileSync('./content.html', content)
+
+ let { type, format, ext } = parm
+ const quoteImage = await render(content, '#quote')
+ const { width, height } = await sharp(quoteImage).metadata()
+ const image = ext ? quoteImage : quoteImage.toString('base64')
+
+ if ((!type && ext) || (type != 'image' && height > 1024 * 2)) {
+ type = 'png'
+ }
+
+
+ return {
+ image,
+ type,
+ width,
+ height,
+ ext
+ }
+
+ // deprecated // TODO
+/*
+ for (const message of parm.messages) {
if (message) {
const canvasQuote = await quoteGenerate.generate(
backgroundColorOne,
@@ -94,12 +151,6 @@ module.exports = async (parm) => {
}
}
- if (quoteImages.length === 0) {
- return {
- error: 'empty_messages'
- }
- }
-
let canvasQuote
if (quoteImages.length > 1) {
@@ -129,11 +180,6 @@ module.exports = async (parm) => {
let quoteImage
- let { type, format, ext } = parm
-
- if (!type && ext) type = 'png'
- if (type !== 'image' && canvasQuote.height > 1024 * 2) type = 'png'
-
if (type === 'quote') {
const downPadding = 75
const maxWidth = 512
@@ -218,21 +264,5 @@ module.exports = async (parm) => {
} else {
quoteImage = canvasQuote.toBuffer()
}
-
- const imageMetadata = await sharp(quoteImage).metadata()
-
- const width = imageMetadata.width
- const height = imageMetadata.height
-
- let image
- if (ext) image = quoteImage
- else image = quoteImage.toString('base64')
-
- return {
- image,
- type,
- width,
- height,
- ext
- }
+*/
}
diff --git a/utils/draw-avatar.js b/utils/draw-avatar.js
new file mode 100644
index 0000000..3d96283
--- /dev/null
+++ b/utils/draw-avatar.js
@@ -0,0 +1,126 @@
+const { createCanvas, loadImage } = require('canvas')
+const runes = require('runes')
+const LRU = require('lru-cache')
+
+const telegram = require('./telegram')
+
+const avatarCache = new LRU({
+ max: 20,
+ maxAge: 1000 * 60 * 5
+})
+
+async function drawAvatar (user) {
+ const avatarImage = await downloadAvatarImage(user)
+
+ if (avatarImage) {
+ const avatarSize = avatarImage.naturalHeight
+
+ const canvas = createCanvas(avatarSize, avatarSize)
+ const canvasCtx = canvas.getContext('2d')
+
+ const avatarX = 0
+ const avatarY = 0
+
+ // canvasCtx.beginPath()
+ // canvasCtx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2, true)
+ // canvasCtx.clip()
+ // canvasCtx.closePath()
+ // canvasCtx.restore()
+ canvasCtx.drawImage(avatarImage, avatarX, avatarY, avatarSize, avatarSize)
+
+ return canvas
+ }
+}
+
+async function downloadAvatarImage (user) {
+ let avatarImage
+
+ let nameLetters
+ if (user.first_name && user.last_name) nameLetters = runes(user.first_name)[0] + (runes(user.last_name || '')[0])
+ else {
+ let name = user.first_name || user.name || user.title
+ name = name.toUpperCase()
+ const nameWord = name.split(' ')
+
+ if (nameWord.length > 1) nameLetters = runes(nameWord[0])[0] + runes(nameWord.splice(-1)[0])[0]
+ else nameLetters = runes(nameWord[0])[0]
+ }
+
+ const cacheKey = user.id
+
+ const avatarImageCache = avatarCache.get(cacheKey)
+
+ const avatarColorArray = [
+ [ '#FF885E', '#FF516A' ], // red
+ [ '#FFCD6A', '#FFA85C' ], // orange
+ [ '#E0A2F3', '#D669ED' ], // purple
+ [ '#A0DE7E', '#54CB68' ], // green
+ [ '#53EDD6', '#28C9B7' ], // sea
+ [ '#72D5FD', '#2A9EF1' ], // blue
+ [ '#FFA8A8', '#FF719A' ] // pink
+ ]
+
+ const nameIndex = Math.abs(user.id) % 7
+
+ const avatarColor = avatarColorArray[nameIndex]
+
+ if (avatarImageCache) {
+ avatarImage = avatarImageCache
+ } else if (user.photo && user.photo.url) {
+ avatarImage = await loadImage(user.photo.url)
+ } else {
+ try {
+ let userPhoto, userPhotoUrl
+
+ if (user.photo && user.photo.big_file_id) userPhotoUrl = await telegram.getFileLink(user.photo.big_file_id).catch(console.error)
+
+ if (!userPhotoUrl) {
+ const getChat = await telegram.getChat(user.id).catch(console.error)
+ if (getChat && getChat.photo && getChat.photo.big_file_id) userPhoto = getChat.photo.big_file_id
+
+ if (userPhoto) userPhotoUrl = await telegram.getFileLink(userPhoto)
+ else if (user.username) userPhotoUrl = `https://telega.one/i/userpic/320/${user.username}.jpg`
+ else avatarImage = await loadImage(await avatarImageLetters(nameLetters, avatarColor))
+ }
+
+ if (userPhotoUrl) avatarImage = await loadImage(userPhotoUrl)
+
+ avatarCache.set(cacheKey, avatarImage)
+ } catch (error) {
+ avatarImage = await loadImage(await avatarImageLetters(nameLetters, avatarColor))
+ }
+ }
+
+ return avatarImage
+}
+
+async function avatarImageLetters (letters, color) {
+ const size = 500
+ const canvas = createCanvas(size, size)
+ const context = canvas.getContext('2d')
+
+ const gradient = context.createLinearGradient(0, 0, canvas.width, canvas.height)
+
+ gradient.addColorStop(0, color[0])
+ gradient.addColorStop(1, color[1])
+
+ context.fillStyle = gradient
+ context.fillRect(0, 0, canvas.width, canvas.height)
+
+ const drawLetters = await this.drawMultilineText(
+ letters,
+ null,
+ size / 2,
+ '#FFF',
+ 0,
+ size,
+ size * 5,
+ size * 5
+ )
+
+ context.drawImage(drawLetters, (canvas.width - drawLetters.width) / 2, (canvas.height - drawLetters.height) / 1.5)
+
+ return canvas.toBuffer()
+}
+
+module.exports = drawAvatar
diff --git a/utils/get-view.js b/utils/get-view.js
new file mode 100644
index 0000000..0a9654d
--- /dev/null
+++ b/utils/get-view.js
@@ -0,0 +1,11 @@
+const path = require('path')
+const fs = require('fs')
+const Handlebars = require('handlebars')
+
+module.exports = viewName => {
+ const template = fs.readFileSync(
+ path.resolve(`./views/${viewName}.hbs`)
+ ).toString('utf-8')
+
+ return Handlebars.compile(template)
+}
diff --git a/utils/quote-generate.js b/utils/quote-generate.js
index 317e41a..c8e9a2e 100644
--- a/utils/quote-generate.js
+++ b/utils/quote-generate.js
@@ -1,7 +1,6 @@
const fs = require('fs')
-const { createCanvas, registerFont, Image } = require('canvas')
+const { createCanvas, registerFont, Image, loadImage } = require('canvas')
const EmojiDbLib = require('emoji-db')
-const { loadImage } = require('canvas')
const loadImageFromUrl = require('./image-load-url')
const sharp = require('sharp')
const Jimp = require('jimp')
@@ -12,9 +11,12 @@ const zlib = require('zlib')
const { Telegram } = require('telegraf')
const render = require('./render')
-const { quote: view } = require('./views')
+const getView = require('./get-view')
+
+const drawAvatar = require('./draw-avatar')
const emojiDb = new EmojiDbLib({ useDefaultDb: true })
+const buildContent = getView('message')
function loadFont () {
console.log('font load start')
@@ -37,13 +39,6 @@ loadFont()
const emojiImageByBrand = require('./emoji-image')
-const LRU = require('lru-cache')
-
-const avatarCache = new LRU({
- max: 20,
- maxAge: 1000 * 60 * 5
-})
-
// write a nodejs function that accepts 2 colors. the first is the background color and the second is the text color. as a result, the first color should come out brighter or darker depending on the contrast. for example, if the first text is dark, then make the second brighter and return it. you need to change not the background color, but the text color
// here are all the possible colors that will be passed as the second argument. the first color can be any
@@ -116,97 +111,6 @@ class QuoteGenerate {
this.telegram = new Telegram(botToken)
}
- async avatarImageLatters (letters, color) {
- const size = 500
- const canvas = createCanvas(size, size)
- const context = canvas.getContext('2d')
-
- const gradient = context.createLinearGradient(0, 0, canvas.width, canvas.height)
-
- gradient.addColorStop(0, color[0])
- gradient.addColorStop(1, color[1])
-
- context.fillStyle = gradient
- context.fillRect(0, 0, canvas.width, canvas.height)
-
- const drawLetters = await this.drawMultilineText(
- letters,
- null,
- size / 2,
- '#FFF',
- 0,
- size,
- size * 5,
- size * 5
- )
-
- context.drawImage(drawLetters, (canvas.width - drawLetters.width) / 2, (canvas.height - drawLetters.height) / 1.5)
-
- return canvas.toBuffer()
- }
-
- async downloadAvatarImage (user) {
- let avatarImage
-
- let nameLatters
- if (user.first_name && user.last_name) nameLatters = runes(user.first_name)[0] + (runes(user.last_name || '')[0])
- else {
- let name = user.first_name || user.name || user.title
- name = name.toUpperCase()
- const nameWord = name.split(' ')
-
- if (nameWord.length > 1) nameLatters = runes(nameWord[0])[0] + runes(nameWord.splice(-1)[0])[0]
- else nameLatters = runes(nameWord[0])[0]
- }
-
- const cacheKey = user.id
-
- const avatarImageCache = avatarCache.get(cacheKey)
-
- const avatarColorArray = [
- [ '#FF885E', '#FF516A' ], // red
- [ '#FFCD6A', '#FFA85C' ], // orange
- [ '#E0A2F3', '#D669ED' ], // purple
- [ '#A0DE7E', '#54CB68' ], // green
- [ '#53EDD6', '#28C9B7' ], // sea
- [ '#72D5FD', '#2A9EF1' ], // blue
- [ '#FFA8A8', '#FF719A' ] // pink
- ]
-
- const nameIndex = Math.abs(user.id) % 7
-
- const avatarColor = avatarColorArray[nameIndex]
-
- if (avatarImageCache) {
- avatarImage = avatarImageCache
- } else if (user.photo && user.photo.url) {
- avatarImage = await loadImage(user.photo.url)
- } else {
- try {
- let userPhoto, userPhotoUrl
-
- if (user.photo && user.photo.big_file_id) userPhotoUrl = await this.telegram.getFileLink(user.photo.big_file_id).catch(console.error)
-
- if (!userPhotoUrl) {
- const getChat = await this.telegram.getChat(user.id).catch(console.error)
- if (getChat && getChat.photo && getChat.photo.big_file_id) userPhoto = getChat.photo.big_file_id
-
- if (userPhoto) userPhotoUrl = await this.telegram.getFileLink(userPhoto)
- else if (user.username) userPhotoUrl = `https://telega.one/i/userpic/320/${user.username}.jpg`
- else avatarImage = await loadImage(await this.avatarImageLatters(nameLatters, avatarColor))
- }
-
- if (userPhotoUrl) avatarImage = await loadImage(userPhotoUrl)
-
- avatarCache.set(cacheKey, avatarImage)
- } catch (error) {
- avatarImage = await loadImage(await this.avatarImageLatters(nameLatters, avatarColor))
- }
- }
-
- return avatarImage
- }
-
ungzip (input, options) {
return new Promise((resolve, reject) => {
zlib.gunzip(input, options, (error, result) => {
@@ -683,29 +587,6 @@ class QuoteGenerate {
return canvas
}
- async drawAvatar (user) {
- const avatarImage = await this.downloadAvatarImage(user)
-
- if (avatarImage) {
- const avatarSize = avatarImage.naturalHeight
-
- const canvas = createCanvas(avatarSize, avatarSize)
- const canvasCtx = canvas.getContext('2d')
-
- const avatarX = 0
- const avatarY = 0
-
- // canvasCtx.beginPath()
- // canvasCtx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2, true)
- // canvasCtx.clip()
- // canvasCtx.closePath()
- // canvasCtx.restore()
- canvasCtx.drawImage(avatarImage, avatarX, avatarY, avatarSize, avatarSize)
-
- return canvas
- }
- }
-
drawLineSegment (ctx, x, y, width, isEven) {
ctx.lineWidth = 35 // how thick the line is
ctx.strokeStyle = '#aec6cf' // what color our line is
@@ -958,9 +839,9 @@ class QuoteGenerate {
}
- const avatarImage = await this.drawAvatar(message.from)
+ const avatarImage = await drawAvatar(message.from)
- const content = view({
+ const content = buildContent({
width,
height,
scale,
@@ -972,8 +853,7 @@ class QuoteGenerate {
text: message.text ?? '',
})
- const page = await render(content)
- const image = await page.locator('#quote').screenshot()
+ const image = await render(content, '#quote')
// convert to canvas for the generator
const canvas = createCanvas(width, height)
@@ -1050,7 +930,7 @@ class QuoteGenerate {
}
let avatarCanvas
- if (message.avatar) avatarCanvas = await this.drawAvatar(message.from)
+ if (message.avatar) avatarCanvas = await drawAvatar(message.from)
let replyName, replyNameColor, replyText
if (message.replyMessage && message.replyMessage.name && message.replyMessage.text) {
diff --git a/utils/render.js b/utils/render.js
index 3fbe2e9..5d8a763 100644
--- a/utils/render.js
+++ b/utils/render.js
@@ -4,8 +4,13 @@ const promise = webkit.launch()
.then(browser => browser.newContext())
.then(context => context.newPage())
-module.exports = async (content) => {
+module.exports = async (content, selector) => {
const page = await promise
await page.setContent(content)
- return page
+ const screenshot = await page.locator(selector).screenshot({
+ type: 'png',
+ scale: 'css',
+ omitBackground: true
+ })
+ return screenshot
}
diff --git a/utils/telegram.js b/utils/telegram.js
new file mode 100644
index 0000000..087e5ab
--- /dev/null
+++ b/utils/telegram.js
@@ -0,0 +1,5 @@
+const { Telegraf } = require('telegraf')
+
+const botToken = process.env.BOT_TOKEN
+
+module.exports = new Telegraf(botToken)
diff --git a/utils/views.js b/utils/views.js
deleted file mode 100644
index 3c515d3..0000000
--- a/utils/views.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const path = require('path')
-const fs = require('fs')
-const Handlebars = require('handlebars')
-
-const quote = Handlebars.compile(
- fs.readFileSync(path.resolve('./views/quote.hbs')).toString('utf-8')
-)
-
-module.exports = {
- quote
-}
diff --git a/views/message.hbs b/views/message.hbs
new file mode 100644
index 0000000..1c6a1b9
--- /dev/null
+++ b/views/message.hbs
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+ {{name}}
+ {{emojiStatus}}
+
+ {{text}}
+
+
+
diff --git a/views/quote.hbs b/views/quote.hbs
index 1c6a1b9..da91fc8 100644
--- a/views/quote.hbs
+++ b/views/quote.hbs
@@ -2,51 +2,130 @@
+
-
-
- {{name}}
- {{emojiStatus}}
-
- {{text}}
-
+
+ {{#each messages}}
+ -
+
+
+
+
+ {{this.from.name}}
+
+
+
+
+ {{this.text}}
+
+
+
+ {{/each}}
+
From a922776cc69a2bb08c14cfdf77adba50c14620b8 Mon Sep 17 00:00:00 2001
From: arelive
Date: Wed, 7 Jun 2023 22:21:35 +0300
Subject: [PATCH 06/60] fix: using dedicated page for each async thread
---
test/async.js | 6 +++---
test/stress.js | 4 ++--
utils/render.js | 10 ++++++----
views/quote.hbs | 5 ++---
4 files changed, 13 insertions(+), 12 deletions(-)
diff --git a/test/async.js b/test/async.js
index 1ca0a34..b7275e2 100644
--- a/test/async.js
+++ b/test/async.js
@@ -10,12 +10,12 @@ const generate = require('../methods/generate')
const lorem = new LoremIpsum({
sentencesPerParagraph: {
- max: 8,
+ max: 4,
min: 1
},
wordsPerSentence: {
max: 16,
- min: 4
+ min: 2
}
})
@@ -66,6 +66,6 @@ for (let i = 0; i < nQuotes; i++) {
path.resolve(`./test/${i}.png`), buffer,
err => err && console.error(err)
)
- console.timeLog(i)
+ console.timeEnd(i)
})
}
diff --git a/test/stress.js b/test/stress.js
index fc10cdd..8b90ca9 100644
--- a/test/stress.js
+++ b/test/stress.js
@@ -8,12 +8,12 @@ require('../app')
const lorem = new LoremIpsum({
sentencesPerParagraph: {
- max: 8,
+ max: 4,
min: 1
},
wordsPerSentence: {
max: 16,
- min: 4
+ min: 2
}
})
diff --git a/utils/render.js b/utils/render.js
index 5d8a763..ccda34f 100644
--- a/utils/render.js
+++ b/utils/render.js
@@ -1,16 +1,18 @@
const { webkit } = require('playwright')
-const promise = webkit.launch()
- .then(browser => browser.newContext())
- .then(context => context.newPage())
+const promise = webkit.launch().then(browser => browser.newContext())
module.exports = async (content, selector) => {
- const page = await promise
+ const context = await promise
+ const page = await context.newPage()
await page.setContent(content)
+ await page.waitForSelector(selector, { state: 'visible' })
+
const screenshot = await page.locator(selector).screenshot({
type: 'png',
scale: 'css',
omitBackground: true
})
+
return screenshot
}
diff --git a/views/quote.hbs b/views/quote.hbs
index da91fc8..dcb5028 100644
--- a/views/quote.hbs
+++ b/views/quote.hbs
@@ -30,15 +30,14 @@
.message-from-avatar {
display: block;
float: left;
- margin: 0 10px 0 5px;
+ margin-left: 5px;
width: 50px;
height: 50px;
border-radius: 50%;
}
.message {
- float: left;
+ margin-left: 65px;
padding: 14px;
- max-width: calc(100% - 60px);
background: linear-gradient(45deg, {{background.color1}}, {{background.color2}});
border-radius: 25px;
}
From d8b24055f625749ba207fad7099850d29083b73b Mon Sep 17 00:00:00 2001
From: arelive
Date: Thu, 8 Jun 2023 18:17:17 +0300
Subject: [PATCH 07/60] resize images to 512px; detect color scheme
---
methods/generate.js | 211 +++++++++-------------------------------
test/.gitignore | 1 +
test/stress.js | 26 ++++-
utils/light-or-dark.js | 38 ++++++++
utils/quote-generate.js | 42 +-------
utils/render.js | 1 +
views/image.hbs | 99 +++++++++++++++++++
views/quote.hbs | 69 +++----------
8 files changed, 224 insertions(+), 263 deletions(-)
create mode 100644 utils/light-or-dark.js
create mode 100644 views/image.hbs
diff --git a/methods/generate.js b/methods/generate.js
index f2f0638..cbb808f 100644
--- a/methods/generate.js
+++ b/methods/generate.js
@@ -6,6 +6,7 @@ const sharp = require('sharp')
const render = require('../utils/render')
const getView = require('../utils/get-view')
const drawAvatar = require('../utils/draw-avatar')
+const lightOrDark = require('../utils/light-or-dark')
const normalizeColor = (color) => {
const canvas = createCanvas(0, 0)
@@ -49,19 +50,31 @@ const imageAlpha = (image, alpha) => {
return canvas
}
-const buildContent = getView('quote')
-// FIXME: doest work
-const bgImageURL = fs.readFileSync(
- path.resolve('./assets/pattern_02.png')
-).toString('base64')
+
+const userColors = {
+ light: ['#FC5C51', '#FA790F', '#895DD5', '#0FB297', '#0FC9D6', '#3CA5EC', '#D54FAF'],
+ dark: ['#FF8E86', '#FFA357', '#B18FFF', '#4DD6BF', '#45E8D1', '#7AC9FF', '#FF7FD5']
+}
+// FIXME: doesnt work
+const bgImageURL = 'file://' + path.resolve('./assets/pattern_02.png').replace(/\\/g, '/')
module.exports = async (parm) => {
- if (!parm) return { error: 'query_empty' }
- if (!parm.messages || !parm.messages.length) return { error: 'messages_empty' }
+ if (!parm || typeof parm != 'object') {
+ return { error: 'query_empty' }
+ }
+ let type = parm.type || 'png'
+ const format = parm.format || ''
+ const ext = parm.ext || false
+ const scale = parm.scale ? Math.min(parseFloat(parm.scale), 20) : 2
let backgroundColor = parm.backgroundColor || '//#292232'
- let backgroundColorOne, backgroundColorTwo
+ let messages = parm.messages?.filter(message => message)
+
+ if (!messages?.length) {
+ return { error: 'messages_empty' }
+ }
+ let backgroundColorOne, backgroundColorTwo
const backgroundColorSplit = backgroundColor.split('/')
if (backgroundColorSplit && backgroundColorSplit.length > 1 && backgroundColorSplit[0] !== '') {
@@ -77,192 +90,60 @@ module.exports = async (parm) => {
backgroundColorTwo = backgroundColor
}
- const scale = parm.scale ? Math.min(parseFloat(parm.scale), 20) : 2
- const nameColors = ['red', 'orange', 'purple', 'green', 'sea', 'blue', 'pink']
+ const theme = lightOrDark(backgroundColorOne)
- const messages = await Promise.all(parm.messages
- .filter(message => message)
+ messages = await Promise.all(messages
.map(async message => {
- const avatarURL = await drawAvatar(message.from)
+ const avatar = await drawAvatar(message.from)
+ const userColor = userColors[theme][message.from?.id ? Math.abs(message.from.id) % 7 : 1]
return {
from: {
name: message.from?.name ?? '',
- color: nameColors[message.from?.id ? Math.abs(message.from.id) % 7 : 1],
+ color: userColor,
emoji_status: message.from?.emojiStatus ?? '',
- avatar: { url: avatarURL.toDataURL() }
+ avatar: { url: avatar.toDataURL() }
},
text: message.text ?? ''
}
})
)
- if (!messages.length) {
- return { error: 'empty_messages' }
- }
-
- const content = buildContent({
+ const content = getView(type)({
scale,
- width: parm.width * scale,
- height: parm.height * scale,
- theme: 'light',
+ theme,
background: {
- image: { url: bgImageURL, },
+ image: { url: bgImageURL },
color1: colorLuminance(backgroundColorTwo, 0.15),
color2: colorLuminance(backgroundColorOne, 0.15)
},
messages,
})
- fs.writeFileSync('./content.html', content)
- let { type, format, ext } = parm
- const quoteImage = await render(content, '#quote')
- const { width, height } = await sharp(quoteImage).metadata()
- const image = ext ? quoteImage : quoteImage.toString('base64')
+ let image = await render(content, '#quote')
+ const imageSharp = await sharp(image)
+ const { width, height } = await imageSharp.metadata()
- if ((!type && ext) || (type != 'image' && height > 1024 * 2)) {
+ // if height is more than 2 width, return png instead quote
+ if (type == 'quote' && height > width * 2) {
type = 'png'
}
+ if (type == 'quote') {
+ const maxWidth = 512
+ const maxHeight = 512
+
+ imageSharp.resize(height > width ? { height: maxHeight } : { width: maxWidth })
+
+ image = format == 'png' ?
+ await imageSharp.png().toBuffer() :
+ await imageSharp.webp({ lossless: true, force: true }).toBuffer()
+ }
return {
- image,
+ image: ext ? image : image.toString('base64'),
type,
width,
height,
ext
}
-
-
- // deprecated // TODO
-/*
- for (const message of parm.messages) {
- if (message) {
- const canvasQuote = await quoteGenerate.generate(
- backgroundColorOne,
- backgroundColorTwo,
- message,
- parm.width,
- parm.height,
- parseFloat(parm.scale),
- parm.emojiBrand
- )
-
- quoteImages.push(canvasQuote)
- }
- }
-
- let canvasQuote
-
- if (quoteImages.length > 1) {
- let width = 0
- let height = 0
-
- for (let index = 0; index < quoteImages.length; index++) {
- if (quoteImages[index].width > width) width = quoteImages[index].width
- height += quoteImages[index].height
- }
-
- const quoteMargin = 5 * parm.scale
-
- const canvas = createCanvas(width, height + (quoteMargin * quoteImages.length))
- const canvasCtx = canvas.getContext('2d')
-
- let imageY = 0
-
- for (let index = 0; index < quoteImages.length; index++) {
- canvasCtx.drawImage(quoteImages[index], 0, imageY)
- imageY += quoteImages[index].height + quoteMargin
- }
- canvasQuote = canvas
- } else {
- canvasQuote = quoteImages[0]
- }
-
- let quoteImage
-
- if (type === 'quote') {
- const downPadding = 75
- const maxWidth = 512
- const maxHeight = 512
-
- const imageQuoteSharp = sharp(canvasQuote.toBuffer())
-
- if (canvasQuote.height > canvasQuote.width) imageQuoteSharp.resize({ height: maxHeight })
- else imageQuoteSharp.resize({ width: maxWidth })
-
- const canvasImage = await loadImage(await imageQuoteSharp.toBuffer())
-
- const canvasPadding = createCanvas(canvasImage.width, canvasImage.height + downPadding)
- const canvasPaddingCtx = canvasPadding.getContext('2d')
-
- canvasPaddingCtx.drawImage(canvasImage, 0, 0)
-
- const imageSharp = sharp(canvasPadding.toBuffer())
-
- if (canvasPadding.height >= canvasPadding.width) imageSharp.resize({ height: maxHeight })
- else imageSharp.resize({ width: maxWidth })
-
- if (format === 'png') quoteImage = await imageSharp.png().toBuffer()
- else quoteImage = await imageSharp.webp({ lossless: true, force: true }).toBuffer()
- } else if (type === 'image') {
- const heightPadding = 75 * parm.scale
- const widthPadding = 95 * parm.scale
-
- const canvasImage = await loadImage(canvasQuote.toBuffer())
-
- const canvasPic = createCanvas(canvasImage.width + widthPadding, canvasImage.height + heightPadding)
- const canvasPicCtx = canvasPic.getContext('2d')
-
- // radial gradient background (top left)
- const gradient = canvasPicCtx.createRadialGradient(
- canvasPic.width / 2,
- canvasPic.height / 2,
- 0,
- canvasPic.width / 2,
- canvasPic.height / 2,
- canvasPic.width / 2
- )
-
- const patternColorOne = colorLuminance(backgroundColorTwo, 0.15)
- const patternColorTwo = colorLuminance(backgroundColorOne, 0.15)
-
- gradient.addColorStop(0, patternColorOne)
- gradient.addColorStop(1, patternColorTwo)
-
- canvasPicCtx.fillStyle = gradient
- canvasPicCtx.fillRect(0, 0, canvasPic.width, canvasPic.height)
-
- const canvasPatternImage = await loadImage('./assets/pattern_02.png')
- // const canvasPatternImage = await loadImage('./assets/pattern_ny.png');
-
- const pattern = canvasPicCtx.createPattern(imageAlpha(canvasPatternImage, 0.3), 'repeat')
-
- canvasPicCtx.fillStyle = pattern
- canvasPicCtx.fillRect(0, 0, canvasPic.width, canvasPic.height)
-
- // Add shadow effect to the canvas image
- canvasPicCtx.shadowOffsetX = 8
- canvasPicCtx.shadowOffsetY = 8
- canvasPicCtx.shadowBlur = 13
- canvasPicCtx.shadowColor = 'rgba(0, 0, 0, 0.5)'
-
- // Draw the image to the canvas with padding centered
- canvasPicCtx.drawImage(canvasImage, widthPadding / 2, heightPadding / 2)
-
- canvasPicCtx.shadowOffsetX = 0
- canvasPicCtx.shadowOffsetY = 0
- canvasPicCtx.shadowBlur = 0
- canvasPicCtx.shadowColor = 'rgba(0, 0, 0, 0)'
-
- // write text button right
- canvasPicCtx.fillStyle = `rgba(0, 0, 0, 0.3)`
- canvasPicCtx.font = `${8 * parm.scale}px Noto Sans`
- canvasPicCtx.textAlign = 'right'
- canvasPicCtx.fillText('@QuotLyBot', canvasPic.width - 25, canvasPic.height - 25)
-
- quoteImage = await sharp(canvasPic.toBuffer()).png({ lossless: true, force: true }).toBuffer()
- } else {
- quoteImage = canvasQuote.toBuffer()
- }
-*/
}
diff --git a/test/.gitignore b/test/.gitignore
index e33609d..cd805b8 100644
--- a/test/.gitignore
+++ b/test/.gitignore
@@ -1 +1,2 @@
*.png
+*.webp
diff --git a/test/stress.js b/test/stress.js
index 8b90ca9..24a0a15 100644
--- a/test/stress.js
+++ b/test/stress.js
@@ -27,9 +27,7 @@ const nQuotes = parseInt(process.argv[2])
const json = {
botToken: process.env.BOT_TOKEN,
- type: 'quote',
- format: 'png',
- backgroundColor: '#FFFFFF',
+ backgroundColor: '',
width: 512,
height: 768,
scale: 2,
@@ -38,7 +36,7 @@ const nQuotes = parseInt(process.argv[2])
entities: [],
avatar: true,
from: {
- id: 1,
+ id: Math.floor(Math.random() * 100),
name: username,
photo: {
url: avatar
@@ -50,7 +48,25 @@ const nQuotes = parseInt(process.argv[2])
]
}
- await axios.post('http://localhost:3000/generate', json, {
+ await axios.post('http://localhost:3000/generate', {
+ ...json,
+ type: 'quote',
+ format: 'webp'
+ }, {
+ headers: { 'Content-Type': 'application/json' }
+ }).then(res => {
+ const buffer = Buffer.from(res.data.result.image, 'base64')
+ fs.writeFile(
+ path.resolve(`./test/${i}.webp`), buffer,
+ err => err && console.error(err)
+ )
+ }).catch(console.error)
+
+ await axios.post('http://localhost:3000/generate', {
+ ...json,
+ type: 'image',
+ format: 'png'
+ }, {
headers: { 'Content-Type': 'application/json' }
}).then(res => {
const buffer = Buffer.from(res.data.result.image, 'base64')
diff --git a/utils/light-or-dark.js b/utils/light-or-dark.js
new file mode 100644
index 0000000..9d07ddf
--- /dev/null
+++ b/utils/light-or-dark.js
@@ -0,0 +1,38 @@
+// https://codepen.io/andreaswik/pen/YjJqpK
+module.exports = (color) => {
+ let r, g, b
+
+ // Check the format of the color, HEX or RGB?
+ if (color.match(/^rgb/)) {
+ // If HEX --> store the red, green, blue values in separate variables
+ color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/)
+
+ r = color[1]
+ g = color[2]
+ b = color[3]
+ } else {
+ // If RGB --> Convert it to HEX: http://gist.github.com/983661
+ color = +('0x' + color.slice(1).replace(
+ color.length < 5 && /./g, '$&$&'
+ )
+ )
+
+ r = color >> 16
+ g = color >> 8 & 255
+ b = color & 255
+ }
+
+ // HSP (Highly Sensitive Poo) equation from http://alienryderflex.com/hsp.html
+ const hsp = Math.sqrt(
+ 0.299 * (r * r) +
+ 0.587 * (g * g) +
+ 0.114 * (b * b)
+ )
+
+ // Using the HSP value, determine whether the color is light or dark
+ if (hsp > 127.5) {
+ return 'light'
+ } else {
+ return 'dark'
+ }
+}
diff --git a/utils/quote-generate.js b/utils/quote-generate.js
index 3a6fd99..3c7755a 100644
--- a/utils/quote-generate.js
+++ b/utils/quote-generate.js
@@ -12,6 +12,7 @@ const zlib = require('zlib')
const render = require('./render')
const getView = require('./get-view')
const drawAvatar = require('./draw-avatar')
+const lightOrDark = require('./light-or-dark')
const emojiDb = new EmojiDbLib({ useDefaultDb: true })
const buildContent = getView('message')
@@ -163,45 +164,6 @@ class QuoteGenerate {
.map(x => parseInt(x, 16))
}
- // https://codepen.io/andreaswik/pen/YjJqpK
- lightOrDark (color) {
- let r, g, b
-
- // Check the format of the color, HEX or RGB?
- if (color.match(/^rgb/)) {
- // If HEX --> store the red, green, blue values in separate variables
- color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/)
-
- r = color[1]
- g = color[2]
- b = color[3]
- } else {
- // If RGB --> Convert it to HEX: http://gist.github.com/983661
- color = +('0x' + color.slice(1).replace(
- color.length < 5 && /./g, '$&$&'
- )
- )
-
- r = color >> 16
- g = color >> 8 & 255
- b = color & 255
- }
-
- // HSP (Highly Sensitive Poo) equation from http://alienryderflex.com/hsp.html
- const hsp = Math.sqrt(
- 0.299 * (r * r) +
- 0.587 * (g * g) +
- 0.114 * (b * b)
- )
-
- // Using the HSP value, determine whether the color is light or dark
- if (hsp > 127.5) {
- return 'light'
- } else {
- return 'dark'
- }
- }
-
async drawMultilineText (text, entities, fontSize, fontColor, textX, textY, maxWidth, maxHeight, emojiBrand = 'apple') {
if (maxWidth > 10000) maxWidth = 10000
if (maxHeight > 10000) maxHeight = 10000
@@ -779,7 +741,7 @@ class QuoteGenerate {
height *= scale
// check background style color black/light
- const backStyle = this.lightOrDark(backgroundColorOne)
+ const backStyle = lightOrDark(backgroundColorOne)
// historyPeer1NameFg: #c03d33; // red
diff --git a/utils/render.js b/utils/render.js
index ccda34f..ba3fa01 100644
--- a/utils/render.js
+++ b/utils/render.js
@@ -14,5 +14,6 @@ module.exports = async (content, selector) => {
omitBackground: true
})
+ page.close().catch(console.error)
return screenshot
}
diff --git a/views/image.hbs b/views/image.hbs
new file mode 100644
index 0000000..b741fa3
--- /dev/null
+++ b/views/image.hbs
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+ {{#each messages}}
+ -
+
+
+
+
+ {{this.from.name}}
+
+
+
+
+ {{this.text}}
+
+
+
+ {{/each}}
+
+ @QuotLyBot
+
+
diff --git a/views/quote.hbs b/views/quote.hbs
index dcb5028..e9db03e 100644
--- a/views/quote.hbs
+++ b/views/quote.hbs
@@ -9,19 +9,18 @@
}
#quote {
position: absolute;
- max-width: {{width}}px;
- max-height: {{height}}px;
+ padding-bottom: 75px;
+ max-width: 512px;
+ width: {{width}}px;
+ height: {{height}}px;
transform: scale({{scale}});
- /* background: radial-gradient({{background.color1}}, {{background.color2}}), url("{{background.image.url}}") top left repeat; */
background: none;
}
.messages-list {
list-style: none;
- overflow: hidden;
}
.message-item {
display: block;
- overflow: hidden;
margin-bottom: 5px;
}
.message-item:last-child {
@@ -33,12 +32,17 @@
margin-left: 5px;
width: 50px;
height: 50px;
+ font-family: 'Nono Sans', sans-serif;
+ font-size: 25px;
+ line-height: 50px;
+ text-align: center;
+ color: #FFF;
border-radius: 50%;
}
.message {
margin-left: 65px;
padding: 14px;
- background: linear-gradient(45deg, {{background.color1}}, {{background.color2}});
+ background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}});
border-radius: 25px;
}
.message-from-name {
@@ -51,58 +55,15 @@
white-space: nowrap;
text-overflow: ellipsis;
}
- .message-from-name.red {
- color: #FC5C51
- }
- .message-from-name.orange {
- color: #FA790F
- }
- .message-from-name.purple {
- color: #895DD5
- }
- .message-from-name.green {
- color: #0FB297
- }
- .message-from-name.sea {
- color: #0FC9D6
- }
- .message-from-name.blue {
- color: #3CA5EC
- }
- .message-from-name.pink {
- color: #D54FAF
- }
- .dark .message-from-name.red {
- color: #FF8E86
- }
- .dark .message-from-name.orange {
- color: #FFA357
- }
- .dark .message-from-name.purple {
- color: #B18FFF
- }
- .dark .message-from-name.green {
- color: #4DD6BF
- }
- .dark .message-from-name.sea {
- color: #45E8D1
- }
- .dark .message-from-name.blue {
- color: #7AC9FF
- }
- .dark .message-from-name.pink {
- color: #FF7FD5
- }
.message-text {
overflow: hidden;
width: 100%;
font-family: 'Noto Sans', sans-serif;
font-size: 24px;
line-height: 1.2;
- color: #000;
- }
- .dark .message-text {
color: #FFF;
+ mix-blend-mode: difference;
+ text-overflow: ellipsis;
}
@@ -111,10 +72,12 @@
{{#each messages}}
-
-
+
-
+
{{this.from.name}}
From 4a2b60412461e7cec72c5ac538b0225593b24353 Mon Sep 17 00:00:00 2001
From: arelive
Date: Thu, 8 Jun 2023 19:11:35 +0300
Subject: [PATCH 08/60] update tests
---
test/async.js | 55 +++++++++++++++++++++++++++++---------------------
test/stress.js | 34 +++++++++++++------------------
2 files changed, 46 insertions(+), 43 deletions(-)
diff --git a/test/async.js b/test/async.js
index b7275e2..ecb67f0 100644
--- a/test/async.js
+++ b/test/async.js
@@ -30,37 +30,46 @@ const queue = async.queue(
)
for (let i = 0; i < nQuotes; i++) {
- const text = lorem.generateParagraphs(1)
- const username = lorem.generateWords(2)
- const avatar = 'https://telegra.ph/file/59952c903fdfb10b752b3.jpg'
-
const json = {
botToken: process.env.BOT_TOKEN,
- type: 'quote',
- format: 'png',
- backgroundColor: '#FFFFFF',
+ backgroundColor: '',
width: 512,
height: 768,
scale: 2,
- messages: [
- {
- entities: [],
- avatar: true,
- from: {
- id: 1,
- name: username,
- photo: {
- url: avatar
- }
- },
- text: text,
- replyMessage: {}
- }
- ]
+ messages: Array.from({ length: i % 5 + 1 }, () => ({
+ entities: [],
+ avatar: true,
+ from: {
+ id: Math.floor(Math.random() * 100),
+ name: lorem.generateWords(2),
+ photo: {
+ url: 'https://telegra.ph/file/59952c903fdfb10b752b3.jpg'
+ }
+ },
+ text: lorem.generateParagraphs(1),
+ replyMessage: {}
+ }))
}
console.time(i)
- queue.push(json, res => {
+
+ queue.push({
+ ...json,
+ type: 'quote',
+ format: 'webp'
+ }, res => {
+ const buffer = Buffer.from(res.image, 'base64')
+ fs.writeFile(
+ path.resolve(`./test/${i}.webp`), buffer,
+ err => err && console.error(err)
+ )
+ })
+
+ queue.push({
+ ...json,
+ type: 'image',
+ format: 'png'
+ }, res => {
const buffer = Buffer.from(res.image, 'base64')
fs.writeFile(
path.resolve(`./test/${i}.png`), buffer,
diff --git a/test/stress.js b/test/stress.js
index 24a0a15..604c7cb 100644
--- a/test/stress.js
+++ b/test/stress.js
@@ -1,7 +1,7 @@
const axios = require('axios')
+const path = require('path')
const fs = require('fs')
const LoremIpsum = require('lorem-ipsum').LoremIpsum
-const path = require('path')
require('dotenv').config({ path: './.env' })
require('../app')
@@ -21,31 +21,25 @@ const nQuotes = parseInt(process.argv[2])
;(async () => {
for (let i = 0; i < nQuotes; i++) {
- const text = lorem.generateParagraphs(1)
- const username = lorem.generateWords(2)
- const avatar = 'https://telegra.ph/file/59952c903fdfb10b752b3.jpg'
-
const json = {
botToken: process.env.BOT_TOKEN,
backgroundColor: '',
width: 512,
height: 768,
scale: 2,
- messages: [
- {
- entities: [],
- avatar: true,
- from: {
- id: Math.floor(Math.random() * 100),
- name: username,
- photo: {
- url: avatar
- }
- },
- text: text,
- replyMessage: {}
- }
- ]
+ messages: Array.from({ length: i % 5 + 1 }, () => ({
+ entities: [],
+ avatar: true,
+ from: {
+ id: Math.floor(Math.random() * 100),
+ name: lorem.generateWords(2),
+ photo: {
+ url: 'https://telegra.ph/file/59952c903fdfb10b752b3.jpg'
+ }
+ },
+ text: lorem.generateParagraphs(1),
+ replyMessage: {}
+ }))
}
await axios.post('http://localhost:3000/generate', {
From bad8f25a326e3f05a7363d02632809a5db7fc71a Mon Sep 17 00:00:00 2001
From: arelive
Date: Thu, 8 Jun 2023 22:27:58 +0300
Subject: [PATCH 09/60] add reply messages; fix asset urls
---
app.js | 4 ++
methods/generate.js | 28 +++++++++++---
package-lock.json | 91 +++++++++++++++++++++++++++++++++++++++++++++
package.json | 2 +
test/stress.js | 5 ++-
utils/render.js | 2 +
views/image.hbs | 34 +++++++++++++++--
views/quote.hbs | 41 +++++++++++++++-----
8 files changed, 187 insertions(+), 20 deletions(-)
diff --git a/app.js b/app.js
index 1910928..230c06e 100644
--- a/app.js
+++ b/app.js
@@ -1,7 +1,10 @@
+const path = require('path')
const logger = require('koa-logger')
const responseTime = require('koa-response-time')
const bodyParser = require('koa-bodyparser')
const ratelimit = require('koa-ratelimit')
+const serve = require('koa-static')
+const mount = require('koa-mount')
const Router = require('koa-router')
const Koa = require('koa')
@@ -10,6 +13,7 @@ const app = new Koa()
app.use(logger())
app.use(responseTime())
app.use(bodyParser())
+app.use(mount('/assets', serve(path.resolve('./assets'))))
const ratelimitВb = new Map()
diff --git a/methods/generate.js b/methods/generate.js
index cbb808f..162f454 100644
--- a/methods/generate.js
+++ b/methods/generate.js
@@ -55,8 +55,7 @@ const userColors = {
light: ['#FC5C51', '#FA790F', '#895DD5', '#0FB297', '#0FC9D6', '#3CA5EC', '#D54FAF'],
dark: ['#FF8E86', '#FFA357', '#B18FFF', '#4DD6BF', '#45E8D1', '#7AC9FF', '#FF7FD5']
}
-// FIXME: doesnt work
-const bgImageURL = 'file://' + path.resolve('./assets/pattern_02.png').replace(/\\/g, '/')
+const bgImageURL = `http://localhost:${process.env.PORT}/assets/pattern_02.png`
module.exports = async (parm) => {
if (!parm || typeof parm != 'object') {
@@ -95,14 +94,33 @@ module.exports = async (parm) => {
messages = await Promise.all(messages
.map(async message => {
const avatar = await drawAvatar(message.from)
- const userColor = userColors[theme][message.from?.id ? Math.abs(message.from.id) % 7 : 1]
+ const fromId = message.from?.id ? Math.abs(message.from.id) % 7 : 1
+ const userColor = userColors[theme][fromId]
+ let replyMessage = message.replyMessage
+
+ if (replyMessage && Object.keys(replyMessage) != 0) {
+ const replyFromId = replyMessage.from?.id ? Math.abs(replyMessage.from.id) % 7 : 1
+ const replyUserColor = userColors[theme][replyFromId]
+
+ replyMessage = {
+ from: {
+ name: replyMessage.name ?? '',
+ color: replyUserColor
+ },
+ text: replyMessage.text
+ }
+ } else {
+ replyMessage = null
+ }
+
return {
from: {
name: message.from?.name ?? '',
color: userColor,
emoji_status: message.from?.emojiStatus ?? '',
- avatar: { url: avatar.toDataURL() }
+ avatar: { url: avatar.toDataURL() } // TODO optimize
},
+ replyMessage,
text: message.text ?? ''
}
})
@@ -116,7 +134,7 @@ module.exports = async (parm) => {
color1: colorLuminance(backgroundColorTwo, 0.15),
color2: colorLuminance(backgroundColorOne, 0.15)
},
- messages,
+ messages
})
let image = await render(content, '#quote')
diff --git a/package-lock.json b/package-lock.json
index 0d91364..c97fe93 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,9 +18,11 @@
"koa": "^2.11.0",
"koa-bodyparser": "^4.2.1",
"koa-logger": "^3.2.1",
+ "koa-mount": "^4.0.0",
"koa-ratelimit": "^4.2.1",
"koa-response-time": "^2.1.0",
"koa-router": "^7.4.0",
+ "koa-static": "^5.0.0",
"lottie-node": "^2.0.0",
"lottie-web": "^5.7.8",
"lru-cache": "^5.1.1",
@@ -2887,6 +2889,18 @@
"node": ">= 7.6.0"
}
},
+ "node_modules/koa-mount": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/koa-mount/-/koa-mount-4.0.0.tgz",
+ "integrity": "sha512-rm71jaA/P+6HeCpoRhmCv8KVBIi0tfGuO/dMKicbQnQW/YJntJ6MnnspkodoA4QstMVEZArsCphmd0bJEtoMjQ==",
+ "dependencies": {
+ "debug": "^4.0.1",
+ "koa-compose": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 7.6.0"
+ }
+ },
"node_modules/koa-ratelimit": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/koa-ratelimit/-/koa-ratelimit-4.3.0.tgz",
@@ -2938,6 +2952,39 @@
"any-promise": "^1.1.0"
}
},
+ "node_modules/koa-send": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz",
+ "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "http-errors": "^1.7.3",
+ "resolve-path": "^1.4.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/koa-static": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz",
+ "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==",
+ "dependencies": {
+ "debug": "^3.1.0",
+ "koa-send": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 7.6.0"
+ }
+ },
+ "node_modules/koa-static/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
"node_modules/levn": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
@@ -3849,6 +3896,50 @@
"node": ">=4"
}
},
+ "node_modules/resolve-path": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz",
+ "integrity": "sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==",
+ "dependencies": {
+ "http-errors": "~1.6.2",
+ "path-is-absolute": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/resolve-path/node_modules/depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/resolve-path/node_modules/http-errors": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+ "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==",
+ "dependencies": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.0",
+ "statuses": ">= 1.4.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/resolve-path/node_modules/inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="
+ },
+ "node_modules/resolve-path/node_modules/setprototypeof": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
+ },
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
diff --git a/package.json b/package.json
index b7cfaa0..a9dbfb0 100644
--- a/package.json
+++ b/package.json
@@ -30,9 +30,11 @@
"koa": "^2.11.0",
"koa-bodyparser": "^4.2.1",
"koa-logger": "^3.2.1",
+ "koa-mount": "^4.0.0",
"koa-ratelimit": "^4.2.1",
"koa-response-time": "^2.1.0",
"koa-router": "^7.4.0",
+ "koa-static": "^5.0.0",
"lottie-node": "^2.0.0",
"lottie-web": "^5.7.8",
"lru-cache": "^5.1.1",
diff --git a/test/stress.js b/test/stress.js
index 604c7cb..dbe8382 100644
--- a/test/stress.js
+++ b/test/stress.js
@@ -38,7 +38,10 @@ const nQuotes = parseInt(process.argv[2])
}
},
text: lorem.generateParagraphs(1),
- replyMessage: {}
+ replyMessage: Math.random() < 0.3 ? {
+ name: lorem.generateWords(2),
+ text: lorem.generateParagraphs(1)
+ } : {}
}))
}
diff --git a/utils/render.js b/utils/render.js
index ba3fa01..1d60c0f 100644
--- a/utils/render.js
+++ b/utils/render.js
@@ -5,6 +5,8 @@ const promise = webkit.launch().then(browser => browser.newContext())
module.exports = async (content, selector) => {
const context = await promise
const page = await context.newPage()
+
+ page.on('console', console.log)
await page.setContent(content)
await page.waitForSelector(selector, { state: 'visible' })
diff --git a/views/image.hbs b/views/image.hbs
index b741fa3..9462e92 100644
--- a/views/image.hbs
+++ b/views/image.hbs
@@ -13,7 +13,7 @@
max-width: 512px;
width: {{width}}px;
height: {{height}}px;
- //transform: scale({{scale}});
+ transform: scale({{scale}});
background:
url("{{background.image.url}}") top left / cover repeat,
radial-gradient(ellipse farthest-corner at center, {{background.color2}}, {{background.color1}});
@@ -47,20 +47,34 @@
border-radius: 25px;
box-shadow: 8px 8px 13px rgba(0, 0, 0, .5);
}
- .message-from-name {
+ .message-from-name,
+ .message-reply-from-name {
overflow: hidden;
width: 100%;
font: bold 22px/1.2 'Noto Sans', sans-serif;
white-space: nowrap;
text-overflow: ellipsis;
}
- .message-text {
+ .message-reply {
+ margin: 8px 0;
+ padding: 0 14px;
+ border-left: 3px solid;
+ }
+ .message-reply-from-name {
+ font-size: 16px;
+ }
+ .message-text,
+ .message-reply-text {
overflow: hidden;
width: 100%;
font: 24px/1.2 'Noto Sans', sans-serif;
+ text-overflow: ellipsis;
color: #FFF;
mix-blend-mode: difference;
- text-overflow: ellipsis;
+ }
+ .message-reply-text {
+ font-size: 21px;
+ white-space: nowrap;
}
.watermark {
position: absolute;
@@ -72,6 +86,7 @@
}
+
+ {{#if this.replyMessage}}
+ {{#with this.replyMessage}}
+
+
+ {{this.from.name}}
+
+
+
{{this.text}}
+
+ {{/with}}
+ {{/if}}
{{this.text}}
diff --git a/views/quote.hbs b/views/quote.hbs
index e9db03e..4386e94 100644
--- a/views/quote.hbs
+++ b/views/quote.hbs
@@ -45,28 +45,38 @@
background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}});
border-radius: 25px;
}
- .message-from-name {
+ .message-from-name,
+ .message-reply-from-name {
overflow: hidden;
width: 100%;
- font-family: 'Noto Sans', sans-serif;
- font-size: 22px;
- line-height: 1.2;
- font-weight: 600;
+ font: bold 22px/1.2 'Noto Sans', sans-serif;
white-space: nowrap;
text-overflow: ellipsis;
}
- .message-text {
+ .message-reply {
+ margin: 8px 0;
+ padding: 0 14px;
+ border-left: 3px solid;
+ }
+ .message-reply-from-name {
+ font-size: 16px;
+ }
+ .message-text,
+ .message-reply-text {
overflow: hidden;
width: 100%;
- font-family: 'Noto Sans', sans-serif;
- font-size: 24px;
- line-height: 1.2;
+ font: 24px/1.2 'Noto Sans', sans-serif;
+ text-overflow: ellipsis;
color: #FFF;
mix-blend-mode: difference;
- text-overflow: ellipsis;
+ }
+ .message-reply-text {
+ font-size: 21px;
+ white-space: nowrap;
}
+
@@ -83,6 +93,17 @@
+ {{#if this.replyMessage}}
+ {{#with this.replyMessage}}
+
+
+ {{this.from.name}}
+
+
+
{{this.text}}
+
+ {{/with}}
+ {{/if}}
{{this.text}}
From 7b79c31dfd9d030ef28614995d4da0c453da4b94 Mon Sep 17 00:00:00 2001
From: arelive
Date: Fri, 9 Jun 2023 18:52:37 +0300
Subject: [PATCH 10/60] add really avatars and initials
---
methods/generate.js | 119 ++++++++++++++++++++++---------------
utils/draw-avatar.js | 126 ----------------------------------------
utils/get-avatar-url.js | 35 +++++++++++
utils/telegram.js | 3 +-
views/image.hbs | 31 ++++++----
views/quote.hbs | 34 +++++++----
6 files changed, 153 insertions(+), 195 deletions(-)
delete mode 100644 utils/draw-avatar.js
create mode 100644 utils/get-avatar-url.js
diff --git a/methods/generate.js b/methods/generate.js
index 162f454..2c3b0ce 100644
--- a/methods/generate.js
+++ b/methods/generate.js
@@ -2,10 +2,11 @@ const path = require('path')
const fs = require('fs')
const { createCanvas, loadImage } = require('canvas')
const sharp = require('sharp')
+const runes = require('runes')
const render = require('../utils/render')
const getView = require('../utils/get-view')
-const drawAvatar = require('../utils/draw-avatar')
+const getAvatarURL = require('../utils/get-avatar-url')
const lightOrDark = require('../utils/light-or-dark')
const normalizeColor = (color) => {
@@ -50,6 +51,64 @@ const imageAlpha = (image, alpha) => {
return canvas
}
+const buildUser = async (user, theme, options={ getAvatar: false }) => {
+ const index = user.id ? Math.abs(user.id) % 7 : 1
+ const color = userColors[theme][index]
+ const emojiStatus = user.emojiStatus ?? ''
+
+ let photo = null
+ if (options.getAvatar) {
+ photo = user.photo || photo
+ if (!photo) {
+ const photoURL = await getAvatarURL(user)
+ photo = photoURL ? { url: photoURL } : photo
+ }
+ }
+
+ let name, initials
+
+ if (user.first_name && user.last_name) {
+ name = user.first_name + ' ' + user.last_name
+ initials = runes(user.first_name)[0] + runes(user.last_name)[0]
+ }
+ else {
+ name = user.name || user.first_name || user.title
+
+ if (typeof name == 'string') {
+ const nameWords = name.split(' ')
+ initials = runes(nameWords[0])[0]
+ if (nameWords.length > 1) {
+ initials += runes(nameWords.splice(-1)[0])[0]
+ }
+ }
+ else {
+ name == ''
+ initials = ''
+ }
+ }
+
+ return { name, initials, color, photo, emojiStatus }
+}
+
+const buildMessage = async (message, theme) => {
+ let replyMessage = message.replyMessage
+ if (replyMessage && Object.keys(replyMessage) != 0) {
+ replyMessage = {
+ from: await buildUser(replyMessage.from, theme),
+ text: replyMessage.text
+ }
+ }
+ else {
+ replyMessage = null
+ }
+
+ return {
+ from: await buildUser(message.from, theme, { getAvatar: message.avatar || false }),
+ replyMessage,
+ showAvatar: message.avatar,
+ text: message.text ?? ''
+ }
+}
const userColors = {
light: ['#FC5C51', '#FA790F', '#895DD5', '#0FB297', '#0FC9D6', '#3CA5EC', '#D54FAF'],
@@ -66,16 +125,12 @@ module.exports = async (parm) => {
const format = parm.format || ''
const ext = parm.ext || false
const scale = parm.scale ? Math.min(parseFloat(parm.scale), 20) : 2
- let backgroundColor = parm.backgroundColor || '//#292232'
- let messages = parm.messages?.filter(message => message)
-
- if (!messages?.length) {
- return { error: 'messages_empty' }
- }
+ let backgroundColor = parm.backgroundColor || '//#292232'
let backgroundColorOne, backgroundColorTwo
const backgroundColorSplit = backgroundColor.split('/')
+ // TODO effect colors with CSS
if (backgroundColorSplit && backgroundColorSplit.length > 1 && backgroundColorSplit[0] !== '') {
backgroundColorOne = normalizeColor(backgroundColorSplit[0])
backgroundColorTwo = normalizeColor(backgroundColorSplit[1])
@@ -91,51 +146,26 @@ module.exports = async (parm) => {
const theme = lightOrDark(backgroundColorOne)
- messages = await Promise.all(messages
- .map(async message => {
- const avatar = await drawAvatar(message.from)
- const fromId = message.from?.id ? Math.abs(message.from.id) % 7 : 1
- const userColor = userColors[theme][fromId]
- let replyMessage = message.replyMessage
-
- if (replyMessage && Object.keys(replyMessage) != 0) {
- const replyFromId = replyMessage.from?.id ? Math.abs(replyMessage.from.id) % 7 : 1
- const replyUserColor = userColors[theme][replyFromId]
-
- replyMessage = {
- from: {
- name: replyMessage.name ?? '',
- color: replyUserColor
- },
- text: replyMessage.text
- }
- } else {
- replyMessage = null
- }
-
- return {
- from: {
- name: message.from?.name ?? '',
- color: userColor,
- emoji_status: message.from?.emojiStatus ?? '',
- avatar: { url: avatar.toDataURL() } // TODO optimize
- },
- replyMessage,
- text: message.text ?? ''
- }
- })
+ const messages = await Promise.all(parm.messages
+ .filter(message => message)
+ .map(message => buildMessage(message, theme))
)
+ if (!messages?.length) {
+ return { error: 'messages_empty' }
+ }
+
const content = getView(type)({
scale,
theme,
background: {
image: { url: bgImageURL },
- color1: colorLuminance(backgroundColorTwo, 0.15),
- color2: colorLuminance(backgroundColorOne, 0.15)
+ color1: colorLuminance(backgroundColorOne, 0.15),
+ color2: colorLuminance(backgroundColorTwo, 0.15)
},
messages
})
+ fs.writeFileSync('./content.html', content)
let image = await render(content, '#quote')
const imageSharp = await sharp(image)
@@ -159,9 +189,6 @@ module.exports = async (parm) => {
return {
image: ext ? image : image.toString('base64'),
- type,
- width,
- height,
- ext
+ type, width, height, ext
}
}
diff --git a/utils/draw-avatar.js b/utils/draw-avatar.js
deleted file mode 100644
index 3d96283..0000000
--- a/utils/draw-avatar.js
+++ /dev/null
@@ -1,126 +0,0 @@
-const { createCanvas, loadImage } = require('canvas')
-const runes = require('runes')
-const LRU = require('lru-cache')
-
-const telegram = require('./telegram')
-
-const avatarCache = new LRU({
- max: 20,
- maxAge: 1000 * 60 * 5
-})
-
-async function drawAvatar (user) {
- const avatarImage = await downloadAvatarImage(user)
-
- if (avatarImage) {
- const avatarSize = avatarImage.naturalHeight
-
- const canvas = createCanvas(avatarSize, avatarSize)
- const canvasCtx = canvas.getContext('2d')
-
- const avatarX = 0
- const avatarY = 0
-
- // canvasCtx.beginPath()
- // canvasCtx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2, true)
- // canvasCtx.clip()
- // canvasCtx.closePath()
- // canvasCtx.restore()
- canvasCtx.drawImage(avatarImage, avatarX, avatarY, avatarSize, avatarSize)
-
- return canvas
- }
-}
-
-async function downloadAvatarImage (user) {
- let avatarImage
-
- let nameLetters
- if (user.first_name && user.last_name) nameLetters = runes(user.first_name)[0] + (runes(user.last_name || '')[0])
- else {
- let name = user.first_name || user.name || user.title
- name = name.toUpperCase()
- const nameWord = name.split(' ')
-
- if (nameWord.length > 1) nameLetters = runes(nameWord[0])[0] + runes(nameWord.splice(-1)[0])[0]
- else nameLetters = runes(nameWord[0])[0]
- }
-
- const cacheKey = user.id
-
- const avatarImageCache = avatarCache.get(cacheKey)
-
- const avatarColorArray = [
- [ '#FF885E', '#FF516A' ], // red
- [ '#FFCD6A', '#FFA85C' ], // orange
- [ '#E0A2F3', '#D669ED' ], // purple
- [ '#A0DE7E', '#54CB68' ], // green
- [ '#53EDD6', '#28C9B7' ], // sea
- [ '#72D5FD', '#2A9EF1' ], // blue
- [ '#FFA8A8', '#FF719A' ] // pink
- ]
-
- const nameIndex = Math.abs(user.id) % 7
-
- const avatarColor = avatarColorArray[nameIndex]
-
- if (avatarImageCache) {
- avatarImage = avatarImageCache
- } else if (user.photo && user.photo.url) {
- avatarImage = await loadImage(user.photo.url)
- } else {
- try {
- let userPhoto, userPhotoUrl
-
- if (user.photo && user.photo.big_file_id) userPhotoUrl = await telegram.getFileLink(user.photo.big_file_id).catch(console.error)
-
- if (!userPhotoUrl) {
- const getChat = await telegram.getChat(user.id).catch(console.error)
- if (getChat && getChat.photo && getChat.photo.big_file_id) userPhoto = getChat.photo.big_file_id
-
- if (userPhoto) userPhotoUrl = await telegram.getFileLink(userPhoto)
- else if (user.username) userPhotoUrl = `https://telega.one/i/userpic/320/${user.username}.jpg`
- else avatarImage = await loadImage(await avatarImageLetters(nameLetters, avatarColor))
- }
-
- if (userPhotoUrl) avatarImage = await loadImage(userPhotoUrl)
-
- avatarCache.set(cacheKey, avatarImage)
- } catch (error) {
- avatarImage = await loadImage(await avatarImageLetters(nameLetters, avatarColor))
- }
- }
-
- return avatarImage
-}
-
-async function avatarImageLetters (letters, color) {
- const size = 500
- const canvas = createCanvas(size, size)
- const context = canvas.getContext('2d')
-
- const gradient = context.createLinearGradient(0, 0, canvas.width, canvas.height)
-
- gradient.addColorStop(0, color[0])
- gradient.addColorStop(1, color[1])
-
- context.fillStyle = gradient
- context.fillRect(0, 0, canvas.width, canvas.height)
-
- const drawLetters = await this.drawMultilineText(
- letters,
- null,
- size / 2,
- '#FFF',
- 0,
- size,
- size * 5,
- size * 5
- )
-
- context.drawImage(drawLetters, (canvas.width - drawLetters.width) / 2, (canvas.height - drawLetters.height) / 1.5)
-
- return canvas.toBuffer()
-}
-
-module.exports = drawAvatar
diff --git a/utils/get-avatar-url.js b/utils/get-avatar-url.js
new file mode 100644
index 0000000..ffababb
--- /dev/null
+++ b/utils/get-avatar-url.js
@@ -0,0 +1,35 @@
+const LRU = require('lru-cache')
+
+const telegram = require('./telegram')
+
+const avatarCache = new LRU({
+ max: 20,
+ maxAge: 1000 * 60 * 5
+})
+
+module.exports = async (user) => {
+ let avatarURL = avatarCache.get(user.id)
+
+ if (!avatarURL && user.photo?.big_file_id) {
+ avatarURL = await telegram.getFileLink(user.photo.big_file_id).catch(console.error)
+ }
+
+ if (!avatarURL) {
+ const chat = await telegram.getChat(user.id).catch(console.error)
+ if (chat?.photo?.big_file_id) {
+ avatarURL = await telegram.getFileLink(chat.photo.big_file_id).catch(console.error)
+ }
+ }
+
+ if (!avatarURL && user.username) {
+ avatarURL = `https://telega.one/i/userpic/320/${user.username}.jpg`
+ }
+
+ if (!avatarURL) {
+ return null
+ }
+
+ avatarCache.set(user.id, avatarImage)
+ return avatarURL
+}
+
diff --git a/utils/telegram.js b/utils/telegram.js
index 087e5ab..325e251 100644
--- a/utils/telegram.js
+++ b/utils/telegram.js
@@ -1,5 +1,6 @@
const { Telegraf } = require('telegraf')
const botToken = process.env.BOT_TOKEN
+const bot = new Telegraf(botToken)
-module.exports = new Telegraf(botToken)
+module.exports = bot.telegram
diff --git a/views/image.hbs b/views/image.hbs
index 9462e92..e9ce4a5 100644
--- a/views/image.hbs
+++ b/views/image.hbs
@@ -16,22 +16,24 @@
transform: scale({{scale}});
background:
url("{{background.image.url}}") top left / cover repeat,
- radial-gradient(ellipse farthest-corner at center, {{background.color2}}, {{background.color1}});
+ radial-gradient(ellipse farthest-corner at center, {{background.color1}}, {{background.color2}});
}
.messages-list {
list-style: none;
}
.message-item {
+ position: relative;
display: block;
margin-bottom: 5px;
}
.message-item:last-child {
margin-bottom: 0;
}
- .message-from-avatar {
- display: block;
- float: left;
- margin-left: 5px;
+ .message-from-avatar,
+ .message-from-initials {
+ position: absolute;
+ top: 0;
+ left: 0;
width: 50px;
height: 50px;
font: 25px/2 'Nono Sans', sans-serif;
@@ -40,6 +42,9 @@
border-radius: 50%;
box-shadow: 8px 8px 13px rgba(0, 0, 0, .5);
}
+ .message-from-initials {
+ mix-blend-mode: hard-light;
+ }
.message {
margin-left: 65px;
padding: 14px;
@@ -92,14 +97,20 @@