@cognima/banners
Version:
Biblioteca avançada para geração de banners dinâmicos para diversas plataformas e aplicações
505 lines (430 loc) • 16.6 kB
JavaScript
"use strict";
/**
* Módulo de Banner de Post do Instagram
*
* Este módulo gera banners no estilo de posts do Instagram com suporte a
* imagem principal, informações de usuário e comentários.
*
* @author Cognima Team (melhorado)
* @version 2.0.0
*/
Object.defineProperty(exports, "__esModule", { value: true });
const pureimage = require("pureimage");
const path = require("path");
const {
loadImageWithAxios,
encodeToBuffer,
roundRect,
wrapText,
registerFontIfNeeded,
isValidHexColor,
DEFAULT_FONT_FAMILY,
applyTextShadow,
clearShadow,
createLinearGradient,
hexToRgba,
formatNumber
} = require("../utils");
/**
* @class InstagramPost
* @classdesc Gera um banner no estilo de post do Instagram.
* @example const postCard = new InstagramPost()
* .setUsername("usuario_instagram")
* .setUserAvatar("avatar.png")
* .setImage("post.jpg")
* .setCaption("Curtindo o dia na praia! #verao #ferias")
* .setLikes(1250)
* .addComment("amigo1", "Que lugar incrível!")
* .build();
*/
module.exports = class InstagramPost {
constructor(options) {
// Dados Principais
this.username = "usuario";
this.userAvatar = null;
this.verified = false;
this.location = null;
this.image = null;
this.caption = null;
this.likes = 0;
this.comments = [];
this.timestamp = "há 1 hora";
// Personalização Visual
this.font = { name: options?.font?.name ?? DEFAULT_FONT_FAMILY, path: options?.font?.path };
this.backgroundColor = "#FFFFFF";
this.textColor = "#262626";
this.secondaryTextColor = "#8e8e8e";
this.accentColor = "#0095F6";
this.showHeader = true;
this.showFooter = true;
// Configurações de Layout
this.cardWidth = 600;
this.imageHeight = 600;
this.cornerRadius = 0;
}
// --- Setters para Dados Principais ---
/**
* Define o nome de usuário
* @param {string} name - Nome do usuário
* @returns {InstagramPost} - Instância atual para encadeamento
*/
setUsername(name) {
if (!name || typeof name !== "string") throw new Error("O nome de usuário deve ser uma string não vazia.");
this.username = name;
return this;
}
/**
* Define o avatar do usuário
* @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do avatar
* @returns {InstagramPost} - Instância atual para encadeamento
*/
setUserAvatar(image) {
if (!image) throw new Error("A fonte da imagem do avatar não pode estar vazia.");
this.userAvatar = image;
return this;
}
/**
* Define se o usuário é verificado
* @param {boolean} isVerified - Se o usuário é verificado
* @returns {InstagramPost} - Instância atual para encadeamento
*/
setVerified(isVerified = true) {
this.verified = !!isVerified;
return this;
}
/**
* Define a localização do post
* @param {string} location - Nome da localização
* @returns {InstagramPost} - Instância atual para encadeamento
*/
setLocation(location) {
if (!location || typeof location !== "string") throw new Error("A localização deve ser uma string não vazia.");
this.location = location;
return this;
}
/**
* Define a imagem principal do post
* @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do post
* @returns {InstagramPost} - Instância atual para encadeamento
*/
setImage(image) {
if (!image) throw new Error("A fonte da imagem do post não pode estar vazia.");
this.image = image;
return this;
}
/**
* Define a legenda do post
* @param {string} text - Texto da legenda
* @returns {InstagramPost} - Instância atual para encadeamento
*/
setCaption(text) {
if (!text || typeof text !== "string") throw new Error("A legenda deve ser uma string não vazia.");
this.caption = text;
return this;
}
/**
* Define o número de curtidas
* @param {number} count - Número de curtidas
* @returns {InstagramPost} - Instância atual para encadeamento
*/
setLikes(count) {
if (typeof count !== "number" || count < 0) throw new Error("O número de curtidas deve ser um número não negativo.");
this.likes = count;
return this;
}
/**
* Adiciona um comentário ao post
* @param {string} username - Nome do usuário que comentou
* @param {string} text - Texto do comentário
* @returns {InstagramPost} - Instância atual para encadeamento
*/
addComment(username, text) {
if (!username || typeof username !== "string") throw new Error("O nome de usuário do comentário deve ser uma string não vazia.");
if (!text || typeof text !== "string") throw new Error("O texto do comentário deve ser uma string não vazia.");
this.comments.push({ username, text });
return this;
}
/**
* Define o timestamp do post
* @param {string} time - Texto do timestamp (ex: "há 2 horas")
* @returns {InstagramPost} - Instância atual para encadeamento
*/
setTimestamp(time) {
if (!time || typeof time !== "string") throw new Error("O timestamp deve ser uma string não vazia.");
this.timestamp = time;
return this;
}
// --- Setters para Personalização Visual ---
/**
* Define a cor de fundo
* @param {string} color - Cor hexadecimal
* @returns {InstagramPost} - Instância atual para encadeamento
*/
setBackgroundColor(color) {
if (!color || !isValidHexColor(color)) throw new Error("Cor de fundo inválida. Use o formato hexadecimal.");
this.backgroundColor = color;
return this;
}
/**
* Define a cor do texto principal
* @param {string} color - Cor hexadecimal
* @returns {InstagramPost} - Instância atual para encadeamento
*/
setTextColor(color) {
if (!color || !isValidHexColor(color)) throw new Error("Cor de texto inválida. Use o formato hexadecimal.");
this.textColor = color;
return this;
}
/**
* Define a cor do texto secundário
* @param {string} color - Cor hexadecimal
* @returns {InstagramPost} - Instância atual para encadeamento
*/
setSecondaryTextColor(color) {
if (!color || !isValidHexColor(color)) throw new Error("Cor de texto secundário inválida. Use o formato hexadecimal.");
this.secondaryTextColor = color;
return this;
}
/**
* Define a cor de destaque
* @param {string} color - Cor hexadecimal
* @returns {InstagramPost} - Instância atual para encadeamento
*/
setAccentColor(color) {
if (!color || !isValidHexColor(color)) throw new Error("Cor de destaque inválida. Use o formato hexadecimal.");
this.accentColor = color;
return this;
}
/**
* Ativa ou desativa o cabeçalho
* @param {boolean} show - Se o cabeçalho deve ser exibido
* @returns {InstagramPost} - Instância atual para encadeamento
*/
showHeader(show = true) {
this.showHeader = show;
return this;
}
/**
* Ativa ou desativa o rodapé
* @param {boolean} show - Se o rodapé deve ser exibido
* @returns {InstagramPost} - Instância atual para encadeamento
*/
showFooter(show = true) {
this.showFooter = show;
return this;
}
/**
* Define as dimensões do card
* @param {number} width - Largura do card em pixels
* @returns {InstagramPost} - Instância atual para encadeamento
*/
setCardWidth(width) {
if (typeof width !== "number" || width < 400 || width > 1080) {
throw new Error("A largura do card deve estar entre 400 e 1080 pixels.");
}
this.cardWidth = width;
return this;
}
/**
* Define a altura da imagem
* @param {number} height - Altura da imagem em pixels
* @returns {InstagramPost} - Instância atual para encadeamento
*/
setImageHeight(height) {
if (typeof height !== "number" || height < 400 || height > 1080) {
throw new Error("A altura da imagem deve estar entre 400 e 1080 pixels.");
}
this.imageHeight = height;
return this;
}
// --- Método de Construção ---
/**
* Constrói o banner e retorna um buffer de imagem
* @returns {Promise<Buffer>} - Buffer contendo a imagem do banner
*/
async build() {
if (!this.image) throw new Error("A imagem do post deve ser definida usando setImage().");
// --- Registro de Fonte ---
const registeredFontName = await registerFontIfNeeded(this.font);
// --- Configuração do Canvas ---
const cardWidth = this.cardWidth;
const headerHeight = this.showHeader ? 60 : 0;
const imageHeight = this.imageHeight;
// Calcula a altura do rodapé com base no conteúdo
let footerHeight = 0;
if (this.showFooter) {
footerHeight += 50; // Área de ícones e curtidas
if (this.caption) {
footerHeight += 60; // Espaço para legenda
}
footerHeight += this.comments.length * 40; // Espaço para comentários
footerHeight += 30; // Espaço para timestamp
}
const cardHeight = headerHeight + imageHeight + footerHeight;
const padding = 15;
const avatarSize = 32;
const canvas = pureimage.make(cardWidth, cardHeight);
const ctx = canvas.getContext("2d");
// --- Desenha Plano de Fundo ---
ctx.fillStyle = this.backgroundColor;
ctx.fillRect(0, 0, cardWidth, cardHeight);
// --- Desenha Cabeçalho (se ativado) ---
if (this.showHeader) {
// Desenha linha divisória
ctx.fillStyle = "#DBDBDB";
ctx.fillRect(0, headerHeight - 1, cardWidth, 1);
// Desenha avatar do usuário
if (this.userAvatar) {
try {
ctx.save();
ctx.beginPath();
ctx.arc(padding + avatarSize / 2, headerHeight / 2, avatarSize / 2, 0, Math.PI * 2);
ctx.closePath();
ctx.clip();
const avatarImg = await loadImageWithAxios(this.userAvatar);
ctx.drawImage(avatarImg, padding, headerHeight / 2 - avatarSize / 2, avatarSize, avatarSize);
ctx.restore();
} catch (e) {
console.error("Falha ao desenhar avatar do usuário:", e.message);
// Avatar de fallback
ctx.fillStyle = "#DBDBDB";
ctx.beginPath();
ctx.arc(padding + avatarSize / 2, headerHeight / 2, avatarSize / 2, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#8e8e8e";
ctx.font = `bold 16px ${registeredFontName}-Bold`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("?", padding + avatarSize / 2, headerHeight / 2);
}
}
// Desenha nome de usuário
ctx.fillStyle = this.textColor;
ctx.font = `bold 14px ${registeredFontName}-Bold`;
ctx.textAlign = "left";
ctx.textBaseline = "middle";
const usernameX = padding + avatarSize + 10;
ctx.fillText(this.username, usernameX, headerHeight / 2 - (this.location ? 7 : 0));
// Desenha ícone de verificado (se aplicável)
if (this.verified) {
const verifiedSize = 14;
const verifiedX = usernameX + ctx.measureText(this.username).width + 5;
ctx.fillStyle = this.accentColor;
ctx.beginPath();
ctx.arc(verifiedX + verifiedSize / 2, headerHeight / 2 - (this.location ? 7 : 0), verifiedSize / 2, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#FFFFFF";
ctx.font = `bold 10px ${registeredFontName}-Bold`;
ctx.textAlign = "center";
ctx.fillText("✓", verifiedX + verifiedSize / 2, headerHeight / 2 - (this.location ? 7 : 0));
}
// Desenha localização (se fornecida)
if (this.location) {
ctx.fillStyle = this.secondaryTextColor;
ctx.font = `regular 12px ${registeredFontName}-Regular`;
ctx.textAlign = "left";
ctx.fillText(this.location, usernameX, headerHeight / 2 + 10);
}
// Desenha ícone de opções
ctx.fillStyle = this.textColor;
ctx.font = `bold 18px ${registeredFontName}-Bold`;
ctx.textAlign = "center";
ctx.fillText("•••", cardWidth - padding - 10, headerHeight / 2);
}
// --- Desenha Imagem Principal ---
try {
const img = await loadImageWithAxios(this.image);
const aspect = img.width / img.height;
let drawWidth = cardWidth;
let drawHeight = drawWidth / aspect;
// Ajusta as dimensões para manter a proporção e preencher a altura desejada
if (drawHeight < imageHeight) {
drawHeight = imageHeight;
drawWidth = drawHeight * aspect;
}
const offsetX = (cardWidth - drawWidth) / 2;
const offsetY = headerHeight;
ctx.drawImage(img, offsetX, offsetY, drawWidth, imageHeight);
} catch (e) {
console.error("Falha ao desenhar imagem principal:", e.message);
// Imagem de fallback
ctx.fillStyle = "#DBDBDB";
ctx.fillRect(0, headerHeight, cardWidth, imageHeight);
ctx.fillStyle = "#8e8e8e";
ctx.font = `bold 24px ${registeredFontName}-Bold`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Imagem não disponível", cardWidth / 2, headerHeight + imageHeight / 2);
}
// --- Desenha Rodapé (se ativado) ---
if (this.showFooter) {
let currentY = headerHeight + imageHeight;
// Desenha linha divisória
ctx.fillStyle = "#DBDBDB";
ctx.fillRect(0, currentY, cardWidth, 1);
currentY += 1;
// Desenha ícones de ação
const iconSize = 24;
const iconSpacing = 15;
let iconX = padding;
// Ícone de curtir
ctx.fillStyle = this.textColor;
ctx.font = `bold ${iconSize}px Arial`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("♡", iconX + iconSize / 2, currentY + 25);
iconX += iconSize + iconSpacing;
// Ícone de comentar
ctx.fillText("💬", iconX + iconSize / 2, currentY + 25);
iconX += iconSize + iconSpacing;
// Ícone de compartilhar
ctx.fillText("➤", iconX + iconSize / 2, currentY + 25);
// Ícone de salvar (à direita)
ctx.fillText("⊕", cardWidth - padding - iconSize / 2, currentY + 25);
currentY += 50;
// Desenha contador de curtidas
ctx.fillStyle = this.textColor;
ctx.font = `bold 14px ${registeredFontName}-Bold`;
ctx.textAlign = "left";
ctx.textBaseline = "top";
ctx.fillText(`${formatNumber(this.likes)} curtidas`, padding, currentY);
currentY += 25;
// Desenha legenda (se fornecida)
if (this.caption) {
ctx.fillStyle = this.textColor;
ctx.font = `bold 14px ${registeredFontName}-Bold`;
ctx.textAlign = "left";
ctx.fillText(this.username, padding, currentY);
const usernameWidth = ctx.measureText(this.username).width;
ctx.font = `regular 14px ${registeredFontName}-Regular`;
const captionY = wrapText(ctx, this.caption, padding + usernameWidth + 5, currentY, cardWidth - padding * 2 - usernameWidth - 5, 18, registeredFontName);
currentY = captionY;
}
// Desenha comentários (se houver)
if (this.comments.length > 0) {
for (const comment of this.comments) {
ctx.fillStyle = this.textColor;
ctx.font = `bold 14px ${registeredFontName}-Bold`;
ctx.textAlign = "left";
ctx.fillText(comment.username, padding, currentY);
const commentUsernameWidth = ctx.measureText(comment.username).width;
ctx.font = `regular 14px ${registeredFontName}-Regular`;
const commentY = wrapText(ctx, comment.text, padding + commentUsernameWidth + 5, currentY, cardWidth - padding * 2 - commentUsernameWidth - 5, 18, registeredFontName);
currentY = commentY;
}
}
// Desenha timestamp
ctx.fillStyle = this.secondaryTextColor;
ctx.font = `regular 12px ${registeredFontName}-Regular`;
ctx.textAlign = "left";
ctx.fillText(this.timestamp, padding, currentY);
}
// --- Codifica e Retorna Buffer ---
try {
return await encodeToBuffer(canvas);
} catch (err) {
console.error("Falha ao codificar o card de Post do Instagram:", err);
throw new Error("Não foi possível gerar o buffer de imagem do card de Post do Instagram.");
}
}
};