From 9b5e03a4f2b25a76c0e0d37833f6f3fb919e2176 Mon Sep 17 00:00:00 2001 From: Shivram Date: Fri, 19 Sep 2025 22:27:59 +0530 Subject: [PATCH] Fix avatar; clean up API --- package-lock.json | 11 +++--- package.json | 11 +++--- src/server.ts | 88 +++++++++++++++++++++++++++++++++-------------- 3 files changed, 75 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index d080ecb..30250e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,15 +8,16 @@ "name": "github-avatar-frame-api", "version": "1.0.0", "dependencies": { - "axios": "^1.6.8", - "express": "^4.18.2", - "sharp": "^0.33.0" + "axios": "^1.12.2", + "express": "^4.21.2", + "sharp": "^0.33.5" }, "devDependencies": { - "@types/express": "^4.17.21", + "@types/express": "^4.17.23", "@types/node": "^20.11.30", + "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", - "typescript": "^5.4.3" + "typescript": "^5.9.2" } }, "node_modules/@cspotcode/source-map-support": { diff --git a/package.json b/package.json index 559d403..9aa6f8e 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,15 @@ "start": "node dist/server.js" }, "dependencies": { - "axios": "^1.6.8", - "express": "^4.18.2", - "sharp": "^0.33.0" + "axios": "^1.12.2", + "express": "^4.21.2", + "sharp": "^0.33.5" }, "devDependencies": { - "@types/express": "^4.17.21", + "@types/express": "^4.17.23", "@types/node": "^20.11.30", + "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", - "typescript": "^5.4.3" + "typescript": "^5.9.2" } } diff --git a/src/server.ts b/src/server.ts index 998124b..8acdc3c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -17,31 +17,63 @@ app.get("/api/framed-avatar/:username", async (req: Request, res: Response) => { const theme = (req.query.theme as string) || "classic"; const size = Number(req.query.size ?? 256); - // Fetch GitHub avatar + if (isNaN(size) || size <= 0 || size > 1024) { + return res.status(400).json({ error: "Invalid size parameter" }); + } + + // 1. Fetch GitHub avatar const avatarUrl = `https://github.com/${username}.png?size=${size}`; const avatarResponse = await axios.get(avatarUrl, { responseType: "arraybuffer" }); const avatarBuffer = Buffer.from(avatarResponse.data); - // Locate theme frame - const themePath = path.join(__dirname, "..", "public", "frames", theme, "frame.png"); - if (!fs.existsSync(themePath)) { - return res.status(404).json({ error: `Theme '${theme}' not found` }); + // 2. Load and validate frame + const framePath = path.join(__dirname, "..", "public", "frames", theme, "frame.png"); + if (!fs.existsSync(framePath)) { + return res.status(404).json({ error: `Theme '${theme}' not found.` }); } - const frameBuffer = fs.readFileSync(themePath); + const frameBuffer = fs.readFileSync(framePath); + + // 3. Resize avatar to match requested size + const avatarResized = await sharp(avatarBuffer) + .resize(size, size) + .png() + .toBuffer(); + + // 4. Pad frame to square (if needed) and resize + const frameMetadata = await sharp(frameBuffer).metadata(); + const maxSide = Math.max(frameMetadata.width!, frameMetadata.height!); - // Resize and overlay - const avatarResized = await sharp(avatarBuffer).resize(size, size).png().toBuffer(); - const frameResized = await sharp(frameBuffer).resize(size, size).png().toBuffer(); + const paddedFrame = await sharp(frameBuffer) + .resize({ + width: maxSide, + height: maxSide, + fit: "contain", + background: { r: 0, g: 0, b: 0, alpha: 0 }, // Transparent background + }) + .resize(size, size) + .png() + .toBuffer(); - const finalImage = await sharp(avatarResized) - .composite([{ input: frameResized, gravity: "center" }]) + // 5. Compose avatar + frame on transparent canvas + const finalImage = await sharp({ + create: { + width: size, + height: size, + channels: 4, + background: { r: 0, g: 0, b: 0, alpha: 0 }, + }, + }) + .composite([ + { input: avatarResized, gravity: "center" }, + { input: paddedFrame, gravity: "center" }, + ]) .png() .toBuffer(); res.set("Content-Type", "image/png"); res.send(finalImage); } catch (error) { - console.error(error); + console.error("Error creating framed avatar:", error); res.status(500).json({ error: "Something went wrong." }); } }); @@ -51,23 +83,29 @@ app.get("/api/framed-avatar/:username", async (req: Request, res: Response) => { * Lists all available themes + metadata */ app.get("/api/themes", (req: Request, res: Response) => { - const framesDir = path.join(__dirname, "..", "public", "frames"); - const themes = fs.readdirSync(framesDir).filter(folder => - fs.existsSync(path.join(framesDir, folder, "frame.png")) - ); + try { + const framesDir = path.join(__dirname, "..", "public", "frames"); + const themes = fs.readdirSync(framesDir).filter(folder => + fs.existsSync(path.join(framesDir, folder, "frame.png")) + ); - const result = themes.map(theme => { - const metadataPath = path.join(framesDir, theme, "metadata.json"); - let metadata = {}; - if (fs.existsSync(metadataPath)) { - metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8")); - } - return { theme, ...metadata }; - }); + const result = themes.map(theme => { + const metadataPath = path.join(framesDir, theme, "metadata.json"); + let metadata = {}; + if (fs.existsSync(metadataPath)) { + metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8")); + } + return { theme, ...metadata }; + }); - res.json(result); + res.json(result); + } catch (error) { + console.error("Error listing themes:", error); + res.status(500).json({ error: "Failed to load themes." }); + } }); +// Start server app.listen(PORT, () => { console.log(`🚀 Server running at http://localhost:${PORT}`); });