gonum / plot

A repository for plotting and visualizing data
BSD 3-Clause "New" or "Revised" License
2.74k stars 203 forks source link

[Question] How to use the latest FillString() and AddFont()? #693

Closed sgon00 closed 3 years ago

sgon00 commented 3 years ago

Hi, I am new to plot. I used some old version in the past and it's working very well. Today, when I try to upgrade to the latest version (using go module), There are two problems in the code. I think it might because the API methods have been upgraded.

Some of my code:

require (
    github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
    gonum.org/v1/plot v0.9.0
)

import (
        "gonum.org/v1/plot/vg"
    "gonum.org/v1/plot/vg/vgimg"
)
c := vgimg.New(diagonal, diagonal)
fs, _ := vg.MakeFont(fontName, vg.Inch*vg.Length(fontInchSize))
c.FillString(fs, vg.Point{X: 0, Y: offset}, markText)

The error shows: cannot use fs (variable of type vg.Font) as font.Face value in argument to c.FillString.

font, err := truetype.Parse(ttfBytes)
vg.AddFont(fontName, font)

The error shows: cannot use font (variable of type *truetype.Font) as *sfnt.Font value in argument to vg.AddFont

I don't know how to use the latest API. How to convert fs to font.Face? I tried fs.FontFace(96), but it didn't work. How to add truetype.Font to vg.AddFont? How to convert truetype.Font to sfnt.Font? Can anyone plese help?

Thank you very much.

kortschak commented 3 years ago

To get the font face, use fnt := hdlr.Fonts.Lookup(sty.Font, sty.Font.Size) where hdlr is a text.Handler and sty is your text style.

Rather than using truetype.Parse(ttfBytes), try sfnt.Parse(ttfBytes)

sgon00 commented 3 years ago

@kortschak Thank you very much for your quick reply. The sfnt.Parse(ttfBytes) solution is clear. But I still don't know how to get font face. I am new to font coding. I tried to google some example code online, but failed to find any. Can you please show me a complete example how to get font face if that is not too much? OR show me some complete example code URLs? Thank you very much.

sgon00 commented 3 years ago

PS: I tried something stupid:

face, _ := opentype.NewFace(font, nil)
c.FillString(face, vg.Point{X: 0, Y: offset}, markText)

It has error message: cannot use face (variable of type font.Face) as font.Face value in argument to c.FillString I am very confused. I can not use font.Face as font.Face?

kortschak commented 3 years ago

Yeah, no worries. The API is new, I think we may need to have some more examples for how to use it effectively.

This is a basic use.

fnt := plot.DefaultTextHandler.Cache.Lookup(plot.DefaultFont, size)

If you look in the liberation.go file, you can see the fonts that are provided in the repo and also how the collection is constructed.

kortschak commented 3 years ago

That's a pretty funny error. There are two font.Face types in your program, opentype.NewFace returns this and FillString wants this. Unfortunately, there are only so many names, and sometime we have to share.

BTW It's easier to help if you provide full code. You can paste the code at https://play.golang.org.

sbinet commented 3 years ago

Here's an example of using a 3rd-party font:

https://pkg.go.dev/gonum.org/v1/plot@v0.9.0/vg#example-package-AddFont

sgon00 commented 3 years ago

I am very sorry that I still can not make 0.9.0 work.

Some of my current code:

fontName := "ABC"
ttfBytes, err := ioutil.ReadFile("/System/Library/Fonts/Menlo.ttc")
font, _ := sfnt.Parse(ttfBytes)
vg.AddFont(fontName, font)
fs, _ := vg.MakeFont(fontName, vg.Inch*vg.Length(fontInchSize))
width := fs.Width(markText)
...
...
fnt := plot.DefaultTextHandler.Cache().Lookup(plot.DefaultFont, fontInchSize)
c.FillString(fnt, vg.Point{X: 0, Y: offset}, markText)

1.

fs.Width line will give me invalid memory address or nil pointer dereference error. It worked before when I use the old plot version. If I change the fontName to the built-in font name such as "Courier", the error will be gone. But I want to use the system font instead of FontMap list.

2.

In the old version, I simply use c.FillString(fs, ...). But in the new version, If I use fnt := plot.DefaultTextHandler.Cache().Lookup(plot.DefaultFont, fontInchSize) and then c.FillString(fnt, ...), How can I use the custom ttc font?

