Add name card generation
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 764 KiB |
@@ -1,56 +1,58 @@
|
||||
import sharp from "sharp";
|
||||
import {Logger} from "../logging/logger.js";
|
||||
import fs from "fs";
|
||||
|
||||
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
|
||||
* @param {string} templatePath - path to the template image
|
||||
* @param {string} avatarPath - path to the avatar
|
||||
* @param avatarPath {string}
|
||||
* @param name {string}
|
||||
* @returns {Promise<sharp.OutputInfo>} resulting image buffer
|
||||
*/
|
||||
static async getWelcomeCard(templatePath, avatarPath) {
|
||||
async getWelcomeCard(avatarPath, name) {
|
||||
try{
|
||||
|
||||
const template = await this.loadTemplate(templatePath);
|
||||
const template = this.loadTemplate();
|
||||
|
||||
const avatar = await this.handleAvatar(avatarPath);
|
||||
const messageBuffer = await this.getMessageBuffer(`Hello ${name}!`);
|
||||
|
||||
const result = await template
|
||||
.composite([{
|
||||
input: avatar,
|
||||
top: 215,
|
||||
left: 200,
|
||||
}])
|
||||
.composite([
|
||||
{ input: avatar, top: 215, left: 275 },
|
||||
{ input: messageBuffer, top: 400, left: 1300 }
|
||||
])
|
||||
.toFile("namecard.png")
|
||||
|
||||
console.log("✅ Welcome card created: welcome-card.png");
|
||||
return result;
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
await Logger.error("Unable to create name card", err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the template file into a sharp instance
|
||||
* @param {string} path - file path
|
||||
* @returns {sharp.Sharp} sharp image object
|
||||
*
|
||||
* @param avatarPath
|
||||
* @returns {Promise<Buffer<ArrayBufferLike>>}
|
||||
*/
|
||||
static async loadTemplate(path) {
|
||||
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) {
|
||||
async handleAvatar(avatarPath) {
|
||||
const avatarSize = 670;
|
||||
const borderSize = 8;
|
||||
const radius = avatarSize / 2;
|
||||
@@ -61,6 +63,30 @@ export class NameCardCreator {
|
||||
.png()
|
||||
.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({
|
||||
create: {
|
||||
width: totalSize,
|
||||
@@ -68,11 +94,53 @@ export class NameCardCreator {
|
||||
channels: 4,
|
||||
background: "#0000"
|
||||
}
|
||||
})
|
||||
.composite([
|
||||
{ input: avatarBuffer, top: borderSize, left: borderSize }
|
||||
])
|
||||
}).composite([
|
||||
{ input: roundedAvatar, top: 0, left: 0 },
|
||||
{ input: roundedBorder, top: borderSize, left: borderSize }
|
||||
])
|
||||
.png()
|
||||
.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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user