Best way to locate and load a font?

After filing

I have been looking for the best way to locate and load a font.
(initially for DynamicGrids.jl, which is otherwise super fast, so it mattered, but the question should be of wider interest).

fontconfig is about 2000 times faster to locate the right font:

using FreeTypeAbstraction
using Fontconfig

font_file(pattern::Fontconfig.Pattern) = Fontconfig.format(Fontconfig.match(pattern), "%{file}")
font_file(str::String) = font_file(Fontconfig.Pattern(str))

# str is either a direct path, or a fontconfig spec such as "cantarell:bold"
function load_font(str::String) 
    path = isfile(str) ? str : font_file(str)
    return FreeTypeAbstraction.FTFont(path)
end

julia> load_font("cantarell:bold")
FTFont (family = Cantarell, style = Bold)

julia> using BenchmarkTools
julia> @btime (load_font("cantarell") |> finalize)  # finalize to avoid "too many open files" error
  1.246 ms (15 allocations: 2.62 KiB)

While

julia> using FreeTypeAbstraction
julia> using BenchmarkTools
julia> @btime FreeTypeAbstraction.findfont("cantarell")
  2.321 s (250478 allocations: 24.99 MiB)
FTFont (family = Cantarell, style = Regular)

Currently Fontconfig.jl and FreeTypeAbstraction.jl are independent, which seems good.

Adding to FreeTypeAbstraction.findfont a cache similar to what Makie does might make sense.
But a one time full scan overhead will remain,
unless a font properties cache is added.
But that looks like reinventing fontconfig, and Fontconfig.jl looks good now.

Did I overlook a package that gather both functionalities ?
(Found nothing, either here on discourse, on stackoverflow, or on juliapackages)

Or maybe is there an unregistered package in preparation ?
If not, would it be interesting ?

1 Like

I think integrating these packages by depending on Fontconfig.jl is a good idea for FreeTypeAbstraction, and probably anywhere else fonts are used. Im not sure if another package is necessary, but fixing the ecosystem from the bottom up so there a no hidden performance hits is the best approach, especially for people like me who know nothing about fonts.

Probably no-one has got around to this yet, as you dont really notice the problems on a system with very few fonts.

1 Like

The reason I wrote the findfont function how it currently is, was that I found it very difficult to choose the fonts I wanted in matplotlib, which also uses fontconfig I believe. It was difficult to select the font with the exact weight I wanted whenever it was more complicated than bold/italic. Many fonts have multiple weights such as semibold, roman, thin, light, etc. So I chose to use a function that goes just by the name (almost no user knows what weight 500 means for a font), but is a bit relaxed in what it matches, so that fonts with complicated names can be specified partially.

So can Fontconfig be used in a similar way? If we just want the fast matching, maybe we can just write a cache file ourselves with all the names.

1 Like

Thanks for the intel.

fontconfig also monitors for disk changes and updates its cache accordingly.
But a manual update_cache() could be fine too
(it is really necessary only when one modifies the fonts, so no big deal)

More important indeed, fontconfig is tolerant to spaces and case:

load_font("SourceCode Pro")
FTFont (family = Source Code Pro, style = Regular)
load_font("sourcecodepro")
FTFont (family = Source Code Pro, style = Regular)

but falls back to a generic font if the name is incomplete:

load_font("SourceCode")
FTFont (family = FreeMono, style = Regular)

Many fonts have multiple weights such as semibold, roman, thin, light, etc. So I chose to use a function that goes just by the name

Indeed, fontconfig is less permissive, one has to separate the weight field with a colon (:).

# findfont is nice and permissive
julia> FreeTypeAbstraction.findfont("helvetica bold")
FTFont (family = Helvetica, style = Bold)

# Because ':' is missing, fontconfig would be looking for "bold"
# in the font name (not the filename), and fall back to a generic font.
julia> load_font("helvetica bold")  
FTFont (family = FreeMono, style = Regular)

# with the correct separator, fontconfig works fine
julia> load_font("helvetica:bold")
FTFont (family = Helvetica, style = Bold)

# and falls back to regular style if the requested weight is not found
julia> load_font("helvetica:light")
FTFont (family = Helvetica, style = Regular)

# just like findfont does
julia> FreeTypeAbstraction.findfont("helvetica light")
FTFont (family = Helvetica, style = Regular)

For others, the FreeTypeAbstraction selection algorithm can be found in match_font.

Bottom line: tough choice; I like your design skills and glad you are the ones to decide :slightly_smiling_face:.

Ha I also read about the colon functionality of fontconfig today, I was not aware that it had this. Maybe it was not documented in matplotlib or something. Anyway, with that functionality I’d probably be fine with using fontconfig if the other benefits are deemed important enough. On the other hand, I kind of like the robustness against partial names in my implementation. I chose that because I often used the font β€œHelvetica Lt Std Light” and the lt Std is annoying to type, and I sometimes couldn’t remember the exact abbreviations.

1 Like

I guess the other option here is doing whatever fontconfig does, but in Julia? If someone want to write that? Then the syntax can be whatever we like.

I often used the font β€œHelvetica Lt Std Light” and the lt Std is annoying to type

The standard solution with fontconfig is font replacement.
Pro: this would work for all applications based on fontconfig.
Con: need manual configuration (clean, but not as user-friendly as your solution).