I've built a service using Node.js & sharp that composites some text onto a background image:
exports.social_get_result = async function(req,res,next){
// Path to the existing background image
const backgroundImagePath = './static/images/bg/social-'+ req.params.division.replace(/([\s]{1,})/g,'-') +'.png';
// Text settings
const linesOfText = ["Result: "+ req.params.homeTeam +" vs ", req.params.awayTeam,req.params.homeScore +"-"+ req.params.awayScore,"#tameside #badminton #tbl #result","https://tameside-badminton.co.uk"];
const textSize = 60;
const textColor = { r: 0, g: 0, b: 0, alpha: 1 }; // White color
const lineHeight = 1.2; // Line height multiplier
const image = sharp(backgroundImagePath);
// Get the metadata of the image (e.g., dimensions)
const metadata = await image.metadata();
const width = metadata.width;
const height = metadata.height;
// Create an SVG overlay with multiple lines of text
const svgText = `
<svg width="${width}" height="${height}">
<style>
.title { fill: rgba(${textColor.r}, ${textColor.g}, ${textColor.b}, ${textColor.alpha}); font-size: ${textSize}px; font-weight: bold; font-family:Arial }
.footer { fill: rgba(${textColor.r}, ${textColor.g}, ${textColor.b}, ${textColor.alpha}); font-size: 30px; font-weight: bold; font-family:Arial }
</style>
<text x="10%" y="70%" text-anchor="left">
<tspan x="10%" dy="1.2em" class="title">${linesOfText[0]}</tspan>
<tspan x="10%" dy="1.2em" class="title">${linesOfText[1]}</tspan>
<tspan x="10%" dy="1.2em" class="title">${linesOfText[2]}</tspan>
<tspan x="10%" dy="1.2em" class="footer">${linesOfText[3]}</tspan>
<tspan x="10%" dy="1.2em" class="footer">${linesOfText[4]}</tspan>
</text>
</svg>`;
const buffer = Buffer.from(svgText);
// Composite the text onto the existing image
const finalImage = await image
.composite([{ input: buffer }])
.toFormat('JPG')
.toFile('static/images/generated/'+ req.params.homeTeam.replace(/([\s]{1,})/g,'-') + req.params.awayTeam.replace(/([\s]{1,})/g,'-') +'.jpg')
const outputImage = await image
.composite([{ input: buffer }])
.toBuffer()
.then(data => res.type('png').send(data))
}
Currently, once deployed on Google Cloud Run, this outputs:
whereas locally I get:
Logs show an error of:
Fontconfig error: Cannot load default config file: No such file: (null)
I've already attempted a number of options:
/usr/share/fonts
, /fonts
, ~/fonts/
I understand that serverless environments need some level of config, but I can't seem to get it to either find the font files that are available, or the fonts.conf file, which I understand (from reading through the sharp docs, and the fontconfig docs, as well as many other posts about similar challenges with AWS Lambda solutions) are required for serverless environments.
I can't help thinking that the (null) part of the default configuration error is trying to tell me something, but beyond FONTCONFIG_PATH I can't think of a way to change that value.
Firstly, a thank you to @DazWilkin for the nudge in the right direction;
for anybody else questioning their sanity (or just a bit new to the serverless /container game like myseld)
The trick here is to write your own dockerfile, rather than relying on Google Cloud CLI to do it all for you. Steps i followed:
gcloud beta code dev
in your directory (Saying yes to installing various services and docker desktop locally) - this'll replicate what's going to happen when you deploy to cloud run allowing you to see locally what's going on - needless to say this is what illuminated the issue locally for me.
FROM node:20-slim
# copy the /fonts directory into a folder that's visible to fontconfig
# and install fontconfig service in the container then clear the font cache
COPY --chown=node:node --chmod=755 /fonts ./usr/local/share/fonts
RUN apt-get update; apt-get install -y fontconfig
RUN fc-cache -f -v
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
# Copy local code to the container image.
COPY . .
# Run the web service on container startup.
CMD [ "npm", "start" ]
gcloud beta code dev
and you should see a working sharp with text instead of boxes :)