Last active
August 4, 2025 17:35
-
-
Save remainstheday/d94475b9a73a5d69e6274a7d638a0cd7 to your computer and use it in GitHub Desktop.
Sharp Image Endpoint
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| router.post("/new-doubles/download-social-image", async (req, res) => { | |
| process.stdout.write(`[AWS] Social image generation started ${{ | |
| timestamp: new Date().toISOString(), | |
| requestBody: req.body, | |
| environment: process.env.NODE_ENV || 'development' | |
| }}`); | |
| try { | |
| let doublesData, athleteA, athleteB, division; | |
| ({doublesData, athleteA, athleteB, division} = req.body); | |
| const templatePath = path.join(__dirname, '..', 'templates', 'doubles-team-story.svg'); | |
| const fontPath = path.join(__dirname, '..', 'fonts', 'Inter-Bold.otf'); | |
| process.stdout.write(`[AWS] File paths resolved ${{ | |
| templatePath, | |
| fontPath, | |
| templateExists: fs.existsSync(templatePath), | |
| fontExists: fs.existsSync(fontPath) | |
| }}`); | |
| // Debug font availability in container with comprehensive checks | |
| try { | |
| const { execSync } = require('child_process'); | |
| const fontList = execSync('fc-list | grep -i inter || echo "No Inter fonts found"', { encoding: 'utf8' }); | |
| const systemFonts = execSync('fc-list | head -5', { encoding: 'utf8' }); | |
| process.stdout.write(`[AWS] Font system comprehensive debug ${{ | |
| environment: process.env.NODE_ENV || 'development', | |
| fontconfigPath: process.env.FONTCONFIG_PATH || 'default', | |
| fontconfigFile: process.env.FONTCONFIG_FILE || 'default', | |
| interFonts: "${fontList.trim()}", | |
| sampleSystemFonts: "${systemFonts.trim()}" | |
| }}`); | |
| } catch (fontDebugError) { | |
| console.error('[AWS] Font debug error:', fontDebugError); | |
| } | |
| if (!fs.existsSync(templatePath)) { | |
| console.error("[AWS] Template file not found at:", templatePath); | |
| return res.json({error: 'Template not found'}); | |
| } | |
| if (!fs.existsSync(fontPath)) { | |
| console.error("[AWS] Font file not found at:", fontPath); | |
| return res.json({error: 'Font file not found'}); | |
| } | |
| // Validate required data | |
| if (!athleteA || !athleteB) { | |
| console.error("[AWS] Missing required athlete data", { athleteA, athleteB }); | |
| return res.json({error: 'Athlete names are required'}); | |
| } | |
| // Extract data with fallbacks | |
| const compatibilityScore = Math.round(doublesData?.compatibility_score); // Use template value as fallback | |
| const overallPredictions = doublesData?.overall_predictions; | |
| const peakPotential = overallPredictions.excellent; // This is already formatted as HH:MM:SS | |
| const expectedFinish = overallPredictions.expected; | |
| process.stdout.write(`[AWS] Data extracted successfully ${{ | |
| compatibilityScore, | |
| overallPredictions, | |
| peakPotential, | |
| expectedFinish, | |
| division, | |
| athleteA, | |
| athleteB | |
| }}`); | |
| // Create text overlays based on actual template dimensions (1080x1919) | |
| // Fonts are now installed system-wide via Dockerfile and fontconfig | |
| const textElements = [ | |
| // Division - positioned in the top Division box (left side) | |
| { | |
| text: division, | |
| left: 105, | |
| top: 445, | |
| fontSize: 32, | |
| fill: '#FFFFFF' | |
| }, | |
| // Partner A name - left box | |
| { | |
| text: athleteA, | |
| left: 127, | |
| top: 645, | |
| fontSize: 28, | |
| fill: '#FFFFFF' | |
| }, | |
| // Partner B name - right box | |
| { | |
| text: athleteB, | |
| left: 605, | |
| top: 645, | |
| fontSize: 28, | |
| fill: '#FFFFFF' | |
| }, | |
| // Compatibility score percentage - center of the compatibility chart area | |
| { | |
| text: `${compatibilityScore}%`, | |
| left: 415, | |
| top: 1065, | |
| fontSize: 100, | |
| fill: '#FFFFFF' | |
| }, | |
| // Peak potential time - left box | |
| { | |
| text: peakPotential, | |
| left: 120, | |
| top: 1500, | |
| fontSize: 48, | |
| fill: '#FFFFFF' | |
| }, | |
| // Expected finish time - right box | |
| { | |
| text: expectedFinish, | |
| left: 615, | |
| top: 1500, | |
| fontSize: 48, | |
| fill: '#FFFFFF' | |
| } | |
| ]; | |
| // Create compatibility chart SVG (based on PartnerCompatibilityScore component) | |
| // Scaled to match the 1080x1919 template dimensions | |
| const radius = 160; | |
| const strokeWidth = 20; | |
| const normalizedRadius = radius - strokeWidth * 2; | |
| const circumference = normalizedRadius * 2 * Math.PI; | |
| const arcLength = circumference * 0.5; // 50% of circle for semi-circle | |
| const strokeDasharray = `${arcLength} ${circumference}`; | |
| const strokeDashoffset = arcLength * (1 - compatibilityScore); | |
| // Calculate the width percentage based on compatibilityScore (0-100) | |
| const widthPercentage = Math.min(Math.max(compatibilityScore, 0), 100); | |
| // Create a clipping path that will show only the percentage of the original path | |
| // The original path goes from x=195.249 to x=888.765, so we calculate the end point | |
| const startX = 168; | |
| const endX = 915.9; | |
| const totalWidth = endX - startX; | |
| const dynamicEndX = startX + (totalWidth * widthPercentage / 100); | |
| const compatibilityChartSvg = { | |
| input: Buffer.from(` | |
| <svg width="1080" height="1919" viewBox="0 0 1080 1919" fill="none"> | |
| <defs> | |
| <clipPath id="compatibilityClip"> | |
| <rect x="${startX}" y="769" width="${totalWidth * widthPercentage / 100}" height="400"/> | |
| </clipPath> | |
| </defs> | |
| <path d="M195.249 1137.02C178.596 1137.02 164.964 1123.5 166.366 1106.91C169.567 1069 178.799 1031.75 193.786 996.499C212.728 951.947 240.491 911.466 275.49 877.367C310.49 843.268 352.04 816.22 397.769 797.766C443.498 779.312 492.51 769.813 542.007 769.813C591.503 769.813 640.515 779.312 686.244 797.766C731.973 816.22 773.524 843.268 808.523 877.367C843.523 911.466 871.286 951.947 890.227 996.499C905.214 1031.75 914.446 1069 917.648 1106.91C919.05 1123.5 905.418 1137.02 888.765 1137.02V1137.02C872.112 1137.02 858.768 1123.49 857.101 1106.92C854.063 1076.74 846.467 1047.1 834.512 1018.98C818.601 981.559 795.28 947.555 765.88 918.912C736.481 890.269 701.579 867.549 663.166 852.047C624.754 836.546 583.584 828.567 542.007 828.567C500.43 828.567 459.26 836.546 420.847 852.047C382.435 867.549 347.533 890.269 318.133 918.912C288.734 947.555 265.413 981.559 249.502 1018.98C237.546 1047.1 229.951 1076.74 226.913 1106.92C225.245 1123.49 211.902 1137.02 195.249 1137.02V1137.02Z" | |
| fill="#FAD94A" | |
| clip-path="url(#compatibilityClip)"/> | |
| </svg> | |
| `), | |
| top: 0, | |
| left: 0 | |
| }; | |
| // Create SVG overlays for each text element using system fonts | |
| const textOverlays = textElements.map((element, index) => { | |
| console.log(`[AWS] Creating text overlay ${index + 1}`, { | |
| text: element.text, | |
| position: { left: element.left, top: element.top }, | |
| fontSize: element.fontSize, | |
| fill: element.fill | |
| }); | |
| return { | |
| input: Buffer.from(` | |
| <svg width="1080" height="1919" viewBox="0 0 1080 1919"> | |
| <text x="${element.left}" y="${element.top}" | |
| font-family="'Inter', 'sans-serif'" | |
| font-size="${element.fontSize}" | |
| font-weight="bold" | |
| fill="${element.fill}">${element.text.replace(/[<>&"']/g, (match) => { | |
| const escapes: { [key: string]: string } = { | |
| '<': '<', | |
| '>': '>', | |
| '&': '&', | |
| '"': '"', | |
| "'": ''' | |
| }; | |
| return escapes[match]; | |
| })}</text> | |
| </svg> | |
| `), | |
| top: 0, | |
| left: 0 | |
| }; | |
| }); | |
| // Combine all overlays | |
| const svgOverlays = [compatibilityChartSvg, ...textOverlays]; | |
| process.stdout.write(`[AWS] SVG overlays prepared ${{ | |
| totalOverlays: svgOverlays.length, | |
| compatibilityChartIncluded: true, | |
| textOverlaysCount: textOverlays.length | |
| }}`); | |
| // Process the image with Sharp | |
| let processedImage: Buffer; | |
| try { | |
| process.stdout.write(`[AWS] Starting Sharp image processing ${{ | |
| templatePath, | |
| overlayCount: svgOverlays.length, | |
| timestamp: new Date().toISOString() | |
| }}`); | |
| const sharpInstance = sharp(templatePath); | |
| // Log template metadata | |
| const metadata = await sharpInstance.metadata(); | |
| console.log("[AWS] Template metadata", metadata); | |
| processedImage = await sharpInstance | |
| .composite(svgOverlays) | |
| .png() | |
| .toBuffer(); | |
| console.log("[AWS] Sharp processing completed successfully", { | |
| outputBufferSize: processedImage.length, | |
| timestamp: new Date().toISOString() | |
| }); | |
| } catch (sharpError: any) { | |
| console.error('[AWS] Sharp processing error:', { | |
| error: sharpError, | |
| message: sharpError.message, | |
| stack: sharpError.stack, | |
| timestamp: new Date().toISOString() | |
| }); | |
| // Fallback: return the original template if processing fails | |
| console.log("[AWS] Using fallback - returning original template"); | |
| processedImage = fs.readFileSync(templatePath); | |
| } | |
| console.log("[AWS] Sending response", { | |
| contentType: 'image/png', | |
| bufferSize: processedImage.length, | |
| timestamp: new Date().toISOString() | |
| }); | |
| res.set({ | |
| 'Content-Type': 'image/png', | |
| 'Content-Disposition': 'attachment; filename="doubles-team-story.png"', | |
| }); | |
| return res.send(processedImage); | |
| } catch (error: any) { | |
| console.error('[AWS] Error generating image:', { | |
| error: error, | |
| message: error.message, | |
| stack: error.stack, | |
| timestamp: new Date().toISOString() | |
| }); | |
| return res.status(500).json({ | |
| error: 'Failed to generate image', | |
| timestamp: new Date().toISOString(), | |
| details: process.env.NODE_ENV === 'development' ? error.message : undefined | |
| }); | |
| } | |
| }) |
Author
remainstheday
commented
Aug 4, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment