Skip to content

Commit 39aed86

Browse files
committed
updating geometry
1 parent c3ae0db commit 39aed86

File tree

1 file changed

+39
-48
lines changed

1 file changed

+39
-48
lines changed

api/server.ts

Lines changed: 39 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -20,76 +20,70 @@ app.get("/api/framed-avatar/:username", async (req: Request, res: Response) => {
2020
try {
2121
const username = req.params.username;
2222
const theme = (req.query.theme as string) || "base";
23-
2423
const sizeStr = (req.query.size as string) ?? "256";
24+
const shape = ((req.query.shape as string) || "circle").toLowerCase();
25+
const radiusStr = req.query.radius as string | undefined;
26+
const canvasParam = (req.query.canvas as string)?.toLowerCase() || "light"; // "dark" or "light"
2527

2628
if (!/^\d+$/.test(sizeStr)) {
27-
return res.status(400).json({
28-
error: "Bad Request",
29-
message: "The 'size' parameter must be a valid integer.",
30-
});
29+
return res.status(400).json({ error: "Bad Request", message: "The 'size' parameter must be a valid integer." });
3130
}
3231

3332
const size = Math.max(64, Math.min(parseInt(sizeStr, 10), 1024));
3433

35-
console.log(`Fetching avatar for username=${username}, theme=${theme}, size=${size}`);
34+
// determine corner radius
35+
let cornerRadius: number;
36+
if (shape === "circle") cornerRadius = Math.floor(size / 2);
37+
else if (radiusStr && /^\d+$/.test(radiusStr)) cornerRadius = Math.max(0, Math.min(parseInt(radiusStr, 10), Math.floor(size / 2)));
38+
else cornerRadius = Math.floor(size * 0.1);
39+
40+
// determine canvas color
41+
let canvasColor: { r: number; g: number; b: number; alpha: number };
42+
if (canvasParam === "dark") canvasColor = { r: 34, g: 34, b: 34, alpha: 1 }; // dark gray
43+
else canvasColor = { r: 240, g: 240, b: 240, alpha: 1 }; // light gray default
3644

37-
// 1. Fetch GitHub avatar
45+
// Fetch avatar
3846
const avatarUrl = `https://github.com/${username}.png?size=${size}`;
3947
const avatarResponse = await axios.get(avatarUrl, { responseType: "arraybuffer" });
40-
41-
// CRITICAL FIX: Check the Content-Type header. If GitHub returns an HTML error page
42-
// (which causes the corrupt header error), we reject it.
43-
const contentType = avatarResponse.headers['content-type'] || '';
44-
if (!contentType.startsWith('image/')) {
45-
console.error(`GitHub returned unexpected content type: ${contentType} for user ${username}.`);
46-
return res.status(404).json({ error: `GitHub user '${username}' avatar not found or returned invalid image data.` });
47-
}
48-
48+
const contentType = avatarResponse.headers["content-type"] || "";
49+
if (!contentType.startsWith("image/")) return res.status(404).json({ error: `GitHub user '${username}' avatar not found.` });
4950
const avatarBuffer = Buffer.from(avatarResponse.data);
5051

51-
// 2. Load and validate frame
52-
// FIX: Use ASSET_BASE_PATH for reliable path resolution (instead of process.cwd())
53-
const framePath = path.join(ASSET_BASE_PATH, "public", "frames", theme, "frame.png");
54-
if (!fs.existsSync(framePath)) {
55-
console.error(`Frame not found at: ${framePath}`);
56-
return res.status(404).json({ error: `Theme '${theme}' not found.` });
57-
}
52+
// Load frame
53+
const framePath = path.join(ASSET_BASE_PATH, "public", "frames", theme, "frame.png");
54+
if (!fs.existsSync(framePath)) return res.status(404).json({ error: `Theme '${theme}' not found.` });
5855
const frameBuffer = fs.readFileSync(framePath);
5956

60-
// 3. Resize avatar to match requested size
61-
const avatarResized = await sharp(avatarBuffer)
57+
// Resize avatar
58+
const avatarResized = await sharp(avatarBuffer).resize(size, size).png().toBuffer();
59+
60+
// Resize frame
61+
const frameMetadata = await sharp(frameBuffer).metadata();
62+
const maxSide = Math.max(frameMetadata.width || size, frameMetadata.height || size);
63+
const paddedFrame = await sharp(frameBuffer)
64+
.resize({ width: maxSide, height: maxSide, fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
6265
.resize(size, size)
6366
.png()
6467
.toBuffer();
6568

66-
// 4. Pad frame to square and resize
67-
const frameMetadata = await sharp(frameBuffer).metadata();
68-
const maxSide = Math.max(frameMetadata.width!, frameMetadata.height!);
69+
// Create mask for rounded corners
70+
const maskSvg = `<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
71+
<rect x="0" y="0" width="${size}" height="${size}" rx="${cornerRadius}" ry="${cornerRadius}" fill="#fff"/>
72+
</svg>`;
73+
const maskBuffer = Buffer.from(maskSvg);
6974

70-
const paddedFrame = await sharp(frameBuffer)
71-
.resize({
72-
width: maxSide,
73-
height: maxSide,
74-
fit: "contain",
75-
background: { r: 0, g: 0, b: 0, alpha: 0 }, // Transparent background
76-
})
77-
.resize(size, size)
75+
const avatarMasked = await sharp(avatarResized)
76+
.composite([{ input: maskBuffer, blend: "dest-in" }])
7877
.png()
7978
.toBuffer();
8079

81-
// 5. Compose avatar + frame on transparent canvas
80+
// Compose final image on custom canvas color
8281
const finalImage = await sharp({
83-
create: {
84-
width: size,
85-
height: size,
86-
channels: 4,
87-
background: { r: 0, g: 0, b: 0, alpha: 0 },
88-
},
82+
create: { width: size, height: size, channels: 4, background: canvasColor }
8983
})
9084
.composite([
91-
{ input: avatarResized, gravity: "center" },
92-
{ input: paddedFrame, gravity: "center" },
85+
{ input: avatarMasked, gravity: "center" },
86+
{ input: paddedFrame, gravity: "center" }
9387
])
9488
.png()
9589
.toBuffer();
@@ -98,16 +92,13 @@ app.get("/api/framed-avatar/:username", async (req: Request, res: Response) => {
9892
res.send(finalImage);
9993
} catch (error) {
10094
console.error("Error creating framed avatar:", error);
101-
// Add a check for specific errors, like user not found from GitHub
10295
if (axios.isAxiosError(error) && error.response?.status === 404) {
10396
return res.status(404).json({ error: `GitHub user '${req.params.username}' not found.` });
10497
}
105-
// Return a clearer 500 message for generic crashes
10698
res.status(500).json({ error: "Internal Server Error during image processing." });
10799
}
108100
});
109101

110-
111102
/**
112103
* GET /api/themes
113104
* Lists all available themes + metadata

0 commit comments

Comments
 (0)