Brooooooklyn / canvas

High performance skia binding to Node.js. Zero system dependencies and pure npm packages without any postinstall scripts nor node-gyp.
https://vercel.skia.rs
MIT License
1.7k stars 70 forks source link

Text not rendered in canvas under Google Cloud Run node:18-alpine #731

Open iKK001 opened 10 months ago

iKK001 commented 10 months ago

I deploy my nodeJS (and nestJS) application by Google Cloud Run with node:18-alpine. Everything works - except:

The Text in my canvas is not rendered !!!!!! (funny enough, rectangles are rendered. It is only simple Arial-text that does not seem to render correctly).

I am running node:18-alpine linux on the Cloud Run Docker (see Dockerfile further down...).

Here is my canvas code (see below).

import { createCanvas } from '@napi-rs/canvas';

GlobalFonts.registerFromPath(
  path.join(__dirname, '..', 'src', 'fonts', 'Arial.ttf'), 'Arial',
);

console.info(GlobalFonts.families); 
// prints out :  "[ { family: 'Arial', styles: [] } ]"
// so I assume the font is correctly registered. But it does not work !!!!!!!!!!!!!!!!!!

@Injectable()
export class CanvasService {
  async createSBBConnectionCanvas(tripData: TripCanvasData): Promise<Buffer> {
    // Set font properties for the text-labels
    const fontSize = 24;
    const fontFamily = 'Arial';

    // Create a new canvas and set its context
    const canvasWidth = 400;
    const canvasHeight = 300;
    const canvas = createCanvas(canvasWidth, canvasHeight);
    const ctx = canvas.getContext('2d');

    // make white background
    ctx.fillStyle = 'rgba(255,255,255,1)';
    ctx.fillRect(0, 0, canvasWidth, canvasHeight);

    // set font and text-color
    ctx.font = `${fontSize}px ${fontFamily}`;
    ctx.fillStyle = 'rgba(0,0,0,1)';

    // draw small rectangle
    // THIS ONE WORKS WELL !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    ctx.fillRect(20, 20, 20, 20);

    // draw text
    // THIS DOES NOT WORK - WHY ????????????????????????????????????????
    ctx.fillText('hello world', 10, 150);

    // Encode the chart as a PNG image Buffer and return it
    const pngData = await canvas.encode('png');
    return pngData;
  }
}

And here is the result-image.

As you can see, the small rectangle is drawn. But the text is MISSING. Why ?????

example

For completeness, here is my Dockerfile:

###################
# BUILD FOR LOCAL DEVELOPMENT
###################

FROM node:18-alpine As development

# Create app directory
WORKDIR /usr/src/app

# Copy application dependency manifests to the container image.
# A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
# Copying this first prevents re-running npm install on every code change.
COPY --chown=node:node package*.json ./

# Install app dependencies using the `npm ci` command instead of `npm install`
RUN yarn install

# Bundle app source
COPY --chown=node:node . .

# Use the node user from the image (instead of the root user)
USER node

###################
# BUILD FOR PRODUCTION
###################

FROM node:18-alpine As build

WORKDIR /usr/src/app

COPY --chown=node:node package*.json ./

# In order to run `npm run build` we need access to the Nest CLI which is a dev dependency. In the previous development stage we ran `npm ci` which installed all dependencies, so we can copy over the node_modules directory from the development image
COPY --chown=node:node --from=development /usr/src/app/node_modules ./node_modules

COPY --chown=node:node . .

# re-add once tests are added
# RUN yarn test

# Run the build command which creates the production bundle
RUN yarn build

# Set NODE_ENV environment variable
ENV NODE_ENV production

# Running `npm ci` removes the existing node_modules directory and passing in --only=production ensures that only the production dependencies are installed. This ensures that the node_modules directory is as optimized as possible
RUN yarn install --production && yarn cache clean

USER node

###################
# PRODUCTION
###################

FROM node:18-alpine As production

