const express = require("express"); const cors = require("cors"); const { createCanvas, registerFont } = require("canvas"); const path = require("path"); const fs = require("fs"); const app = express(); app.use(cors()); app.use(express.json()); app.use(express.static("public")); // Create fonts directory if it doesn't exist const fontsDir = path.join(__dirname, "fonts"); if (!fs.existsSync(fontsDir)) { fs.mkdirSync(fontsDir); } // Store available fonts and their variations const availableFonts = new Map(); function initializeFonts() { if (!fs.existsSync(fontsDir)) { console.log( "Created fonts directory. Please add font files (.ttf or .otf) to the fonts folder." ); return; } const fontFiles = fs .readdirSync(fontsDir) .filter( (file) => file.toLowerCase().endsWith(".ttf") || file.toLowerCase().endsWith(".otf") ); fontFiles.forEach((file) => { const fontPath = path.join(fontsDir, file); const fontName = file.replace(/\.(ttf|otf)$/i, ""); // Parse font variations (Regular, Bold, Italic, etc.) let weight = "normal"; let style = "normal"; const lowerFontName = fontName.toLowerCase(); if (lowerFontName.includes("bold")) weight = "bold"; if (lowerFontName.includes("light")) weight = "light"; if (lowerFontName.includes("medium")) weight = "medium"; if (lowerFontName.includes("italic")) style = "italic"; // Register font with canvas registerFont(fontPath, { family: fontName.split("-")[0], // Get base font name weight, style, }); // Store font info const familyName = fontName.split("-")[0]; if (!availableFonts.has(familyName)) { availableFonts.set(familyName, []); } availableFonts.get(familyName).push({ fullName: fontName, weight, style, }); }); console.log("Available font families:", Array.from(availableFonts.keys())); } // Initialize fonts initializeFonts(); // Store requests history let requestsHistory = []; function generateQuoteImage(ctx, canvas, data) { const { text, author, bgColor, barColor, textColor, authorColor, quoteFontFamily, quoteFontWeight, quoteFontStyle, authorFontFamily, authorFontWeight, authorFontStyle, barWidth = 4, } = data; // Constants for layout const margin = 80; const quoteMarkSize = 120; const padding = 30; const lineHeight = 50; // Clear canvas ctx.fillStyle = bgColor; ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw bars ctx.fillStyle = barColor; ctx.fillRect(margin, margin, barWidth, canvas.height - margin * 2); ctx.fillRect( canvas.width - margin - barWidth, margin, barWidth, canvas.height - margin * 2 ); // Set up quote font ctx.fillStyle = barColor; const quoteMarkFont = []; if (quoteFontStyle === "italic") quoteMarkFont.push("italic"); quoteMarkFont.push(quoteFontWeight); quoteMarkFont.push(`${quoteMarkSize}px`); quoteMarkFont.push(`"${quoteFontFamily}"`); ctx.font = quoteMarkFont.join(" "); ctx.textBaseline = "top"; // Calculate quote mark metrics const quoteMarkWidth = ctx.measureText('"').width; const textStartX = margin + barWidth + padding + quoteMarkWidth + padding; const textEndX = canvas.width - margin - barWidth - padding * 2; const maxWidth = textEndX - textStartX; // Set up quote text font ctx.fillStyle = textColor; const quoteFont = []; if (quoteFontStyle === "italic") quoteFont.push("italic"); quoteFont.push(quoteFontWeight); quoteFont.push("36px"); quoteFont.push(`"${quoteFontFamily}"`); ctx.font = quoteFont.join(" "); // Word wrap text const words = text.split(" "); const lines = []; let currentLine = ""; for (let word of words) { const testLine = currentLine + (currentLine ? " " : "") + word; const metrics = ctx.measureText(testLine); if (metrics.width > maxWidth) { if (currentLine) { lines.push(currentLine); currentLine = word; } else { currentLine = word; } } else { currentLine = testLine; } } if (currentLine) { lines.push(currentLine); } // Calculate total height of text block const totalTextHeight = lines.length * lineHeight; const authorHeight = 60; // Space reserved for author const availableHeight = canvas.height - margin * 2; // Calculate starting Y position to center text block let startY = margin + (availableHeight - (totalTextHeight + authorHeight)) / 2; // Draw quote mark at the same vertical position as first line ctx.fillStyle = barColor; ctx.font = quoteMarkFont.join(" "); ctx.fillText('"', margin + barWidth + padding, startY); // Draw quote lines ctx.fillStyle = textColor; ctx.font = quoteFont.join(" "); lines.forEach((line, index) => { ctx.fillText(line.trim(), textStartX, startY + index * lineHeight); }); // Draw author below the quote ctx.fillStyle = authorColor; const authorFont = []; if (authorFontStyle === "italic") authorFont.push("italic"); authorFont.push(authorFontWeight); authorFont.push("28px"); authorFont.push(`"${authorFontFamily}"`); ctx.font = authorFont.join(" "); // Ensure author doesn't overflow let authorText = `- ${author}`; let authorMetrics = ctx.measureText(authorText); if (authorMetrics.width > maxWidth) { const ellipsis = "..."; while (authorMetrics.width > maxWidth && author.length > 0) { author = author.slice(0, -1); authorText = `- ${author}${ellipsis}`; authorMetrics = ctx.measureText(authorText); } } // Position author text below quote with spacing const authorY = startY + totalTextHeight + 40; ctx.fillText(authorText, textStartX, authorY); } // API Endpoints app.get("/api/fonts", (req, res) => { const fontDetails = Array.from(availableFonts.entries()).map( ([family, variations]) => ({ family, variations: variations.map((v) => ({ weight: v.weight, style: v.style, fullName: v.fullName, })), }) ); res.json(fontDetails); }); app.post("/api/generate-quote", (req, res) => { try { const data = req.body; // Validate fonts exist if (!availableFonts.has(data.quoteFontFamily)) { throw new Error("Selected quote font is not available"); } if (!availableFonts.has(data.authorFontFamily)) { throw new Error("Selected author font is not available"); } // Store request requestsHistory.unshift({ timestamp: new Date(), request: data, }); // Keep only last 10 requests requestsHistory = requestsHistory.slice(0, 10); // Create canvas const canvas = createCanvas(1200, 675); // 16:9 ratio const ctx = canvas.getContext("2d"); // Generate quote image generateQuoteImage(ctx, canvas, data); // Send response res.json({ success: true, imageUrl: canvas.toDataURL(), timestamp: new Date().toISOString(), }); } catch (error) { console.error("Error generating quote:", error); res.status(500).json({ success: false, error: error.message, }); } }); app.get("/api/requests-history", (req, res) => { res.json(requestsHistory); }); // Start server const PORT = process.env.PORT || 7860; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); console.log(`View the application at http://localhost:${PORT}`); });