Add name card generation

This commit is contained in:
2025-11-10 22:16:07 +01:00
parent 40a5b37825
commit 57f8515491
5 changed files with 106 additions and 36 deletions

3
bot.js
View File

@@ -1,6 +1,7 @@
import {data} from "./wwwroot/core/appData.js"; import {data} from "./wwwroot/core/appData.js";
import JsonManager from "./wwwroot/core/utils/jsonManager.js"; import JsonManager from "./wwwroot/core/utils/jsonManager.js";
import {Logger} from "./wwwroot/core/logging/logger.js"; import {Logger} from "./wwwroot/core/logging/logger.js";
import {NameCardCreator} from "./wwwroot/core/welcome/nameCardCreator.js";
const launch = async () => { const launch = async () => {
try{ try{
@@ -30,7 +31,7 @@ const launch = async () => {
}); });
data.client.on('guildMemberAdd', member => { data.client.on('guildMemberAdd', member => {
const avatar = member.user.avatarURL(); NameCardCreator.getWelcomeCard("", member.user.avatarURL(), member.user.displayName)
}); });
process.on('SIGINT', async () => { process.on('SIGINT', async () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 764 KiB

After

Width:  |  Height:  |  Size: 764 KiB

View File

@@ -1,6 +1,7 @@
import {NameCardCreator} from "../wwwroot/core/welcome/nameCardCreator.js"; import {NameCardCreator} from "../wwwroot/core/welcome/nameCardCreator.js";
const templatePath = "./assets/name-card-template.png"; const templatePath = "./wwwroot/assets/name-card-template.png";
const avatarPath = "./assets/avatar-test.png"; const avatarPath = "./tests/assets/avatar-test.png";
const name = "Aude Vaiselle";
const res = await NameCardCreator.getWelcomeCard(templatePath, avatarPath); const creator = new NameCardCreator(templatePath);
await creator.getWelcomeCard(avatarPath, name)

View File

@@ -1,56 +1,58 @@
import sharp from "sharp"; import sharp from "sharp";
import {Logger} from "../logging/logger.js"; import {Logger} from "../logging/logger.js";
import fs from "fs";
export class NameCardCreator { export class NameCardCreator {
constructor(templatePath) {
this.templatePath = templatePath;
this.fontPath = "./wwwroot/assets/fonts/Fredoka/Fredoka-VariableFont_wdth,wght.ttf";
this.fontData = this.loadFontData();
}
/**
* Loads the template file into a sharp instance
* @returns {sharp.Sharp} sharp image object
*/
loadTemplate() {
return sharp(this.templatePath);
}
/** /**
* Combines a template image with a user avatar and saves it * Combines a template image with a user avatar and saves it
* @param {string} templatePath - path to the template image * @param avatarPath {string}
* @param {string} avatarPath - path to the avatar * @param name {string}
* @returns {Promise<sharp.OutputInfo>} resulting image buffer * @returns {Promise<sharp.OutputInfo>} resulting image buffer
*/ */
static async getWelcomeCard(templatePath, avatarPath) { async getWelcomeCard(avatarPath, name) {
try{ try{
const template = await this.loadTemplate(templatePath); const template = this.loadTemplate();
const avatar = await this.handleAvatar(avatarPath); const avatar = await this.handleAvatar(avatarPath);
const messageBuffer = await this.getMessageBuffer(`Hello ${name}!`);
const result = await template const result = await template
.composite([{ .composite([
input: avatar, { input: avatar, top: 215, left: 275 },
top: 215, { input: messageBuffer, top: 400, left: 1300 }
left: 200, ])
}])
.toFile("namecard.png") .toFile("namecard.png")
console.log("✅ Welcome card created: welcome-card.png"); console.log("✅ Welcome card created: welcome-card.png");
return result; return result;
} catch(err) { } catch(err) {
console.log(err);
await Logger.error("Unable to create name card", err); await Logger.error("Unable to create name card", err);
} }
} }
/** /**
* Loads the template file into a sharp instance *
* @param {string} path - file path * @param avatarPath
* @returns {sharp.Sharp} sharp image object * @returns {Promise<Buffer<ArrayBufferLike>>}
*/ */
static async loadTemplate(path) { async handleAvatar(avatarPath) {
return sharp(path);
}
/**
* Overlays an image (item) on top of a base image
* @param {sharp.Sharp|string} base - sharp image or path
* @param {{item:string, x:number, y:number}} overlay - overlay info
* @returns {Promise<sharp.Sharp>} combined image
*/
static async mergeBitmaps(base, { item, x, y }) {
return null;
}
static async handleAvatar(avatarPath) {
const avatarSize = 670; const avatarSize = 670;
const borderSize = 8; const borderSize = 8;
const radius = avatarSize / 2; const radius = avatarSize / 2;
@@ -61,6 +63,30 @@ export class NameCardCreator {
.png() .png()
.toBuffer(); .toBuffer();
const maskSvg = Buffer.from(`
<svg width="${avatarSize}" height="${avatarSize}" xmlns="http://www.w3.org/2000/svg">
<circle cx="${radius}" cy="${radius}" r="${radius}" fill="white"/>
</svg>
`);
const roundedAvatar = await sharp(avatarBuffer)
.composite([{ input: maskSvg, blend: "dest-in" }])
.png()
.toBuffer();
const roundedBorder = Buffer.from(`
<svg width="${totalSize}" height="${totalSize}" xmlns="http://www.w3.org/2000/svg">
<circle
cx="${totalSize / 2}"
cy="${totalSize / 2}"
r="${radius + borderSize / 2}"
stroke="#ffffff"
stroke-width="${borderSize}"
fill="none"
/>
</svg>
`)
return await sharp({ return await sharp({
create: { create: {
width: totalSize, width: totalSize,
@@ -68,11 +94,53 @@ export class NameCardCreator {
channels: 4, channels: 4,
background: "#0000" background: "#0000"
} }
}) }).composite([
.composite([ { input: roundedAvatar, top: 0, left: 0 },
{ input: avatarBuffer, top: borderSize, left: borderSize } { input: roundedBorder, top: borderSize, left: borderSize }
]) ])
.png() .png()
.toBuffer(); .toBuffer();
} }
async getMessageBuffer(message){
const messageSvg = this.getMessageSvg(message);
return Buffer.from(messageSvg, "utf-8");
}
/**
*
* @param message {string}
* @returns {string}
*/
getMessageSvg(message) {
const safeMessage =
message
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
return `
<svg width="1500" height="200" xmlns="http://www.w3.org/2000/svg">
<style>
@font-face {
font-family: 'Fredoka';
src: url(data:font/truetype;charset=utf-8;base64,${this.fontData}) format('truetype');
}
.title {
font-family: 'Fredoka', sans-serif;
fill: #ede6e6;
font-size: 80px;
font-weight: bold;
dominant-baseline: middle;
}
</style>
<text x="50%" y="50%" text-anchor="middle" class="title">${safeMessage}</text>
</svg>
`;
}
loadFontData(){
return fs.readFileSync(this.fontPath).toString("base64");
}
} }