@@ -39,15 +39,65 @@ function style_name(x::FTFont)
39
39
lowercase (x. style_name)
40
40
end
41
41
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)
43
72
fname = family_name (face)
44
73
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
51
101
end
52
102
53
103
function try_load (fpath)
@@ -59,26 +109,29 @@ function try_load(fpath)
59
109
end
60
110
61
111
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 = " "
64
116
)
65
117
font_folders = copy (fontpaths ())
66
- normalized_name = family_name (name)
118
+ # normalized_name = family_name(name)
67
119
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}[]
69
123
for folder in font_folders
70
124
for font in readdir (folder)
71
125
fpath = joinpath (folder, font)
72
126
face = try_load (fpath)
73
127
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
79
131
push! (candidates, face => score)
80
132
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)
82
135
end
83
136
end
84
137
end
0 commit comments