Automattic / node-canvas

Node canvas is a Cairo backed Canvas implementation for NodeJS.
10.21k stars 1.17k forks source link

Canvas::RegisterFont does not detect font family on Alpine and end up "Could not parse font file" #1164

Open marcj opened 6 years ago

marcj commented 6 years ago

I have an application where users are allowed to upload their fonts. Some of them do not have a font family defined. When you use a font file like that you get always an error Could not parse font file.

The check is here https://github.com/Automattic/node-canvas/blob/06fbaf9e1bb51009c5170a337d67c3cbdf70dd6d/src/register_font.cc#L207

and came with this commit:

https://github.com/Automattic/node-canvas/commit/06fbaf9e1bb51009c5170a337d67c3cbdf70dd6d

This function get_pango_font_description is called by Canvas::RegisterFont which has the fontFamily as argument value, so theoretically we have all information in place and should not return with an error if the font file has no font family.

I made already changes to support this and also to improve error messages. We currently get always 'Could not parse font file', no matter what the error was inside of get_pango_font_description, so it was really hard for me to debug this case. I'm not sure if this project is still kind of active, so I'd appreciate some feedback whether a PR is welcomed before I actually provide a PR.


There might be another issue with this as I saw the font I'm using (a .ttf) is shown in my OSX with a font name, however node canvas says it can't find a font family name, so it might be that get_family_name is not fully functional.

chearon commented 6 years ago

Do you have an example of such a font? It seems more likely that we might not be looking at all the right fields of the font, since a font without a family name probably wouldn't work right in the OS.

In any case the ideal thing would be to associate the family name that they pass to registerFont with that actual font file so that the font file's metadata doesn't even matter, like browsers do with @font-face. That's not possible using Pango APIs though, we would have to implement font selection ourselves. (I actually think that's the right approach anyways, but I haven't had the time to do it yet).

marcj commented 6 years ago

Yes, I'm working currently with this font here https://www.fontsquirrel.com/fonts/alex-brush. OSX detects the name as well as the tool I use in the frontend https://github.com/nodebox/opentype.js.

However, in get_family_name(face) returns NULL so it overall fails with the error from above.

marcj commented 6 years ago

In any case the ideal thing would be to associate the family name that they pass to registerFont with that actual font file

