Profile: Use sharp to prerender the profile background

including the user image. Also introduces axios as a dependency
This commit is contained in:
2022-09-13 00:18:30 +02:00
parent 494f574048
commit 54aee7624b
6 changed files with 63 additions and 7 deletions

View File

@@ -41,7 +41,7 @@
<text x="387.202px" y="200.273px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:12px;">DEV</text> <text x="387.202px" y="200.273px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:12px;">DEV</text>
<text x="387.202px" y="212.666px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:12px;">CONTRIB</text> <text x="387.202px" y="212.666px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:12px;">CONTRIB</text>
</g> </g>
<g transform="matrix(2.31555,0,0,1.66192,-505.061,-60.4179)"> <g transform="matrix(2.31555,0,0,1.66192,-505.061,-78.2437)">
<text x="443.006px" y="119.692px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:12px;">{{PROFILE_TEXT}}</text> <text x="443.006px" y="119.692px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:12px;">{{PROFILE_TEXT}}</text>
</g> </g>
</g> </g>
@@ -72,7 +72,7 @@
<g transform="matrix(0.166016,0,0,0.166016,499.979,0.476193)"> <g transform="matrix(0.166016,0,0,0.166016,499.979,0.476193)">
<use xlink:href="#_Image5" x="0" y="0" width="512px" height="512px"/> <use xlink:href="#_Image5" x="0" y="0" width="512px" height="512px"/>
</g> </g>
<g id="USER_IMAGE" transform="matrix(0.853333,0,0,0.853333,73.7407,8.57671)"> <g id="USER_IMAGE" transform="matrix(1.09227,0,0,1.09227,-12.8119,-1.02181)">
<circle cx="399.913" cy="77.84" r="58.594" style="fill:rgb(79,255,171);"/> <circle cx="399.913" cy="77.84" r="58.594" style="fill:rgb(79,255,171);"/>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

View File

@@ -1,6 +1,8 @@
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js");
const { Card, User, Character } = require("../models"); const { Card, User, Character } = require("../models");
const { UserUtils, Compositing, Rendering } = require("../util"); const { UserUtils, Compositing, Rendering } = require("../util");
const axios = require("axios");
const sharp = require("sharp");
const fs = require('fs'); const fs = require('fs');
const pageSize = 8; const pageSize = 8;
@@ -34,6 +36,16 @@ module.exports = {
profileTemplate = profileTemplate.replace(/{{CC}}/g, await Card.count({where: {userId: user.id}})); profileTemplate = profileTemplate.replace(/{{CC}}/g, await Card.count({where: {userId: user.id}}));
profileTemplate = profileTemplate.replace(/{{LVL}}/g, await user.level().currentLevel); profileTemplate = profileTemplate.replace(/{{LVL}}/g, await user.level().currentLevel);
let userImageBuffer = await axios.get(discordUser.displayAvatarURL({format: 'png', size: 128}), { responseType: 'arraybuffer' });
userImage = await sharp(userImageBuffer.data);
const rect = new Buffer.from(
'<svg><rect x="0" y="0" width="128" height="128" rx="100%" ry="100%"/></svg>'
);
userImage = await userImage.composite([{input: rect, blend: 'dest-in' }]).png().toBuffer();
let background = await sharp(Buffer.from(profileTemplate, 'utf8'))
.composite([{ input: userImage, left: 360, top: 20 }]).png().toBuffer();
let slots = ['slotOne', 'slotTwo', 'slotThree', 'slotFour']; let slots = ['slotOne', 'slotTwo', 'slotThree', 'slotFour'];
let renderedCards = []; let renderedCards = [];
for (slot of slots) { for (slot of slots) {
@@ -47,7 +59,7 @@ module.exports = {
} }
let profileImage = await Compositing.renderProfile(profile, profileTemplate, renderedCards); let profileImage = await Compositing.renderProfile(profile, background, renderedCards);
await interaction.editReply({ files: [profileImage] }); await interaction.editReply({ files: [profileImage] });
} }
} }

43
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@discordjs/rest": "^0.3.0", "@discordjs/rest": "^0.3.0",
"axios": "^0.27.2",
"discord-api-types": "^0.37.2", "discord-api-types": "^0.37.2",
"discord.js": "^14.0.0", "discord.js": "^14.0.0",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
@@ -176,6 +177,15 @@
"node": ">= 4.0.0" "node": ">= 4.0.0"
} }
}, },
"node_modules/axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"dependencies": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -691,6 +701,25 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/follow-redirects": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
@@ -2220,6 +2249,15 @@
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="
}, },
"axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"requires": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"balanced-match": { "balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2629,6 +2667,11 @@
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
} }
}, },
"follow-redirects": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
},
"form-data": { "form-data": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",

View File

@@ -12,6 +12,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@discordjs/rest": "^0.3.0", "@discordjs/rest": "^0.3.0",
"axios": "^0.27.2",
"discord-api-types": "^0.37.2", "discord-api-types": "^0.37.2",
"discord.js": "^14.0.0", "discord.js": "^14.0.0",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",

View File

@@ -6,14 +6,14 @@ const { Card } = require('../models');
//TODO: Handle missing images //TODO: Handle missing images
module.exports = { module.exports = {
name: "Compositing", name: "Compositing",
renderProfile: async function(profile, svgTemplate, renderedCards) { renderProfile: async function(profile, background, renderedCards) {
let hash = crypto.createHash('md5').update(JSON.stringify(profile) + svgTemplate).digest('hex'); let hash = crypto.createHash('md5').update(JSON.stringify(profile) + background).digest('hex');
let outFile = `/app/assets/image_cache/profiles/${hash}.gif`; let outFile = `/app/assets/image_cache/profiles/${hash}.gif`;
console.log('Rendering profile to ' + outFile); console.log('Rendering profile to ' + outFile);
//composite {overlay} {background} [{mask}] [-compose {method}] {result} //composite {overlay} {background} [{mask}] [-compose {method}] {result}
let args = ['svg:-', 'null:', let args = ['png:-', 'null:',
'\(', `${renderedCards[0]}`, '-coalesce', '\)', '\(', `${renderedCards[0]}`, '-coalesce', '\)',
'-geometry', '+25+85', '-compose', 'over', '-layers', 'composite', '-geometry', '+25+85', '-compose', 'over', '-layers', 'composite',
'null:', '\(', `${renderedCards[1]}`, '-coalesce', '-resize', '170x283', '\)', 'null:', '\(', `${renderedCards[1]}`, '-coalesce', '-resize', '170x283', '\)',
@@ -27,7 +27,7 @@ module.exports = {
console.log('GM Args: ' + args); console.log('GM Args: ' + args);
const composite = spawn('convert', args); const composite = spawn('convert', args);
composite.stdin.write(svgTemplate); composite.stdin.write(background);
composite.stdin.end(); composite.stdin.end();
composite.stderr.on('data', (data) => { composite.stderr.on('data', (data) => {