I have read the example link provided by @sbinet. But I am still very lost. I have the variable fs created by vg.MakeFont. Why can't I use it somehow in c.FillString like before?

Thank you very much for your help.

sbinet commented 3 years ago

as always in Go, please check the returned error values (that's why you get a nil pointer as fs.)

the new font system was put in place to allow for a much finer control of fonts (and to use golang.org/x/image/font/sfnt instead of freetype)

as hinted in the example, the way to add a new font is to populate the "font cache":

    mincho := font.Font{Typeface: "Mincho"}
    font.DefaultCache.Add([]font.Face{
        {
            Font: mincho,
            Face: fontTTF,
        },
    })
    if !font.DefaultCache.Has(mincho) {
        log.Fatalf("no font %q!", mincho.Typeface)
    }

ie: one adds a plot/font.Face value to the default font cache, which can then be used by the whole set of gonum/plot packages.

one can use that plot/font.Face value with the plot/vg.Canvas.FillString method, as before:

face := font.Face{
    Font: mincho,
    Face: fontTTF,
}
c.FillString(face, vg.Point{5,5}, "some text")
sgon00 commented 3 years ago

Thank you both very much for the help.

Finally, I made it work.

Sorry that I made one mistake above that I should NOT use ttc font collection file, instead, I should use ttf file. I should check the error in the line sfnt.Parse which will produce a reasonable error message instead of a nil pointer error. This is my bad.

Btw, the new font system is way more complicated than before.

In the past, I just need to add the font once in vg.AddFont(). That's all. I can then use the font in all methods such as vg.MakeFont() and vgimg.Canvas.FillString(),

But in the new version, I have to add the fonts twice. One by vg.AddFont() so that I can call vg.MakeFont(). Another by creating a font.Face, so that I can use vgimg.Canvas.FillString().

Anyway, thank you very much again and closing this question.

sbinet commented 3 years ago

I believe that, as the example shown above might indicate, one doesn't need to use both vg.AddFont and the "font cache" to add a new 3rd-party font to the gonum/plot font system.

could you please open another issue if that's not the case? thanks.

Note to self: probably consider a migration path and delete vg.AddFont, vg.MakeFont and vg.Font altogether (which were IIRC kept only for embedding fonts in PDF documents while there was no way to get back at the raw []bytes)

sgon00 commented 3 years ago

@sbinet Thanks a lot for your reply. I was not saying that the font needs to add to "font cache" too. I mean the ttfBytes need to pass to both vg.AddFont and font.Face object. Maybe I am concerning too much. I just feel weird that I need to pass the same ttfBytes to two different places. I used some of the codes from this gist: https://gist.github.com/linw1995/41e103f29e7dd97679c577a6f4830be8 Cheers.

sbinet commented 3 years ago

and what I am saying is that no call to vg.AddFont (nor vg.MakeFont) should be needed. so the raw []byte representing the TTF font needs only to be passed once to the font cache.

one can then retrieve a font face (with the expected size) via plot/font.Cache.Lookup as Dan wrote earlier.

earlier on, I wrote:

face := font.Face{
    Font: mincho,
    Face: fontTTF,
}
c.FillString(face, vg.Point{5,5}, "some text")

but this could as well be written like:

face := font.DefaultCache.Lookup(font.Font{Typeface: "Mincho"}, 42)
c.FillString(face, vg.Point{5,5}, "some text")
sgon00 commented 3 years ago

@sbinet I posted the gist code that I studied from. (I am not using the same code, but they are similar). From the gist code, you can see that it needs to calculate the following:

    fontStyle, _ := vg.MakeFont("Courier", vg.Inch*0.7)
    markTextWidth := fontStyle.Width(markText)
    unitText := markText
    for markTextWidth <= diagonal {
        markText += " " + unitText
        markTextWidth = fontStyle.Width(markText)
    }
        .......
    lineHeight := fontStyle.Extents().Height * 1

I don't know how to achieve the same if I don't call vg.AddFont and vg.MakeFont. Thanks a lot.

sbinet commented 3 years ago

The 'font.Face' type has the methods you are looking for:

sgon00 commented 3 years ago

@sbinet Thank you very much. I will try to replace the old code with font.Face methods then. :)