Skip to content

Instantly share code, notes, and snippets.

@remainstheday
Last active August 4, 2025 17:35
Show Gist options
  • Select an option

  • Save remainstheday/d94475b9a73a5d69e6274a7d638a0cd7 to your computer and use it in GitHub Desktop.

Select an option

Save remainstheday/d94475b9a73a5d69e6274a7d638a0cd7 to your computer and use it in GitHub Desktop.
Sharp Image Endpoint
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 } = {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'"': '&quot;',
"'": '&#39;'
};
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
});
}
})
@remainstheday
Copy link
Author

FROM node:20-bookworm
ARG SUPABASE_URL
ARG SUPABASE_ANON_KEY
ARG STRIPE_SECRET
ARG DASHBOARD_URL

ENV SUPABASE_URL=$SUPABASE_URL
ENV SUPABASE_ANON_KEY=$SUPABASE_ANON_KEY
ENV STRIPE_SECRET=$STRIPE_SECRET
ENV DASHBOARD_URL=$DASHBOARD_URL



# Set the working directory in the container to /app
WORKDIR /app

COPY package*.json ./

# Clear npm cache and install packages
RUN npm install

# Install font rendering dependencies for Sharp/librsvg
RUN apt-get update && apt-get install -y \
    fontconfig \
    fonts-dejavu-core \
    librsvg2-dev \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# Copy custom fonts BEFORE main application copy to avoid duplication
COPY src/fonts/ /usr/local/share/fonts/
RUN chmod -R 644 /usr/local/share/fonts/* \
    && fc-cache -f -v \
    && fc-list | grep -i inter || echo "Inter fonts not found in fontconfig"

# Copy the rest of the application code
COPY . .

# Build the TypeScript code
RUN npm run build


# Make port 3000 available to the world outside this container
EXPOSE 3000

# Run the app when the container launches
CMD ["npm", "start"]```

@remainstheday
Copy link
Author

doubles-team-story

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment