UNPKG

@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
"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."); } } };