diff --git a/README.md b/README.md
index f758ce3..ff4e9a8 100644
--- a/README.md
+++ b/README.md
@@ -80,6 +80,13 @@ https://github-avatar-frame-api.onrender.com/api/framed-avatar/{username}?theme=
| canvas | string | light | Background color of avatar canvas: light / dark | dark |
| shape | string | circle | Avatar shape: circle or rounded | rounded |
| radius | integer | 25 | Corner radius for rounded shape in px | 50 |
+| text | string | optional | Custom text to display on avatar | Hello World |
+| textColor | string | #ffffff | Color of text in HEX format | #ff0000 |
+| textSize | integer | 20 | Size of text in pixels (8-100) | 24 |
+| textPosition | string | bottom | Position of text: top / bottom / center | top |
+| emojis | string | optional | Comma-separated list of emojis | 🚀,💻,🔥 |
+| emojiSize | integer | 40 | Size of emojis in pixels (16-120) | 30 |
+| emojiPosition | string | top | Position of emojis: top / bottom / corners | corners |
@@ -95,6 +102,25 @@ https://github-avatar-frame-api.onrender.com/api/framed-avatar/{username}?theme=
Combine all three to customize your avatar:
+
+Text & Emoji Overlays
+
+Add personalized text and emojis to make your avatar more expressive:
+
+
+- text: Add custom text like your name, role, or tagline.
+- textColor: Set text color using HEX format (e.g., #ff0000 for red).
+- textSize: Control text size from 8-100 pixels.
+- textPosition: Place text at top, bottom, or center of the avatar.
+- emojis: Add multiple emojis separated by commas (e.g., 🚀,💻,🔥).
+- emojiSize: Control emoji size from 16-120 pixels.
+- emojiPosition: Place emojis at top, bottom, or in corners.
+
+
+Example with text and emojis:
+
+https://github-avatar-frame-api.onrender.com/api/framed-avatar/octocat?theme=base&text=GitHub%20User&textColor=%23ffffff&textSize=20&textPosition=bottom&emojis=%F0%9F%9A%80%2C%F0%9F%92%BB%2C%F0%9F%94%A5&emojiSize=30&emojiPosition=top
+
diff --git a/api/server.ts b/api/server.ts
index bf3ec90..a25483a 100644
--- a/api/server.ts
+++ b/api/server.ts
@@ -22,6 +22,100 @@ const ASSET_BASE_PATH = path.join(__dirname, "..");
//serve static files
app.use(express.static(path.join(ASSET_BASE_PATH,"public")));
+
+// Helper function to create text overlay
+async function createTextOverlay(text: string, textColor: string, textSize: number, textPosition: string, canvasSize: number): Promise {
+ if (!text || text.trim() === "") return null;
+
+ // Create SVG for text overlay
+ const svg = `
+
+ `;
+
+ return Buffer.from(svg);
+}
+
+// Helper function to create emoji overlay
+async function createEmojiOverlay(emojis: string, emojiSize: number, emojiPosition: string, canvasSize: number): Promise {
+ if (!emojis || emojis.trim() === "") return null;
+
+ const emojiList = emojis.split(',').map(e => e.trim()).filter(e => e.length > 0);
+ if (emojiList.length === 0) return null;
+
+ let emojiElements = '';
+ const spacing = emojiSize + 10;
+
+ if (emojiPosition === 'corners' && emojiList.length >= 4) {
+ // Place emojis in corners
+ const positions = [
+ { x: emojiSize/2 + 5, y: emojiSize/2 + 5 }, // top-left
+ { x: canvasSize - emojiSize/2 - 5, y: emojiSize/2 + 5 }, // top-right
+ { x: emojiSize/2 + 5, y: canvasSize - emojiSize/2 - 5 }, // bottom-left
+ { x: canvasSize - emojiSize/2 - 5, y: canvasSize - emojiSize/2 - 5 } // bottom-right
+ ];
+
+ emojiList.slice(0, 4).forEach((emoji, index) => {
+ emojiElements += `
+
+ ${emoji}
+
+ `;
+ });
+ } else {
+ // Place emojis in a row at top or bottom
+ const y = emojiPosition === 'top' ? emojiSize + 5 : canvasSize - 5;
+ const totalWidth = emojiList.length * spacing;
+ const startX = (canvasSize - totalWidth) / 2 + emojiSize / 2;
+
+ emojiList.forEach((emoji, index) => {
+ const x = startX + index * spacing;
+ emojiElements += `
+
+ ${emoji}
+
+ `;
+ });
+ }
+
+ const svg = `
+
+ `;
+
+ return Buffer.from(svg);
+}
/**
* GET /api/framed-avatar/:username
* Example: /api/framed-avatar/octocat?theme=base&size=256&accentColor=%23ff6b6b
@@ -33,6 +127,13 @@ app.use(express.static(path.join(ASSET_BASE_PATH,"public")));
* - radius: Corner radius for rounded/rect shapes
* - canvas: "light", "dark", or "transparent" (default: "light")
* - accentColor: Custom color for frame tinting (hex format, e.g., "#ff6b6b")
+ * - text: Custom text to display (e.g., "Darshan Parmar")
+ * - textColor: Color of text in HEX or keyword (default: #ffffff)
+ * - textSize: Size of text in pixels (default: 20)
+ * - textPosition: Position of text — top | bottom | center (default: bottom)
+ * - emojis: Comma-separated list of emojis (e.g., 🚀,💻,🔥)
+ * - emojiSize: Size of emojis in pixels (default: 40)
+ * - emojiPosition: Position of emojis — top | bottom | corners (default: top)
*/
app.get("/api/framed-avatar/:username", async (req: Request, res: Response) => {
try {
@@ -43,6 +144,17 @@ app.get("/api/framed-avatar/:username", async (req: Request, res: Response) => {
const radiusStr = req.query.radius as string | undefined;
const canvasParam = (req.query.canvas as string)?.toLowerCase() || "light"; // "dark" or "light"
const accentColor = req.query.accentColor as string | undefined;
+
+ // Text overlay parameters
+ const text = req.query.text as string | undefined;
+ const textColor = (req.query.textColor as string) || "#ffffff";
+ const textSizeStr = (req.query.textSize as string) || "20";
+ const textPosition = ((req.query.textPosition as string) || "bottom").toLowerCase();
+
+ // Emoji overlay parameters
+ const emojis = req.query.emojis as string | undefined;
+ const emojiSizeStr = (req.query.emojiSize as string) || "40";
+ const emojiPosition = ((req.query.emojiPosition as string) || "top").toLowerCase();
// Validate username
if (!username || typeof username !== "string" || username.trim() === "") {
@@ -76,6 +188,24 @@ app.get("/api/framed-avatar/:username", async (req: Request, res: Response) => {
});
}
+ // Validate text parameters
+ const textSize = Math.max(8, Math.min(parseInt(textSizeStr, 10), 100));
+ if (!["top", "bottom", "center"].includes(textPosition)) {
+ return res.status(400).json({
+ error: "Bad Request",
+ message: "textPosition must be 'top', 'bottom', or 'center'.",
+ });
+ }
+
+ // Validate emoji parameters
+ const emojiSize = Math.max(16, Math.min(parseInt(emojiSizeStr, 10), 120));
+ if (!["top", "bottom", "corners"].includes(emojiPosition)) {
+ return res.status(400).json({
+ error: "Bad Request",
+ message: "emojiPosition must be 'top', 'bottom', or 'corners'.",
+ });
+ }
+
// determine corner radius
let cornerRadius: number;
if (shape === "circle") cornerRadius = Math.floor(size / 2);
@@ -194,6 +324,26 @@ app.get("/api/framed-avatar/:username", async (req: Request, res: Response) => {
.png()
.toBuffer();
+ // Create text and emoji overlays
+ const textOverlay = text ? await createTextOverlay(text, textColor, textSize, textPosition, size) : null;
+ const emojiOverlay = emojis ? await createEmojiOverlay(emojis, emojiSize, emojiPosition, size) : null;
+
+ // Build composite layers
+ const compositeLayers = [
+ { input: avatarMasked, gravity: "center" },
+ { input: paddedFrame, gravity: "center" },
+ ];
+
+ // Add text overlay if provided
+ if (textOverlay) {
+ compositeLayers.push({ input: textOverlay, gravity: "center" });
+ }
+
+ // Add emoji overlay if provided
+ if (emojiOverlay) {
+ compositeLayers.push({ input: emojiOverlay, gravity: "center" });
+ }
+
// Compose final image on custom canvas color
const finalImage = await sharp({
create: {
@@ -203,10 +353,7 @@ app.get("/api/framed-avatar/:username", async (req: Request, res: Response) => {
background: canvasColor,
},
})
- .composite([
- { input: avatarMasked, gravity: "center" },
- { input: paddedFrame, gravity: "center" },
- ])
+ .composite(compositeLayers)
.png()
.toBuffer();
diff --git a/client/src/App.jsx b/client/src/App.jsx
index e1b23be..2296c68 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -199,6 +199,17 @@ function App() {
const [shape, setShape] = useState("circle");
const [radius, setRadius] = useState(38);
const [frameStyle, setFrameStyle] = useState("default");
+
+ // Text overlay parameters
+ const [text, setText] = useState("");
+ const [textColor, setTextColor] = useState("#ffffff");
+ const [textSize, setTextSize] = useState(20);
+ const [textPosition, setTextPosition] = useState("bottom");
+
+ // Emoji overlay parameters
+ const [emojis, setEmojis] = useState("");
+ const [emojiSize, setEmojiSize] = useState(40);
+ const [emojiPosition, setEmojiPosition] = useState("top");
const [loading, setLoading] = useState(false);
const [themesLoading, setThemesLoading] = useState(true);
@@ -319,6 +330,16 @@ function App() {
apiUrl += `&accentColor=${encodeURIComponent(customAccentColor)}`;
}
+ // Add text parameters if provided
+ if (text.trim()) {
+ apiUrl += `&text=${encodeURIComponent(text)}&textColor=${encodeURIComponent(textColor)}&textSize=${textSize}&textPosition=${textPosition}`;
+ }
+
+ // Add emoji parameters if provided
+ if (emojis.trim()) {
+ apiUrl += `&emojis=${encodeURIComponent(emojis)}&emojiSize=${emojiSize}&emojiPosition=${emojiPosition}`;
+ }
+
try {
// Use document.execCommand('copy') for better compatibility in iframe environments
const tempInput = document.createElement("textarea");
@@ -355,6 +376,16 @@ function App() {
if (customAccentColor) {
url += `&accentColor=${encodeURIComponent(customAccentColor)}`;
}
+
+ // Add text parameters if provided
+ if (text.trim()) {
+ url += `&text=${encodeURIComponent(text)}&textColor=${encodeURIComponent(textColor)}&textSize=${textSize}&textPosition=${textPosition}`;
+ }
+
+ // Add emoji parameters if provided
+ if (emojis.trim()) {
+ url += `&emojis=${encodeURIComponent(emojis)}&emojiSize=${emojiSize}&emojiPosition=${emojiPosition}`;
+ }
// Create AbortController for timeout
const controller = new AbortController();
@@ -1158,6 +1189,182 @@ function App() {
)}
+ {/* Text Overlay Controls */}
+
+
+
+ setText(e.target.value)}
+ style={{
+ flex: 1,
+ padding: "10px 12px",
+ borderRadius: "8px",
+ border: `2px solid ${colors.border}`,
+ background: colors.bgInput,
+ color: colors.textPrimary,
+ fontSize: "14px",
+ outline: "none",
+ transition: "border-color 0.2s",
+ }}
+ />
+ setTextColor(e.target.value)}
+ style={{
+ width: "50px",
+ height: "40px",
+ borderRadius: "8px",
+ border: `2px solid ${colors.border}`,
+ cursor: "pointer",
+ }}
+ />
+
+
+
+
+ setTextSize(parseInt(e.target.value))}
+ className="range-slider"
+ style={{
+ width: "100%",
+ height: "6px",
+ borderRadius: "3px",
+ background: isDark ? "#374151" : "linear-gradient(to right, #a78bfa, #c4b5fd)",
+ outline: "none",
+ cursor: "pointer",
+ WebkitAppearance: "none",
+ }}
+ />
+
+
+
+
+ setTextPosition("top")}
+ isSelected={textPosition === "top"}
+ isDark={isDark}>
+ Top
+
+ setTextPosition("center")}
+ isSelected={textPosition === "center"}
+ isDark={isDark}>
+ Center
+
+ setTextPosition("bottom")}
+ isSelected={textPosition === "bottom"}
+ isDark={isDark}>
+ Bottom
+
+
+
+
+
+
+ {/* Emoji Overlay Controls */}
+
+
+
+ setEmojis(e.target.value)}
+ style={{
+ flex: 1,
+ padding: "10px 12px",
+ borderRadius: "8px",
+ border: `2px solid ${colors.border}`,
+ background: colors.bgInput,
+ color: colors.textPrimary,
+ fontSize: "14px",
+ outline: "none",
+ transition: "border-color 0.2s",
+ }}
+ />
+
+
+
+
+ setEmojiSize(parseInt(e.target.value))}
+ className="range-slider"
+ style={{
+ width: "100%",
+ height: "6px",
+ borderRadius: "3px",
+ background: isDark ? "#374151" : "linear-gradient(to right, #a78bfa, #c4b5fd)",
+ outline: "none",
+ cursor: "pointer",
+ WebkitAppearance: "none",
+ }}
+ />
+
+
+
+
+ setEmojiPosition("top")}
+ isSelected={emojiPosition === "top"}
+ isDark={isDark}>
+ Top
+
+ setEmojiPosition("bottom")}
+ isSelected={emojiPosition === "bottom"}
+ isDark={isDark}>
+ Bottom
+
+ setEmojiPosition("corners")}
+ isSelected={emojiPosition === "corners"}
+ isDark={isDark}>
+ Corners
+
+
+
+
+
+
{/* Generate Button */}