@@ -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