node.jsexpressserverless-frameworkgoogle-cloud-runsharp

How can I configure Google Cloud Run and sharp so that my generated images contain text instead of boxes?


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:

boxes on image

whereas locally I get:

text on image

Logs show an error of:

Fontconfig error: Cannot load default config file: No such file: (null)

I've already attempted a number of options:

  1. different fonts in the svg (Arial, sans-serif, dejavu, "DejaVu Sans")
  2. put an Arial.ttf (and fonts.conf) in multiple places (root of the service, in a fonts folder, in a 'rootfiles' folder that (i.e. so that it's available at https://tameside-badminton.co.uk/Arial.ttf, in the 'home' folder via the cloudshell console)
  3. passed FC_DEBUG values (1 etc.) and simpl
  4. passed FONTCONFIG_PATH and FONTCONFIG_FILE values like /usr/share/fonts, /fonts, ~/fonts/
  5. set PANGOCAIRO_BACKEND = fontconfig
  6. ran fc-list, fc-cat, fc-cache that show fonts like Dejavu Sans etc. etc.

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.


Solution

  • 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:

    1. run 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.
    2. create a folder in the root of your application for fonts (and called fonts). for my own purposes i've just dropped an Arial.ttf in mine, you could add whatever truetype fonts you like in that folder
    3. create yourself a Dockerfile in the root of your application, after a bit of trial and error combining information i found from a medium.com article and an example docker file on google cloud github i pulled together the following:
    
    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" ]
    
    
    1. now run gcloud beta code dev and you should see a working sharp with text instead of boxes :)