From a73bf1afda2568724546f22f3fe3841b31660ec9 Mon Sep 17 00:00:00 2001 From: ezdiy Date: Tue, 20 Oct 2020 06:30:41 +0200 Subject: [PATCH] fontlist: Cache categorized font info (for mupdf) (#6786) Info about each face (l10n, name, family, style etc) is now cached offline, so fonts can be queried ahead of time. Fixes https://github.com/koreader/koreader/issues/6763 --- frontend/apps/reader/modules/readerfont.lua | 7 +- frontend/fontlist.lua | 146 +++++++++++++++++--- frontend/util.lua | 22 +++ 3 files changed, 154 insertions(+), 21 deletions(-) diff --git a/frontend/apps/reader/modules/readerfont.lua b/frontend/apps/reader/modules/readerfont.lua index f3f36a8e1..5909e894a 100644 --- a/frontend/apps/reader/modules/readerfont.lua +++ b/frontend/apps/reader/modules/readerfont.lua @@ -4,6 +4,7 @@ local ConfirmBox = require("ui/widget/confirmbox") local Device = require("device") local Event = require("ui/event") local Font = require("ui/font") +local FontList = require("fontlist") local Input = Device.input local InputContainer = require("ui/widget/container/inputcontainer") local Menu = require("ui/widget/menu") @@ -61,12 +62,17 @@ function ReaderFont:init() -- Font list local face_list = cre.getFontFaces() for k,v in ipairs(face_list) do + local font_filename, font_faceindex = cre.getFontFaceFilenameAndFaceIndex(v) table.insert(self.face_table, { text_func = function() -- defaults are hardcoded in credocument.lua local default_font = G_reader_settings:readSetting("cre_font") or self.ui.document.default_font local fallback_font = G_reader_settings:readSetting("fallback_font") or self.ui.document.fallback_fonts[1] local text = v + if font_filename and font_faceindex then + text = FontList:getLocalizedFontName(font_filename, font_faceindex) or text + end + if v == default_font then text = text .. " ★" end @@ -77,7 +83,6 @@ function ReaderFont:init() end, font_func = function(size) if G_reader_settings:nilOrTrue("font_menu_use_font_face") then - local font_filename, font_faceindex = cre.getFontFaceFilenameAndFaceIndex(v) if font_filename and font_faceindex then return Font:getFace(font_filename, size, font_faceindex) end diff --git a/frontend/fontlist.lua b/frontend/fontlist.lua index b87a8d31e..2cbd7eb22 100644 --- a/frontend/fontlist.lua +++ b/frontend/fontlist.lua @@ -1,8 +1,17 @@ local CanvasContext = require("document/canvascontext") +local DataStorage = require("datastorage") +local dump = require("dump") +local FT = require("ffi/freetype") +local HB = require("ffi/harfbuzz") +local util = require("util") +local logger = require("logger") +local dbg = require("dbg") local FontList = { fontdir = "./fonts", fontlist = {}, + fontinfo = {}, + fontnames = {}, } --[[ @@ -95,37 +104,134 @@ local function getExternalFontDir() end end -local function _readList(target, dir) - -- lfs.dir non-existent directory will give an error, weird! - local ok, iter, dir_obj = pcall(lfs.dir, dir) - if not ok then return end - for f in iter, dir_obj do - local mode = lfs.attributes(dir.."/"..f, "mode") - if mode == "directory" and f ~= "." and f ~= ".." then - _readList(target, dir.."/"..f) - elseif mode == "file" or mode == "link" then - if string.sub(f, 1, 1) ~= "." then - local file_type = string.lower(string.match(f, ".+%.([^.]+)") or "") - if file_type == "ttf" or file_type == "ttc" - or file_type == "cff" or file_type == "otf" then - if not isInFontsBlacklist(f) then - table.insert(target, dir.."/"..f) - end - end - end +-- Query FreeType/HarfBuzz about font metadata +local function collectFaceInfo(path) + local res = {} + local n = FT.getFaceCount(path) + if not n then + return + end + for i=0, n-1 do + local ok, face = pcall(FT.newFace, path, nil, i) + if not ok then + return nil end + + local fres = face:getInfo() + local hbface = HB.hb_ft_face_create_referenced(face) + fres.names = hbface:getNames() + fres.scripts, fres.langs = hbface:getCoverage() + fres.path = path + fres.index = i + table.insert(res, fres) + + hbface:destroy() + face:done() end + return res +end + +local font_exts = { + ["ttf"] = true, + ["ttc"] = true, + ["cff"] = true, + ["otf"] = true, +} + +function FontList:_readList(dir, mark) + util.findFiles(dir, function(path, file, attr) + -- See if we're interested + if file:sub(1,1) == "." then return end + local file_type = file:lower():match(".+%.([^.]+)") or "" + if not font_exts[file_type] then return end + + -- Add it to the list + if not isInFontsBlacklist(file) then + table.insert(self.fontlist, path) + end + + -- And into cached info table + mark[path] = true + if self.fontinfo[path] and (self.fontinfo[path].change == attr.change) then + return + end + local fi = collectFaceInfo(path) + if not fi then return end + fi.change = attr.change + self.fontinfo[path] = fi + mark.cache_dirty = true + end) end function FontList:getFontList() if #self.fontlist > 0 then return self.fontlist end - _readList(self.fontlist, self.fontdir) + + local cache_file = DataStorage:getDataDir() .. "/cache/fontinfo.dat" + local ok + ok, self.fontinfo = pcall(dofile, cache_file) + if not ok or not self.fontinfo then + self.fontinfo = {} + end + + -- used for marking fonts we're seeing + local mark = { cache_dirty = false } + + self:_readList(self.fontdir, mark) -- multiple paths should be joined with semicolon for dir in string.gmatch(getExternalFontDir() or "", "([^;]+)") do - _readList(self.fontlist, dir) + self:_readList(dir, mark) + end + + -- clear fonts that no longer exist + for k, _ in pairs(self.fontinfo) do + if not mark[k] then + self.fontinfo[k] = nil + mark.cache_dirty = true + end + end + + if dbg.is_verbose then + -- when verbose debug is on, always dump the cache in plain text (to inspect the db output) + local cache = io.open(cache_file, "w") + cache:write("return " .. dump(self.fontinfo)) + cache:close() + elseif mark.cache_dirty then + -- otherwise dump the db in binary (more compact), and only if something has changed + local bc = load("return " .. dump(self.fontinfo)) + bc = string.dump(bc, true) + local cache = io.open(cache_file, "wb") + cache:write(bc) + cache:close() end + + local names = self.fontnames + for _,coll in pairs(self.fontinfo) do + for _,v in ipairs(coll) do + local nlist = names[v.name] or {} + assert(v.name) + if #nlist == 0 then + logger.dbg("FONTNAMES ADD: ", v.name) + end + names[v.name] = nlist + table.insert(nlist, v) + end + end + table.sort(self.fontlist) return self.fontlist end +-- Try to determine the localized font name +function FontList:getLocalizedFontName(file, index) + local lang = G_reader_settings:readSetting("language") + if not lang then return end + lang = lang:lower():gsub("_","-") + local altname = self.fontinfo[file] + altname = altname and altname[index+1] + altname = altname and altname.names and (altname.names[lang] or altname.names[lang:match("%w+")]) + altname = altname and (altname[tonumber(HB.HB_OT_NAME_ID_FULL_NAME)] or altname[tonumber(HB.HB_OT_NAME_ID_FONT_FAMILY)]) + if not altname then return end -- ensure nil + return altname +end + return FontList diff --git a/frontend/util.lua b/frontend/util.lua index 548d2656e..9a27f2a6e 100644 --- a/frontend/util.lua +++ b/frontend/util.lua @@ -580,6 +580,28 @@ function util.getFilesystemType(path) return type end +--- Recursively scan directory for files inside +-- @string path +-- @function callback(fullpath, name, attr) +function util.findFiles(dir, cb) + local function scan(current) + local ok, iter, dir_obj = pcall(lfs.dir, current) + if not ok then return end + for f in iter, dir_obj do + local path = current.."/"..f + local attr = lfs.attributes(path) + if attr.mode == "directory" then + if f ~= "." and f ~= ".." then + scan(path) + end + else + cb(path, f, attr) + end + end + end + scan(dir) +end + --- Checks if directory is empty. ---- @string path ---- @treturn bool