-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
font.lua
450 lines (413 loc) · 17.9 KB
/
font.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
--[[--
Font module.
]]
local FontList = require("fontlist")
local Freetype = require("ffi/freetype")
local Screen = require("device").screen
local logger = require("logger")
local util = require("util")
-- Known regular (and italic) fonts with an available bold font file
local _bold_font_variant = {}
_bold_font_variant["NotoSans-Regular.ttf"] = "NotoSans-Bold.ttf"
_bold_font_variant["NotoSans-Italic.ttf"] = "NotoSans-BoldItalic.ttf"
_bold_font_variant["NotoSansArabicUI-Regular.ttf"] = "NotoSansArabicUI-Bold.ttf"
_bold_font_variant["NotoSerif-Regular.ttf"] = "NotoSerif-Bold.ttf"
_bold_font_variant["NotoSerif-Italic.ttf"] = "NotoSerif-BoldItalic.ttf"
-- Build the reverse mapping, so we can know a font is bold
local _regular_font_variant = {}
for regular, bold in pairs(_bold_font_variant) do
_regular_font_variant[bold] = regular
end
local Font = {
-- Make these available in the Font object, so other code
-- can complete them if needed.
bold_font_variant = _bold_font_variant,
regular_font_variant = _regular_font_variant,
-- Allow globally not promoting fonts to their bold variants
-- (and use thiner and narrower synthetized bold instead).
use_bold_font_for_bold = G_reader_settings:nilOrTrue("use_bold_font_for_bold"),
-- Widgets can provide "bold = Font.FORCE_SYNTHETIZED_BOLD" instead
-- of "bold = true" to explicitely request synthetized bold, which,
-- with XText, makes a bold string the same width as itself non-bold.
FORCE_SYNTHETIZED_BOLD = "FORCE_SYNTHETIZED_BOLD",
fontmap = {
-- default font for menu contents
cfont = "NotoSans-Regular.ttf",
-- default font for title
--tfont = "NimbusSanL-BoldItal.cff",
tfont = "NotoSans-Bold.ttf",
smalltfont = "NotoSans-Bold.ttf",
x_smalltfont = "NotoSans-Bold.ttf",
-- default font for footer
ffont = "NotoSans-Regular.ttf",
smallffont = "NotoSans-Regular.ttf",
largeffont = "NotoSans-Regular.ttf",
-- default font for reading position info
rifont = "NotoSans-Regular.ttf",
-- default font for pagination display
pgfont = "NotoSans-Regular.ttf",
-- selectmenu: font for item shortcut
scfont = "DroidSansMono.ttf",
-- help page: font for displaying keys
hpkfont = "DroidSansMono.ttf",
-- font for displaying help messages
hfont = "NotoSans-Regular.ttf",
-- font for displaying input content
-- we have to use mono here for better distance controlling
infont = "DroidSansMono.ttf",
-- small mono font for displaying code
smallinfont = "DroidSansMono.ttf",
-- font for info messages
infofont = "NotoSans-Regular.ttf",
-- small font for info messages
smallinfofont = "NotoSans-Regular.ttf",
-- small bold font for info messages
smallinfofontbold = "NotoSans-Bold.ttf",
-- extra small font for info messages
x_smallinfofont = "NotoSans-Regular.ttf",
-- extra extra small font for info messages
xx_smallinfofont = "NotoSans-Regular.ttf",
},
sizemap = {
cfont = 24,
tfont = 26,
smalltfont = 24,
x_smalltfont = 22,
ffont = 20,
smallffont = 15,
largeffont = 25,
pgfont = 20,
scfont = 20,
rifont = 16,
hpkfont = 20,
hfont = 24,
infont = 22,
smallinfont = 16,
infofont = 24,
smallinfofont = 22,
smallinfofontbold = 22,
x_smallinfofont = 20,
xx_smallinfofont = 18,
},
-- This fallback fonts list should only contain
-- regular weight (non bold) font files.
fallbacks = {
[1] = "NotoSans-Regular.ttf",
[2] = "NotoSansCJKsc-Regular.otf",
[3] = "NotoSansArabicUI-Regular.ttf",
[4] = "NotoSansDevanagariUI-Regular.ttf",
[5] = "NotoSansBengaliUI-Regular.ttf",
[6] = "nerdfonts/symbols.ttf",
[7] = "freefont/FreeSans.ttf",
[8] = "freefont/FreeSerif.ttf",
},
-- Additional fallback fonts are managed by frontend/ui/elements/font_ui_fallbacks.lua
-- Add any after NotoSansCJKsc (because CJKsc has better symbols, and has 'locl' OTF
-- features to support all of SC, TC, JA and KO that other CJK fonts may not have.)
additional_fallback_insert_indice = 3,
-- Xtext supports up to 15 fallback fonts, but keep some slots free and available for
-- future additions to our hardcoded fallbacks list above, and to not slow down
-- rendering with too many fallback fonts.
additional_fallback_max_nb = 4,
-- face table
faces = {},
}
if G_reader_settings and G_reader_settings:has("font_ui_fallbacks") then
local additional_fallbacks = G_reader_settings:readSetting("font_ui_fallbacks")
for i=#additional_fallbacks, 1, -1 do
table.insert(Font.fallbacks, Font.additional_fallback_insert_indice, additional_fallbacks[i])
end
logger.dbg("updated Font.fallbacks:", Font.fallbacks)
end
-- We don't ship a bold variant for some of our fallback fonts.
-- Allow users themselves to drop a Noto Sans Bold variant of their most used fallbacks,
-- and we will use them if present.
-- Match bold font to fallback by name. We do not use FontInfo name match
-- to allow users more flexibility.
-- Because the hardcoded fallback fonts' paths are their filenames not actual paths,
-- we need to match with filenames rather than paths
local bold_candidates = {} -- key: bold font's name, value: corresponding regular font's path
for _, fallback_font_path in ipairs(Font.fallbacks) do
local _, font_name = util.splitFilePathName(fallback_font_path)
if font_name and not _bold_font_variant[fallback_font_path]
and not _bold_font_variant[font_name]
and font_name:find("-Regular") then
local bold_font_name = font_name:gsub("-Regular", "-Bold", 1, true)
bold_candidates[bold_font_name] = fallback_font_path
end
end
for _, font_path in ipairs(FontList:getFontList()) do
local _, bold_font_name = util.splitFilePathName(font_path)
local fallback_font_path = bold_candidates[bold_font_name]
if bold_font_name and fallback_font_path then
Font.bold_font_variant[fallback_font_path] = font_path
Font.regular_font_variant[font_path] = fallback_font_path
bold_candidates[bold_font_name] = nil
end
if #bold_candidates == 0 then
break
end
end
bold_candidates = nil -- luacheck: ignore
-- Helper functions with explicite names around
-- bold/regular_font_variant tables
function Font:hasBoldVariant(name)
return self.bold_font_variant[name] and true or false
end
function Font:getBoldVariantName(name)
return self.bold_font_variant[name]
end
function Font:isRealBoldFont(name)
return self.regular_font_variant[name] and true or false
end
function Font:getRegularVariantName(name)
return self.regular_font_variant[name] or name
end
-- Synthetized bold strength can be tuned:
-- local bold_strength_factor = 1 -- really too bold
-- local bold_strength_factor = 1/2 -- bold enough
local bold_strength_factor = 3/8 -- as crengine, lighter
-- Add some properties to a face object as needed
local _completeFaceProperties = function(face_obj)
if not face_obj.embolden_half_strength then
-- Cache this value in case we use bold, to avoid recomputation
face_obj.embolden_half_strength = face_obj.ftsize:getEmboldenHalfStrength(bold_strength_factor)
end
end
-- Callback to be used by libkoreader-xtext.so to get Freetype
-- instantiated fallback fonts when needed for shaping text
-- (Beware: any error in this code won't be noticed when this
-- is called from the C module...)
local _getFallbackFont = function(face_obj, num)
if not num or num == 0 then -- return the main font
_completeFaceProperties(face_obj)
return face_obj
end
if not face_obj.fallbacks then
face_obj.fallbacks = {}
end
if face_obj.fallbacks[num] ~= nil then -- (false means: no more fallback font)
return face_obj.fallbacks[num]
end
local next_num = #face_obj.fallbacks + 1
local cur_num = 0
local realname = face_obj.realname
if face_obj.is_real_bold then
-- Get the regular name, to skip it from Font.fallbacks
realname = Font:getRegularVariantName(realname)
end
for index, fontname in pairs(Font.fallbacks) do
if fontname ~= realname then -- Skip base one if among fallbacks
-- If main font is a real bold, or if it's not but we want bold,
-- get the bold variant of the fallback if one exists.
-- But if one exists, use the regular variant as an additional
-- fallback, drawn with synthetized bold (often, bold fonts
-- have less glyphs than their regular counterpart).
if face_obj.is_real_bold or face_obj.wants_bold == true then
-- (not if wants_bold==Font.FORCE_SYNTHETIZED_BOLD)
local bold_variant_name = Font:getBoldVariantName(fontname)
if bold_variant_name then
-- There is a bold variant of that fallback font, that we can use
local fb_face = Font:getFace(bold_variant_name, face_obj.orig_size)
if fb_face ~= nil then -- valid font
cur_num = cur_num + 1
if cur_num == next_num then
_completeFaceProperties(fb_face)
face_obj.fallbacks[next_num] = fb_face
return fb_face
end
-- otherwise, go on with the regular variant
end
end
end
local fb_face = Font:getFace(fontname, face_obj.orig_size)
if fb_face ~= nil then -- valid font
cur_num = cur_num + 1
if cur_num == next_num then
_completeFaceProperties(fb_face)
face_obj.fallbacks[next_num] = fb_face
return fb_face
end
end
end
end
-- no more fallback font
face_obj.fallbacks[next_num] = false
return false
end
--- Gets font face object.
-- @string font
-- @int size optional size
-- @int faceindex optional index of font face in font file
-- @treturn table @{FontFaceObj}
function Font:getFace(font, size, faceindex)
-- default to content font
if not font then font = self.cfont end
if not size then size = self.sizemap[font] end
-- original size before scaling by screen DPI
local orig_size = size
size = Screen:scaleBySize(size)
local realname = self.fontmap[font]
if not realname then
realname = font
end
-- Avoid emboldening already bold fonts
local is_real_bold = self:isRealBoldFont(realname)
-- Make a hash from the realname (many fonts in our fontmap use
-- the same font file: have them share their glyphs cache)
local hash = realname..size
if faceindex then
hash = hash .. "/" .. faceindex
end
local face_obj = self.faces[hash]
if face_obj then
-- Font found
if face_obj.orig_size ~= orig_size then
-- orig_size has changed (which may happen on small orig_size variations
-- mapping to a same final size, but more importantly when geometry
-- or dpi has changed): keep it updated, so code that would re-use
-- it to fetch another font get the current original font size and
-- not one from the past
face_obj.orig_size = orig_size
end
else
-- Build face size if not found
local builtin_font_location = FontList.fontdir.."/"..realname
local ok, ftsize = pcall(Freetype.newFaceSize, builtin_font_location, size, faceindex)
-- Not all fonts are bundled on all platforms because they come with the system.
-- In that case, search through all font folders for the requested font.
if not ok then
local fonts = FontList:getFontList()
local escaped_realname = realname:gsub("[-]", "%%-")
for _k, _v in ipairs(fonts) do
if _v:find(escaped_realname) then
logger.dbg("Found font:", realname, "in", _v)
ok, ftsize = pcall(Freetype.newFaceSize, _v, size, faceindex)
if ok then break end
end
end
end
if not ok then
logger.err("#! Font ", font, " (", realname, ") not supported: ", ftsize)
return nil
end
--- Freetype font face wrapper object
-- @table FontFaceObj
-- @field orig_font font name requested
-- @field size size of the font face (after scaled by screen size)
-- @field orig_size raw size of the font face (before scale)
-- @field ftsize font size object from freetype
-- @field hash hash key for this font face
face_obj = {
orig_font = font,
realname = realname,
size = size,
orig_size = orig_size,
ftsize = ftsize,
hash = hash,
is_real_bold = is_real_bold,
}
self.faces[hash] = face_obj
-- Callback to be used by libkoreader-xtext.so to get Freetype
-- instantiated fallback fonts when needed for shaping text
face_obj.getFallbackFont = function(num)
return _getFallbackFont(face_obj, num)
end
-- Font features, to be given by libkoreader-xtext.so to HarfBuzz.
-- (Could be tweaked by font if needed. Note that NotoSans does not
-- have common ligatures, like for "fi" or "fl", so we won't see
-- them in the UI.)
-- Use HB defaults, and be sure to enable kerning and ligatures
-- (which might be part of HB defaults, or not, not sure).
face_obj.hb_features = { "+kern", "+liga" }
-- If we'd wanted to disable all features that might be enabled
-- by HarfBuzz (see harfbuzz/src/hb-ot-shape.cc, quite unclear
-- what's enabled or not by default):
-- face_obj.hb_features = {
-- "-kern", "-mark", "-mkmk", "-curs", "-locl", "-liga",
-- "-rlig", "-clig", "-ccmp", "-calt", "-rclt", "-rvrn",
-- "-ltra", "-ltrm", "-rtla", "-rtlm", "-frac", "-numr",
-- "-dnom", "-rand", "-trak", "-vert", }
end
return face_obj
end
--- Returns an alternative face instance to be used for measuring
-- and drawing (in most cases, the one provided untouched)
--
-- If 'bold' is true, or if 'face' is a real bold face, we may need to
-- use an alternative instance of the font, with possibly the associated
-- real bold font, and/or with tweaks so fallback fonts are rendered
-- bold too, without affecting the regular 'face'.
-- (This function should only be used by TextWidget and TextBoxWidget.
-- Other widgets should not use it, and neither _getFallbackFont()
-- which will do its own processing.)
--
-- @tparam ui.font.FontFaceObj provided face font face
-- @bool bold whether bold is requested
-- @treturn ui.font.FontFaceObj face face to use for drawing
-- @treturn bool bold adjusted bold properties
function Font:getAdjustedFace(face, bold)
if face.is_real_bold then
-- No adjustment needed: main real bold font will ensure
-- fallback fonts use their associated bold font or
-- get synthetized bold - whether bold is requested or not
-- (Set returned bold to true, to force synthetized bold
-- on fallback fonts with no associated real bold)
-- (Drop bold=FORCE_SYNTHETIZED_BOLD and use 'true' if
-- we were given a real bold font.)
return face, true
end
if not bold then
-- No adjustment needed: regular main font, and regular
-- fallback fonts untouched.
return face, false
end
-- We have bold requested, and a regular/non-bold font.
if not self.use_bold_font_for_bold then
-- If promotion to real bold is not wished, force synth bold
bold = Font.FORCE_SYNTHETIZED_BOLD
end
if bold ~= Font.FORCE_SYNTHETIZED_BOLD then
-- See if a bold font file exists for that regular font.
local bold_variant_name = self:getBoldVariantName(face.realname)
if bold_variant_name then
face = Font:getFace(bold_variant_name, face.orig_size)
-- It has is_real_bold=true: no adjustment needed
return face, true
end
end
-- Only the regular font is available, and bold requested:
-- we'll have synthetized bold - but _getFallbackFont() should
-- build a list of fallback fonts either synthetized, or possibly
-- using the bold variant of a regular fallback font.
-- We don't want to collide with the regular font face_obj.fallbacks
-- so let's make a shallow clone of this face_obj, and have it cached.
-- (Different hash if real bold accepted or not, as the fallback
-- fonts list may then be different.)
local hash = face.hash..(bold == Font.FORCE_SYNTHETIZED_BOLD and "synthbold" or "realbold")
local face_obj = self.faces[hash]
if face_obj then
return face_obj, bold
end
face_obj = {
orig_font = face.orig_font,
realname = face.realname,
size = face.size,
orig_size = face.orig_size,
-- We can keep the same FT object and the same hash in this face_obj
-- (which is only used to identify cached glyphs, that we don't need
-- to distinguish as "bold" is appended when synthetized as bold)
ftsize = face.ftsize,
hash = face.hash,
hb_features = face.hb_features,
is_real_bold = nil,
wants_bold = bold, -- true or Font.FORCE_SYNTHETIZED_BOLD, used
-- to pick the appropritate fallback fonts
}
face_obj.getFallbackFont = function(num)
return _getFallbackFont(face_obj, num)
end
self.faces[hash] = face_obj
return face_obj, bold
end
return Font