# Copy the bundled code from the build stage to the production image
COPY --chown=node:node --from=build /usr/src/app/node_modules ./node_modules
COPY --chown=node:node --from=build /usr/src/app/dist ./dist

# Start the server using the production build
CMD [ "node", "dist/main.js" ]

P.S. As mentioned here, I also tried adding the following to the dockerfile - but WITHOUT SUCCESS :(

COPY ./src/fonts/Arial.ttf ./
RUN mkdir -p /usr/share/fonts/truetype/
RUN install -m644 Arial.ttf /usr/share/fonts/truetype/
RUN rm ./Arial.ttf
Brooooooklyn commented 10 months ago

@iKK001 you need to provide the Arial.ttf file so I can debug it

iKK001 commented 10 months ago

Arial.ttf.zip

iKK001 commented 10 months ago

I also tried with helvetica-bold.tff

Both do not work...

helvetica-bold.ttf.zip

iKK001 commented 10 months ago

I am still clueless why the text is not rendered

My first thought was that it has nothing to do with the individual.ttf files - but rather the fonts cannot be found for some reason.

So I made the following:

The font-file 'Arial.ttf' is now downloaded from a Firestore Storage Bucket. This way, I am absolutely sure to have the correct file at run-time.

import { getStorage } from 'firebase-admin/storage';
import { GlobalFonts, Image, loadImage } from '@napi-rs/canvas';

const fontFile = getStorage().bucket().file(`assets/fonts/Arial.ttf`);
const [fontUrl] = await fontFile.getSignedUrl({
  version: 'v4',
  action: 'read',
  expires: Date.now() + 60 * 60 * 1000 * 12, // 12 hrs
});
GlobalFonts.registerFromPath(fontUrl, 'Arial');

I know that it works becasue I did the same with a logo. And the logo renders correctly.

--> But the text still does not render at all !!!!!

--> The Font Registry is corrupt !!!!

This is really nerv wrecking.

By the way, everything works nice and smooth if I run the same code locally on my Mac.

But as soon as I deploy it to the Google Cloud Run node:18-alpine Docker container, then NO TEXT IS SHOWN ANYMORE !!!!

P.S. I'm using version @napi-rs/canvas v0.1.44

"dependencies": {
    "@napi-rs/canvas": "^0.1.44",
}

P.S. My nodeJS server runs nestJS - not sure if that plays a role at all. It shouldn't...

iKK001 commented 10 months ago

There is a temporary workaround:

You can use a npm package for canvas fonts as this example shows:

import { GlobalFonts } from '@napi-rs/canvas';

GlobalFonts.registerFromPath(
  require('@canvas-fonts/helveticaneue'),
  'HelveticaNeue',
);

Just make sure to add the corresponding npm package to your package.json file:

"dependencies": {
  "@napi-rs/canvas": "^0.1.44",
  "@canvas-fonts/helveticaneue": "^1.0.4",
  // ...
}

There are many more such packages out there if you need other fonts.

Still, open questions to the original issue with Google Cloud Run and node:18-alpine remain:

  1. do I need to COPY font files in my Dockerfile ? If yes, how exactly ? How can I persist font-files in a nodeJS server application hosted on Google Cloud Run ? How does the font-registry work exactly ?

  2. When using a font-downloaddURL (as the example from the previous comment with the bucket-storage shows): Why is the registry not working from a web-URL ? GlobalFonts.registerFromPath(fontUrl, 'Arial'); ?? Is it due to the corrupt font or is this not possible at all ?

Thank you for clarifying more on this.

In the meantime I can live with the extra npm-package font.

0xYoyo commented 8 months ago

@iKK001 You are an absolute legend, I was stuck for hours trying to figure out why my text won't render using a digital ocean VPS until I came across your temporary fix.

It seems to be an issue across multiple providers since we are both using different ones. Bumping this issue as I'm sure its relevant across even more. Thank you.