canvas icon indicating copy to clipboard operation
canvas copied to clipboard

`GlobalFonts.register` uses reference to passed-in Buffer which can be invalidated

Open 190n opened this issue 10 months ago • 3 comments

The register function takes a Buffer of the font data, but it makes the registered font reference the memory owned by the Buffer instead of making a copy of the data. See: https://github.com/Brooooooklyn/canvas/blob/e609b202ed35b0c0bfc16d7dcfa95afb0c66100c/skia-c/skia_c.cpp#L1712

This is fine sometimes, but it doesn't work if the buffer will get overwritten or if it gets garbage-collected. oven-sh/bun#17620 is an example of unexpected behavior caused by this issue.

Here is a script to demonstrate this problem:

import fs from 'node:fs/promises';
import { GlobalFonts, createCanvas } from "@napi-rs/canvas";

// `npm install @fontsource/roboto`, or replace with the path to another font
let roboto = Buffer.from(await fs.readFile('./node_modules/@fontsource/roboto/files/roboto-latin-400-normal.woff'));
GlobalFonts.register(
  roboto,
  "roboto"
);

if (process.env.OVERWRITE) {
  roboto.fill(0);
}

const canvas = createCanvas(420, 72);
const ctx = canvas.getContext("2d");
ctx.fillStyle = "silver";
ctx.beginPath();
ctx.rect(0, 0, 420, 72);
ctx.fill();
ctx.font = 'bold 72px "roboto"';
ctx.fillStyle = "#2B2B2B";
ctx.fillText("hello world", 0, 72);
const content = await canvas.encode("png");
await fs.writeFile("./out.png", content);

When run normally, out.png has the text "hello world" visible:

Image

When run with the environment variable OVERWRITE=1, the image is blank because the font data isn't valid anymore:

Image

I believe either GlobalFonts.register should make a copy of the Buffer data to ensure that it can always access the font data, or it should be clearly documented that the user needs to pass in a Buffer that will never be overwritten nor garbage-collected.

190n avatar Feb 24 '25 23:02 190n

Thanks for the fix, but it looks like that might have only fixed the case where the buffer gets garbage-collected (I'm guessing based on the use of Arc), not the case where it is still alive but gets overwritten? After upgrading to v0.1.68, running the script I sent above with OVERWRITE=1 still produces a blank image with no text. I do see that GlobalFonts.register returns a FontKey object so it seems like I definitely have the right version installed.

190n avatar Mar 05 '25 05:03 190n

Could you reopen this issue? Or should I open a new one? The reproducer script still shows the same buggy behavior for me in v0.1.68.

190n avatar Mar 07 '25 21:03 190n

Having the same issue with FROM node:22-alpine Docker image.

No issue under Windows or Ubuntu.

This is my workaround to keep the buffer:

var buffers: { [key: string]: Buffer } = {};

  LoadFontFromBuffer(buffer: Buffer, font: string): void {
    buffers[font] = buffer;

    GlobalFonts.register(buffers[font], font);
  }

valamidev avatar May 21 '25 18:05 valamidev