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