Spaces:
Running
Running
| 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}`); | |
| }); | |