Skip to content

Commit c1a4a39

Browse files
committed
updating geometry
1 parent 18511ea commit c1a4a39

File tree

2 files changed

+49
-57
lines changed

2 files changed

+49
-57
lines changed

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ A free and open-source API to frame your GitHub avatar using creative themes. Pe
6161
https://github-avatar-frame-api.onrender.com/api/framed-avatar/{username}?theme={theme}&size={size}&canvas={canvas}&shape={shape}&radius={radius}
6262
</pre>
6363

64-
<h3 style="color:#009688;">Query Parameters:</h3>
65-
64+
<h3 style="color:#009688;" align=center>Query Parameters:</h3>
65+
<div align=right>
6666
<table style="width:100%; border-collapse:collapse; font-size:1.05em;">
6767
<thead style="background-color:#f5f5f5; text-align:center;">
6868
<tr>
@@ -83,6 +83,7 @@ https://github-avatar-frame-api.onrender.com/api/framed-avatar/{username}?theme=
8383
</tbody>
8484
</table>
8585

86+
</div>
8687
<br>
8788

8889
<h3 style="color:#ff4081;">Canvas, Shape & Radius Explained</h3>
@@ -94,7 +95,7 @@ https://github-avatar-frame-api.onrender.com/api/framed-avatar/{username}?theme=
9495
</ul>
9596

9697
<p>Combine all three to customize your avatar:</p>
97-
98+
<div align=center>
9899
<table style="width:100%; border-collapse:collapse; font-size:1.05em; text-align:center;">
99100
<thead style="background-color:#f5f5f5;">
100101
<tr>
@@ -111,14 +112,14 @@ https://github-avatar-frame-api.onrender.com/api/framed-avatar/{username}?theme=
111112
<td>circle</td>
112113
<td>-</td>
113114
<td><a href="https://github-avatar-frame-api.onrender.com/api/framed-avatar/octocat?canvas=light&shape=circle" target="_blank">URL</a></td>
114-
<td><img src="https://github-avatar-frame-api.onrender.com/api/framed-avatar/octocat?canvas=light&shape=circle&size=100&theme=base" width="80"></td>
115+
<td><img src="https://github-avatar-frame-api.onrender.com/api/framed-avatar/octocat?theme=classic&size=256&shape=circle&radius=15&canvas=light" width="80"></td>
115116
</tr>
116117
<tr>
117118
<td>dark</td>
118119
<td>circle</td>
119120
<td>-</td>
120121
<td><a href="https://github-avatar-frame-api.onrender.com/api/framed-avatar/octocat?canvas=dark&shape=circle" target="_blank">URL</a></td>
121-
<td><img src="https://github-avatar-frame-api.onrender.com/api/framed-avatar/octocat?canvas=dark&shape=circle&size=100&theme=base" width="80"></td>
122+
<td><img src="https://github-avatar-frame-api.onrender.com/api/framed-avatar/octocat?theme=classic&size=256&shape=circle&radius=15&canvas=dark" width="80"></td>
122123
</tr>
123124
<tr>
124125
<td>light</td>
@@ -132,11 +133,11 @@ https://github-avatar-frame-api.onrender.com/api/framed-avatar/{username}?theme=
132133
<td>rounded</td>
133134
<td>50</td>
134135
<td><a href="https://github-avatar-frame-api.onrender.com/api/framed-avatar/octocat?canvas=dark&shape=rounded&radius=50" target="_blank">URL</a></td>
135-
<td><img src="https://github-avatar-frame-api.onrender.com/api/framed-avatar/octocat?canvas=dark&shape=rounded&radius=50&size=100&theme=base" width="80"></td>
136+
<td><img src="https://github-avatar-frame-api.onrender.com/api/framed-avatar/octocat?theme=classic&size=256&shape=rounded&radius=20&canvas=dark" width="80"></td>
136137
</tr>
137138
</tbody>
138139
</table>
139-
140+
</div>
140141
<br>
141142

142143
<h3 style="color:#ff4081;">Live Examples by Theme</h3>
@@ -172,7 +173,7 @@ https://github-avatar-frame-api.onrender.com/api/framed-avatar/{username}?theme=
172173
<tr>
173174
<td>base</td>
174175
<td>light / rounded / 50</td>
175-
<td><img src="https://github-avatar-frame-api.onrender.com/api/framed-avatar/octocat?theme=base&size=100&canvas=light&shape=rounded&radius=50" width="80"></td>
176+
<td><img src="https://github-avatar-frame-api.onrender.com/api/framed-avatar/octocat?theme=neon&size=100&canvas=light&shape=rounded&radius=50" width="80"></td>
176177
<td>Base theme, light background, rounded corners 50px</td>
177178
</tr>
178179
<tr>
@@ -192,7 +193,7 @@ https://github-avatar-frame-api.onrender.com/api/framed-avatar/{username}?theme=
192193

193194
<br>
194195

195-
<h3 style="color:#3f51b5;">Embed in README</h3>
196+
<h3 style="color:#3f51b5;" align=right>Embed in README</h3>
196197

197198
<pre style="background-color:#f0f0f0; padding:10px; border-radius:10px;">
198199
![My Avatar](https://github-avatar-frame-api.onrender.com/api/framed-avatar/your-username?theme=flamingo&size=256&canvas=dark&shape=rounded&radius=20)

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)