I agree. As far as I see (I don't know internals or API of pango well enough), we need to skip the call to pango_font_description_set_family_static when we can't detect a font family name using get_family_name, and if so (not detected) the caller Canvas::RegisterFont needs to check whether the user provided a font family name, and if so we set it. if no font family name is detect and no font family name is given by the user, then we should throw an error. I'm not sure if this works the way FT_Done_Face|FT_Done_FreeType is implemented. It might be they expect to have a family set before calling those, but as I said, I have no knowledge about pango/FT. (Edit: I saw they are just cleaning methods and have nothing to do with pango) If someone points me into the right direction I can provide a PR.

Edit2: I see we use here two different methods to set a family name pango_font_description_set_family_static inside the pango_font_description_new function vs pango_font_description_set_family inside Canvas::RegisterFont. I'm not sure why we have here two different and if one overwrites the other.

chearon commented 6 years ago

As far as I see (I don't know internals or API of pango well enough), we need to skip the call to pango_font_description_set_family_static when we can't detect a font family name using get_family_name, and if so (not detected) the caller Canvas::RegisterFont needs to check whether the user provided a font family name, and if so we set it. if no font family name is detect and no font family name is given by the user, then we should throw an error.

We actually always use the family name the user provided. Node-canvas can only set the current Pango font via a full description (family, weight, etc), so RegisterFont actually creates a description that should end up with the right font (get_family_name is part of that). It's really quite an inconvenient API for us so it doesn't work how you might expect it to...

Right now I think the problem might be with your system's FreeType though. I just tried the font you linked and it worked for me (it both registered the font and drew text with it) on macOS and Ubuntu. What OS are you getting the issue on and can you try it on another one to see if it works?

marcj commented 6 years ago

Mh, I see. I use alpine in Docker. My Dockerfile is:

# edge necessary for vips
FROM alpine:edge

ARG VERSION=dev-master
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV PORT 80
ENV PATH /app/node_modules/.bin/:${PATH}

RUN apk --no-cache add tzdata && \
    cp /usr/share/zoneinfo/Etc/UTC /etc/localtime && \
    echo "UTC" | tee /etc/timezone && \
    apk del tzdata

RUN apk --no-cache add git bash
RUN apk add --update nodejs nodejs-npm && npm install -g npm

# for node-canvas
RUN apk add --no-cache \
    build-base \
    g++ \
    cairo-dev \
    jpeg-dev \
    pango \
    pango-dev \
    giflib-dev \
    bash \
    imagemagick \
    # for canvas-prebuilt
    pixman \
    libc6-compat \
    # defaults fonts for canvas
    ttf-opensans ttf-dejavu ttf-droid ttf-freefont ttf-liberation ttf-ubuntu-font-family fontconfig

# necessary for npm package sharp
RUN apk --no-cache add vips-dev fftw-dev --repository https://dl-3.alpinelinux.org/alpine/edge/testing/

ADD app/ /app/
RUN echo ${VERSION} > /app/version
RUN cd /app && ([ ! -d "./node_modules" ] && npm install) || true
RUN cd /app && node_modules/.bin/ng build --prod --no-aot

ADD docker/run.sh /run.sh

EXPOSE 80

WORKDIR /app

CMD /bin/bash /run.sh

and "canvas": "^2.0.0-alpha.12". I haven't tried it on other OS yet as I actually only need it in Docker. I might be able to check Ubuntu, but need to write first a new Dockerfile and would definitely prefer alpine due to the size.

chearon commented 6 years ago

I dunno, looking at get_family_name it's pretty straightforward, but I also don't see anything in FreeType's changelogs that indicate anything like this ever broke. You could try compiling FreeType from source instead of using Alpine's version I guess. Here's canvas-prebuilt's Dockerfile for building in Linux.

marcj commented 6 years ago

Could you tell me please which FreeType you are using that worked for you?

chearon commented 6 years ago

macOS - 2.9.0 Ubuntu - 2.8.1

marcj commented 6 years ago

Alright thanks! I just tried both from source in Alpine, neither worked. I'm still investigating.

I'm also getting following warnings when working with the canvas and those fonts, but I currently don't know if that is related.

adflow_1        | (sharp:14481): Pango-WARNING **: 20:12:50.239: failed to create cairo scaled font, expect ugly output. the offending font is 'Alex Brush 19.5'
adflow_1        |
adflow_1        | (sharp:14481): Pango-WARNING **: 20:12:50.239: font_face status is: file not found
adflow_1        |
adflow_1        | (sharp:14481): Pango-WARNING **: 20:12:50.239: scaled_font status is: file not found

I'm trying now lib per lib from your linked Dockerfile. Thanks for your fast replies! :)

marcj commented 6 years ago

Ok I tried now Ubuntu 16:04 using this Dockerfile based on your linked Dockerfile

FROM ubuntu:16.04

ARG VERSION=dev-master
ENV PORT 80
ENV PATH /app/node_modules/.bin/:${PATH}

RUN apt-get -y update && apt-get install -y locales && locale-gen en_US.UTF-8
RUN locale-gen en_US.UTF-8
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8'

RUN apt-get update && apt-get -y install curl python pkg-config g++-4.7 git \
    gtk-doc-tools x11proto-xext-dev make gcc g++ nasm wget gperf bzip2 libmount-dev
RUN curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.1/install.sh | bash
RUN bash -c 'cd; export NVM_DIR=$HOME/.nvm; . $NVM_DIR/nvm.sh; nvm install 7'

RUN bash -c 'cd; curl -O https://zlib.net/fossils/zlib-1.2.8.tar.gz; tar -xvf zlib-1.2.8.tar.gz; cd zlib-1.2.8; ./configure; make; make install'
RUN bash -c 'cd; curl -O http://superb-dca2.dl.sourceforge.net/project/giflib/giflib-5.1.4.tar.gz; tar -xvf giflib-5.1.4.tar.gz; cd giflib-5.1.4; ./configure; make; make install'
RUN bash -c 'cd; curl -O ftp://sourceware.org/pub/libffi/libffi-3.2.1.tar.gz; tar -xvf libffi-3.2.1.tar.gz; cd libffi-3.2.1; ./configure; make; make install'
RUN bash -c 'cd; curl -O https://kent.dl.sourceforge.net/project/libpng/libpng16/older-releases/1.6.30/libpng-1.6.30.tar.xz; tar -xvf libpng-1.6.30.tar.xz; cd libpng-1.6.30; ./configure; make; make install'
RUN bash -c 'cd; curl -O https://cytranet.dl.sourceforge.net/project/libjpeg-turbo/1.5.2/libjpeg-turbo-1.5.2.tar.gz; tar -xvf libjpeg-turbo-1.5.2.tar.gz; cd libjpeg-turbo-1.5.2; ./configure --prefix=/usr/local; make; make install'
RUN bash -c 'cd; curl -O https://ftp.pcre.org/pub/pcre/pcre-8.41.tar.bz2; tar -xvf pcre-8.41.tar.bz2; cd pcre-8.41; ./configure --enable-pcre16 --enable-pcre32 --enable-utf --enable-unicode-properties; make; make install '
RUN ldconfig
RUN bash -c 'cd; curl -O http://gensho.ftp.acc.umu.se/pub/gnome/sources/glib/2.52/glib-2.52.3.tar.xz; tar -xvf glib-2.52.3.tar.xz; cd glib-2.52.3; ./configure; make; make install'
RUN bash -c 'cd; curl -O https://svwh.dl.sourceforge.net/project/freetype/freetype2/2.8/freetype-2.8.tar.gz; tar -xvf freetype-2.8.tar.gz; cd freetype-2.8; ./configure; make; make install'
RUN bash -c 'cd; curl -O https://www.freedesktop.org/software/harfbuzz/release/harfbuzz-1.4.7.tar.bz2; tar -xvf harfbuzz-1.4.7.tar.bz2; cd harfbuzz-1.4.7; ./configure; make; make install'
RUN bash -c 'cd; curl -O https://cytranet.dl.sourceforge.net/project/expat/expat/2.2.0/expat-2.2.0.tar.bz2; tar -xvf expat-2.2.0.tar.bz2; cd expat-2.2.0; ./configure; make; make install'
RUN ldconfig
RUN bash -c 'cd; curl -O https://www.freedesktop.org/software/fontconfig/release/fontconfig-2.12.4.tar.bz2; tar -xvf fontconfig-2.12.4.tar.bz2; cd fontconfig-2.12.4; ./configure --enable-static --sysconfdir=/etc --localstatedir=/var; make; make install'
RUN bash -c 'cd; curl -O https://www.cairographics.org/releases/pixman-0.34.0.tar.gz; tar -xvf pixman-0.34.0.tar.gz; cd pixman-0.34.0; ./configure; make; make install'
RUN bash -c 'cd; curl -O https://www.cairographics.org/releases/cairo-1.14.10.tar.xz; tar -xvf cairo-1.14.10.tar.xz; cd cairo-1.14.10; ./configure; make; make install'
RUN bash -c 'cd; curl -O http://ftp.gnome.org/pub/GNOME/sources/pango/1.40/pango-1.40.7.tar.xz; tar -xvf pango-1.40.7.tar.xz; cd pango-1.40.7; ./configure; make; make install'

# https://stackoverflow.com/a/29729834/487014
RUN cp /etc/apt/sources.list /etc/apt/sources.list.WHEEZY
RUN cat /etc/apt/sources.list.WHEEZY | sed 's/wheezy/jessie/g' > /etc/apt/sources.list
RUN apt-get update
RUN apt-get -y install gcc-4.9 g++-4.9
RUN cp /etc/apt/sources.list.WHEEZY /etc/apt/sources.list

RUN curl -sL https://deb.nodesource.com/setup_8.x | bash -
RUN apt-get install -y nodejs build-essential

ADD app/ /app/
RUN echo ${VERSION} > /app/version
RUN cd /app && ([ ! -d "./node_modules" ] && npm install) || true
RUN cd /app && node_modules/.bin/ng build --prod --no-aot

ADD docker/run.sh /run.sh

EXPOSE 80

WORKDIR /app

CMD /bin/bash /run.sh

and have exactly the same issue.

adflow_1        | Error: Could not parse font file
adflow_1        |     at Object.registerFont (/app/node_modules/canvas/index.js:48:17)

Maybe some more information is necessary: I have the ttf of Alex Brush in /tmp folder. I'm not sure if this folder is somewhat blacklisted or something. I'm running actually out of ideas why this my environment can't read that font.

marcj commented 6 years ago

Alright, I found out it registers now without error after rebuilding everything using Ubuntu. :) Weird that it doesn't work in Alpine using exactly the same versions from the Dockerfile using even the same RUN commands. However, the canvas displays the text using blocks instead of the actual font. :(

.woff files trigger currently the same error Could not parse font file. However, I can convert them to ttf then those font can at least be registered at the Canvas. The Alex Brush font and my custom font converted from woff to ttf will be displayed like that:

screen shot 2018-05-13 at 00 14 02

Pango throws some warnings:

adflow_1        | (sharp:885): Pango-WARNING **: failed to create cairo scaled font, expect ugly output. the offending font is 'Alex Brush 19.5'
adflow_1        |
adflow_1        | (sharp:885): Pango-WARNING **: font_face status is: file not found
adflow_1        |
adflow_1        | (sharp:885): Pango-WARNING **: scaled_font status is: file not found
adflow_1        |
adflow_1        | (sharp:885): Pango-WARNING **: shaping failure, expect ugly output. shape-engine='PangoFcShapeEngine', font='Alex Brush 19.5', text='|M?q'
adflow_1        |
adflow_1        | (sharp:885): Pango-WARNING **: failed to create cairo scaled font, expect ugly output. the offending font is 'Alex Brush 19.5'
adflow_1        |
adflow_1        | (sharp:885): Pango-WARNING **: font_face status is: file not found
adflow_1        |
adflow_1        | (sharp:885): Pango-WARNING **: scaled_font status is: file not found
adflow_1        |
adflow_1        | (sharp:885): Pango-WARNING **: failed to create cairo scaled font, expect ugly output. the offending font is 'Alex Brush 19.5'
adflow_1        |
adflow_1        | (sharp:885): Pango-WARNING **: font_face status is: file not found
adflow_1        |
adflow_1        | (sharp:885): Pango-WARNING **: scaled_font status is: file not found
adflow_1        |
adflow_1        | (sharp:885): Pango-WARNING **: failed to create cairo scaled font, expect ugly output. the offending font is 'Alex Brush 19.5'
adflow_1        |
adflow_1        | (sharp:885): Pango-WARNING **: font_face status is: file not found
adflow_1        |
adflow_1        | (sharp:885): Pango-WARNING **: scaled_font status is: file not found

Don't know where 'Alex Brush 19.5' is coming from tho. I register the Alex Brush font like registerFont('/tmp/tmp-813sfOjbd429Fy1Alex Brush2_normal_NORMAL.ttf', { family: 'Alex Brush2', weight: 400, style: 'normal' })

Interestingly, official Google fonts downloaded locally work perfectly fine. Mhh. Still investigating.

marcj commented 6 years ago

Ok, I got it. The last missing piece was a permission issue of the font. I had not closed the FD of the ttf file before registering the font at the Canvas object.

Thanks again @chearon!

chearon commented 6 years ago

Glad you figured it out. The 19.5 is just the size in points, I think - can't remember if we do any px conversions to get that right now. So the original issue was with the WOFF format, or the file descriptor or both?

marcj commented 6 years ago

Woff/TTF work for me now on Ubuntu and on Alpine (using the packages from apk, not manually compiled). Fonts are a bit lighter than the Chrome Canvas, but I guess that's rather a Chrome rendering/antialiasing issue, they look in node canvas better. The actual error message of registerFont is a bit misleading, under the hood it didn't get access to the file or read a zero size file. Cost me 8hours. I could provide a PR that can try to improve those errors, but my c skills are very rusty.

chearon commented 6 years ago

Doesn't need to be in C actually - it just needs to do an fs.existsSync after the realpath here. If you still want to make a PR I'll merge it.

woshidamaomao commented 6 years ago

I hava the same problem.

maxpaj commented 6 years ago

I'm getting a weird thing.

registerFont works fine when I build my Docker image on my dev machine (Linux 4.15.0-36-generic 16.04.1 Ubuntu). But after building my image with AWS CodeBuild or on my EC2 machine (Linux 4.4.0-1067-aws Ubuntu), when running the container, registerFont fails work and produces the same error message ("Could not parse font file"). I've tried this with multiple font files.

EDIT: I opted for not using registerFont. Since I'm only using one font, this works for me:

I added this to my Dockerfile: RUN apk add --no-cache ttf-freefont fontconfig

Then used "FreeFont" as my font.

Drelaky commented 3 years ago

Hello, that would be a problem for me too! Here's a video to tell me exactly what isn't working: https://www.youtube.com/watch?v=Qqrp0p_PGaE os: win10 64bit

EXA-Hub commented 2 years ago

Error: Could not parse font file

BrentFarese commented 2 years ago

Woff/TTF work for me now on Ubuntu and on Alpine (using the packages from apk, not manually compiled). Fonts are a bit lighter than the Chrome Canvas, but I guess that's rather a Chrome rendering/antialiasing issue, they look in node canvas better. The actual error message of registerFont is a bit misleading, under the hood it didn't get access to the file or read a zero size file. Cost me 8hours. I could provide a PR that can try to improve those errors, but my c skills are very rusty.

@marcj or @chearon this is some years later but I've been struggling with a similar error using Node Alpine inside Docker for about 5 hours. How did you get it so registerFont correctly located your font file(s)? Some sample code we are running right now and our Dockerfile is below. If you see anything off, any help would be great. Thanks!

We register font(s) like this:

const nodePath = path.resolve(__dirname, `../fonts/CourierNew.ttf`);

registerFont(nodePath, { family: 'CourierNew', style: 'normal', weight: 'normal' });

Dockerfile:

FROM node:16.13-alpine

WORKDIR /home/app

RUN apk --no-cache add \
     curl \
     libreoffice \
# Installs below are for node-canvas.
     build-base \
     g++ \
     cairo-dev \
     jpeg-dev \
     pango \
     pango-dev \
     giflib-dev \
     imagemagick \
     freetype \
     fontconfig

COPY package.json \
     lerna.json \
     .eslintrc \
     yarn.lock \
     .env \
     tsconfig.json \
     /home/app/

COPY packages/common/ \
     /home/app/packages/common/

COPY packages/server/ \
     /home/app/packages/server/

COPY patches \
    /home/app/patches

RUN yarn install --frozen-lockfile && \
    yarn build

WORKDIR /home/app/packages/server

RUN yarn test:unit

# Remove env file.
WORKDIR /home/app
RUN rm .env
WORKDIR /home/app/packages/server

CMD ["yarn", "start"]
marcj commented 2 years ago

I'd double check if the file really exists in the docker container and has actual valid data in it, maybe using tools like "file" or simply "cat", maybe the font is even corrupt. Checking that first eliminates obvious errors first.

BrentFarese commented 2 years ago

Thank you, we ended up switching Node distros for a number of reasons and were able to remove the error. But we are having problems with measureText and our custom fonts so there's another issue! Appreciate it.

ptabor commented 1 year ago

TLDR: To make registerFont work on alpine, make sure that glibc-iconv is installed:


Lets create a test file to reproduce the problm: t.js:

const { registerFont} = require('canvas')
const path = require('path')
registerFont(path.join(__dirname, "..", "ui", "static", "fonts", 'Inter-Regular.woff'), {
    family: 'Inter',
});

Running under strace: strace node t.js:

...
openat(AT_FDCWD, "[redacted]/src/ui/static/fonts/Inter-Regular.woff", O_RDONLY) = 17
...
openat(AT_FDCWD, "/usr/lib/gconv/gconv-modules.cache", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/gconv/gconv-modules", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/gconv/gconv-modules.d", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = -1 ENOENT (No such file or directory)
futex(0xffffa5b3167c, FUTEX_WAKE_PRIVATE, 2147483647) = 0
openat(AT_FDCWD, "/usr/lib/charset.alias", O_RDONLY) = -1 ENOENT (No such file or directory)
...
write(2, "... return Canvas._registerFont(fs.realpathSync(src), fontFace)
                ^
Error: Could not parse font file
    at registerFont ([redacted]/node_modules/.aspect_rules_js/canvas@2.11.2/node_modules/canvas/index.js:48:17)
    at Object.<anonymous> ([redacted]/src/nunode/t.js:3:1)
    at Module._compile (node:internal/modules/cjs/loader:1256:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Module.load (node:internal/modules/cjs/loader:1119:32)
    at Module._load (node:internal/modules/cjs/loader:960:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:86:12)
    at node:internal/main/run_main_module:23:47
) = 1036

After instalation of glibc-iconv, works like a charm.

jlsubia404 commented 8 months ago

I have the same issue on Wolfi linux distribution.

OS detail:

ID=wolfi NAME="Wolfi" PRETTY_NAME="Wolfi" VERSION_ID="20230201" HOME_URL="https://wolfi.dev"

Stack trace

`/app/node_modules/canvas/index.js:48 return Canvas._registerFont(fs.realpathSync(src), fontFace) ^

Error: Could not parse font file at registerFont (/app/node_modules/canvas/index.js:48:17)`

I have this command on my Dockerfile RUN apk add --no-cache \ libuuid \ build-base \ glibc \ libpng \ libpng-dev \ libjpeg-turbo-dev \ pango-dev \ cairo-dev \ giflib-dev \ python3

I don't know if I have to install other libraries. Could anyone help me please?