Skip to content

Commit 1f240ee

Browse files
authored
Merge pull request #32 from JuliaGraphics/jkrumbiegel-jk/find-fonts-by-name
Jkrumbiegel jk/find fonts by name
2 parents 3704b48 + dbe0d40 commit 1f240ee

File tree

1 file changed

+70
-17
lines changed

1 file changed

+70
-17
lines changed

src/findfonts.jl

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,65 @@ function style_name(x::FTFont)
3939
lowercase(x.style_name)
4040
end
4141

42-
function match_font(face::FTFont, name, italic, bold)
42+
"""
43+
Match a font using the user-specified search string, by increasing the score
44+
for each part that appears in the font family + style name, and decreasing it
45+
for each part that doesn't. The function also prefers shorter font names when
46+
encountering similar scores.
47+
48+
49+
Example:
50+
51+
If we had only four fonts:
52+
- Helvetica
53+
- Helvetica Neue
54+
- Helvetica Neue Light
55+
- Times New Roman
56+
57+
Then this is how this function would match different search strings:
58+
- "helvetica" => Helvetica
59+
- "helv" => Helvetica
60+
- "HeLvEtIcA" => Helvetica
61+
- "helvetica neue" => Helvetica Neue
62+
- "tica eue" => Helvetica Neue
63+
- "helvetica light" => Helvetica Neue Light
64+
- "light" => Helvetica Neue Light
65+
- "helvetica bold" => Helvetica
66+
- "helvetica neue bold" => Helvetica Neue
67+
- "times" => Times New Roman
68+
- "times new roman" => Times New Roman
69+
- "arial" => no match
70+
"""
71+
function match_font(face::FTFont, searchparts)
4372
fname = family_name(face)
4473
sname = style_name(face)
45-
italic = italic == (sname == "italic")
46-
bold = bold == (sname == "bold")
47-
perfect_match = (fname == name) && italic && bold
48-
fuzzy_match = occursin(name, fname)
49-
score = fuzzy_match + bold + italic
50-
return perfect_match, fuzzy_match, score
74+
# Regular should get selected / full match if we dont specificy any styling!
75+
full_name = if sname == "regular"
76+
"$fname"
77+
else
78+
"$fname $sname"
79+
end
80+
full_name == "" && return 0
81+
# count letters of parts that occurred in the font name positively and those that didn't negatively.
82+
# we assume that the user knows at least parts of the name and doesn't misspell them
83+
# but they might not know the exact name, especially for long font names, or they
84+
# might simply not want to be forced to spell it out completely.
85+
# therefore we let each part we can find count towards a font, and each that
86+
# doesn't match against it, therefore rejecting fonts that mismatch more parts
87+
# than they match. this heuristic should be good enough to provide a hassle-free
88+
# font selection experience where most spellings that are expected to work, work.
89+
match_score = sum(map(part -> (2 * occursin(part, full_name) - 1) * length(part), searchparts))
90+
# give shorter font names that matched equally well a higher score after the decimal point.
91+
# this should usually pick the "standard" variant of a font as long as it
92+
# doesn't have a special identifier like "regular", "roman", "book", etc.
93+
# to be fair, with these fonts the old fontconfig method also often fails because
94+
# it's not clearly defined what the most normal version is for the user.
95+
# it's therefore better to just have them specify these parts of the name that
96+
# they think are important. this is especially important for attributes that
97+
# fall outside of the standard italic / bold distinction like "condensed",
98+
# "semibold", "oblique", etc.
99+
final_score = match_score + (1.0 / length(full_name))
100+
return final_score
51101
end
52102

53103
function try_load(fpath)
@@ -59,26 +109,29 @@ function try_load(fpath)
59109
end
60110

61111
function findfont(
62-
name::String;
63-
italic::Bool=false, bold::Bool=false, additional_fonts::String=""
112+
searchstring::String;
113+
italic::Bool=false, # this is unused in the new implementation
114+
bold::Bool=false, # and this as well
115+
additional_fonts::String=""
64116
)
65117
font_folders = copy(fontpaths())
66-
normalized_name = family_name(name)
118+
# normalized_name = family_name(name)
67119
isempty(additional_fonts) || pushfirst!(font_folders, additional_fonts)
68-
candidates = Pair{FTFont, Int}[]
120+
# \W splits at all groups of non-word characters (like space, -, ., etc)
121+
searchparts = unique(split(lowercase(searchstring), r"\W+", keepempty=false))
122+
candidates = Pair{FTFont, Float64}[]
69123
for folder in font_folders
70124
for font in readdir(folder)
71125
fpath = joinpath(folder, font)
72126
face = try_load(fpath)
73127
face === nothing && continue
74-
perfect_match, fuzzy_match, score = match_font(
75-
face, normalized_name, italic, bold
76-
)
77-
perfect_match && return face
78-
if fuzzy_match
128+
score = match_font(face, searchparts)
129+
# only take results with net positive character matches into account
130+
if floor(score) > 0
79131
push!(candidates, face => score)
80132
else
81-
finalize(face) # help gc a bit!
133+
# help gc a bit! Otherwise, this won't end well with the font keeping tons of open files
134+
finalize(face)
82135
end
83136
end
84137
end

0 commit comments

Comments
 (0)