Skip to content

Commit 9f858fa

Browse files
committed
simplify algorithm a lot, now only one best font is kept open at a time
1 parent d1aab50 commit 9f858fa

File tree

1 file changed

+42
-85
lines changed

1 file changed

+42
-85
lines changed

src/findfonts.jl

Lines changed: 42 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ function style_name(x::FTFont)
3737
lowercase(x.style_name)
3838
end
3939

40+
const REGULAR_STYLES = ("regular", "normal", "medium", "standard", "roman")
41+
4042

4143
"""
4244
Match a font using the user-specified search string. Each part of the search string
@@ -70,34 +72,37 @@ Then this is how this function would match different search strings:
7072
- "times" => no match
7173
- "arial" => no match
7274
"""
73-
function match_font_by_family_and_style(face::FTFont, searchparts)::Tuple{Int, Int}
75+
function match_font(face::FTFont, searchparts)::Tuple{Int, Int, Bool, Int}
7476

7577
fname = family_name(face)
78+
sname = style_name(face)
79+
is_regular_style = any(occursin(s, sname) for s in REGULAR_STYLES)
7680

77-
family_matches = any(occursin(part, fname) for part in searchparts)
81+
fontlength_penalty = -(length(fname) + length(sname))
7882

79-
# return early if family name doesn't have a match
80-
family_matches || return (0, 0)
8183

82-
# now enhance the score with style information
84+
family_matches = any(occursin(part, fname) for part in searchparts)
8385

84-
remaining_parts = filter(part -> !occursin(part, fname), searchparts)
86+
# return early if family name doesn't have a match
87+
family_matches || return (0, 0, is_regular_style, fontlength_penalty)
8588

8689
family_score = sum(length(part) for part in searchparts if occursin(part, fname))
8790

88-
# return early if no parts remain for style scoring
89-
isempty(remaining_parts) && return (family_score, 0)
90-
91-
sname = style_name(face)
91+
# now enhance the score with style information
92+
remaining_parts = filter(part -> !occursin(part, fname), searchparts)
9293

93-
style_name_matches = any(occursin(part, sname) for part in remaining_parts)
94+
if isempty(remaining_parts)
95+
return (family_score, 0, is_regular_style, fontlength_penalty)
96+
end
9497

95-
# return early if no remaining part matches the style name
96-
style_name_matches || return (family_score, 0)
98+
# check if any parts match the style name, otherwise return early
99+
if !any(occursin(part, sname) for part in remaining_parts)
100+
return (family_score, 0, is_regular_style, fontlength_penalty)
101+
end
97102

98103
style_score = sum(length(part) for part in remaining_parts if occursin(part, sname))
99104

100-
(family_score, style_score)
105+
(family_score, style_score, is_regular_style, fontlength_penalty)
101106
end
102107

103108
function try_load(fpath)
@@ -110,6 +115,7 @@ end
110115

111116
fontname(ft::FTFont) = "$(family_name(ft)) $(style_name(ft))"
112117

118+
113119
function findfont(
114120
searchstring::String;
115121
italic::Bool=false, # this is unused in the new implementation
@@ -125,89 +131,40 @@ function findfont(
125131

126132
candidates = Pair{FTFont, Tuple{Int, Int}}[]
127133

134+
best_score_so_far = (0, 0, false, typemin(Int))
135+
best_font = nothing
136+
128137
for folder in font_folders
129138
for font in readdir(folder)
130139
fpath = joinpath(folder, font)
131140
face = try_load(fpath)
132141
face === nothing && continue
133-
family_score, style_score = match_font_by_family_and_style(face, searchparts)
134-
# only take results with net positive character matches into account
135-
if family_score > 0
136-
push!(candidates, face => (family_score, style_score))
137-
else
138-
# help gc a bit! Otherwise, this won't end well with the font keeping tons of open files
139-
finalize(face)
140-
end
141-
end
142-
end
143142

144-
isempty(candidates) && return nothing
143+
score = match_font(face, searchparts)
145144

146-
# sort by family score then style score, highest first
147-
sort!(candidates; by=x -> x[2], rev = true)
145+
# we can compare all four tuple elements of the score at once
146+
# in order of importance:
148147

148+
# 1. number of family match characters
149+
# 2. number of style match characters
150+
# 3. is font a "regular" style variant?
151+
# 4. the negative length of the font name, the shorter the better
149152

150-
# best score for comparison
151-
highscore = candidates[1][2] # tuple{int, int}
153+
family_match_score = score[1]
154+
if family_match_score > 0 && score > best_score_so_far
155+
# finalize previous best font to close the font file
156+
if !isnothing(best_font)
157+
finalize(best_font)
158+
end
152159

153-
# remove all candidates that have lesser scores than the first one
154-
for i in 2:length(candidates)
155-
if candidates[i][2][1] < highscore[1] || candidates[i][2][2] < highscore[2]
156-
# remove fonts from back to front and finalize them to close
157-
# their files
158-
for j in length(candidates):-1:i
159-
to_remove = pop!(candidates)[1]
160-
finalize(to_remove)
161-
end
162-
# there will be no other i's we need to check after this
163-
break
164-
end
165-
end
166-
167-
# return early if only one font remains
168-
length(candidates) == 1 && return candidates[1][1]
169-
170-
# there is still more than one candidate
171-
# all candidates have the same family and style score
172-
173-
# prefer regular fonts among the remaining options
174-
regular_styles = ("regular", "normal", "medium", "standard", "roman")
175-
regular_matches = filter(candidates) do c
176-
any(occursin(style, style_name(c[1])) for style in regular_styles)
177-
end
178-
179-
# if any fonts match a regular-type style name, choose the overall shortest
180-
if !isempty(regular_matches)
181-
182-
sort!(regular_matches,
183-
by = c -> length(fontname(c[1])))
184-
185-
shortest = regular_matches[1][1]
186-
187-
# close all non-used font files
188-
for c in candidates
189-
if c[1] !== shortest
190-
finalize(c)
160+
# new candidate
161+
best_font = face
162+
best_score_so_far = score
163+
else
164+
finalize(face)
191165
end
192166
end
193-
194-
return shortest
195-
end
196-
197-
# we didn't find any font with a "regular" name
198-
# as a last heuristic, we just choose the font with the shortest name
199-
200-
sort!(candidates,
201-
by = c -> length(fontname(c[1])))
202-
203-
shortest = candidates[1][1]
204-
205-
# close all non-used font files
206-
for c in candidates
207-
if c[1] !== shortest
208-
finalize(c)
209-
end
210167
end
211168

212-
shortest
169+
best_font
213170
end

0 commit comments

Comments
 (0)