summaryrefslogtreecommitdiff
path: root/src/luaotfload-database.lua
diff options
context:
space:
mode:
authorPhilipp Gesang <phg42.2a@gmail.com>2014-02-12 07:50:06 +0100
committerPhilipp Gesang <phg42.2a@gmail.com>2014-02-12 07:50:06 +0100
commit9138da7d4a53d65bc15f3a5dc73fd373db40bdf7 (patch)
tree702093c750d81aa2e8810f484627b51d6b485c27 /src/luaotfload-database.lua
parentffa5a347f68805e218c61c344c0b8a895c4bb8db (diff)
downloadluaotfload-9138da7d4a53d65bc15f3a5dc73fd373db40bdf7.tar.gz
[*] move source files to ./src
Diffstat (limited to 'src/luaotfload-database.lua')
-rw-r--r--src/luaotfload-database.lua3445
1 files changed, 3445 insertions, 0 deletions
diff --git a/src/luaotfload-database.lua b/src/luaotfload-database.lua
new file mode 100644
index 0000000..4b2d201
--- /dev/null
+++ b/src/luaotfload-database.lua
@@ -0,0 +1,3445 @@
+if not modules then modules = { } end modules ['luaotfload-database'] = {
+ version = "2.5",
+ comment = "companion to luaotfload-main.lua",
+ author = "Khaled Hosny, Elie Roux, Philipp Gesang",
+ copyright = "Luaotfload Development Team",
+ license = "GNU GPL v2.0"
+}
+
+--[[doc--
+
+ Some statistics:
+
+ a) TL 2012, mkluatexfontdb --force
+ b) v2.4, luaotfload-tool --update --force
+ c) v2.4, luaotfload-tool --update --force --formats=+afm,pfa,pfb
+ d) Context, mtxrun --script fonts --reload --force
+
+ (Keep in mind that Context does index fewer fonts since it
+ considers only the contents of the minimals tree, not the
+ tex live one!)
+
+ time (m:s) peak VmSize (kB)
+ a 1:19 386 018
+ b 0:37 715 797
+ c 2:27 1 017 674
+ d 0:44 1 082 313
+
+ Most of the increase in memory consumption from version 1.x to 2.2+
+ can be attributed to the move from single-pass to a multi-pass
+ approach to building the index: Information is first gathered from
+ all reachable fonts and only afterwards processed, classified and
+ discarded. Also, there is a good deal of additional stuff kept in
+ the database now: two extra tables for file names and font families
+ have been added, making font lookups more efficient while improving
+ maintainability of the code.
+
+--doc]]--
+
+local lpeg = require "lpeg"
+local P, Cc, lpegmatch = lpeg.P, lpeg.Cc, lpeg.match
+
+local parsers = luaotfload.parsers
+local read_fonts_conf = parsers.read_fonts_conf
+local stripslashes = parsers.stripslashes
+local splitcomma = parsers.splitcomma
+
+local log = luaotfload.log
+local report = log.report
+local report_status = log.names_status
+local report_status_start = log.names_status_start
+local report_status_stop = log.names_status_stop
+
+
+--- Luatex builtins
+local load = load
+local next = next
+local require = require
+local tonumber = tonumber
+local unpack = table.unpack
+
+local fontloaderinfo = fontloader.info
+local fontloaderclose = fontloader.close
+local fontloaderopen = fontloader.open
+----- fontloaderto_table = fontloader.to_table
+local gzipopen = gzip.open
+local iolines = io.lines
+local ioopen = io.open
+local iopopen = io.popen
+local kpseexpand_path = kpse.expand_path
+local kpsefind_file = kpse.find_file
+local kpselookup = kpse.lookup
+local kpsereadable_file = kpse.readable_file
+local lfsattributes = lfs.attributes
+local lfschdir = lfs.chdir
+local lfscurrentdir = lfs.currentdir
+local lfsdir = lfs.dir
+local mathabs = math.abs
+local mathmin = math.min
+local osgetenv = os.getenv
+local osgettimeofday = os.gettimeofday
+local osremove = os.remove
+local stringfind = string.find
+local stringformat = string.format
+local stringgmatch = string.gmatch
+local stringgsub = string.gsub
+local stringlower = string.lower
+local stringsub = string.sub
+local stringupper = string.upper
+local tableconcat = table.concat
+local tablesort = table.sort
+local utf8gsub = unicode.utf8.gsub
+local utf8lower = unicode.utf8.lower
+local utf8len = unicode.utf8.len
+local zlibcompress = zlib.compress
+
+--- these come from Lualibs/Context
+local filebasename = file.basename
+local filecollapsepath = file.collapsepath or file.collapse_path
+local filedirname = file.dirname
+local fileextname = file.extname
+local fileiswritable = file.iswritable
+local filejoin = file.join
+local filenameonly = file.nameonly
+local filereplacesuffix = file.replacesuffix
+local filesplitpath = file.splitpath or file.split_path
+local filesuffix = file.suffix
+local getwritablepath = caches.getwritablepath
+local lfsisdir = lfs.isdir
+local lfsisfile = lfs.isfile
+local lfsmkdirs = lfs.mkdirs
+local lpegsplitat = lpeg.splitat
+local stringis_empty = string.is_empty
+local stringsplit = string.split
+local stringstrip = string.strip
+local tableappend = table.append
+local tablecontains = table.contains
+local tablecopy = table.copy
+local tablefastcopy = table.fastcopy
+local tabletofile = table.tofile
+local tabletohash = table.tohash
+local tableserialize = table.serialize
+local runasscript = caches == nil
+--- the font loader namespace is “fonts”, same as in Context
+--- we need to put some fallbacks into place for when running
+--- as a script
+fonts = fonts or { }
+fonts.names = fonts.names or { }
+fonts.definers = fonts.definers or { }
+
+local luaotfloadconfig = config.luaotfload --- always present
+luaotfloadconfig.resolver = luaotfloadconfig.resolver or "normal"
+luaotfloadconfig.formats = luaotfloadconfig.formats or "otf,ttf,ttc,dfont"
+luaotfloadconfig.strip = luaotfloadconfig.strip == true
+
+--- this option allows for disabling updates
+--- during a TeX run
+luaotfloadconfig.update_live = luaotfloadconfig.update_live ~= false
+luaotfloadconfig.compress = luaotfloadconfig.compress ~= false
+
+local names = fonts.names
+local name_index = nil --> upvalue for names.data
+local lookup_cache = nil --> for names.lookups
+names.version = 2.5
+names.data = nil --- contains the loaded database
+names.lookups = nil --- contains the lookup cache
+
+names.path = { index = { }, lookups = { } }
+names.path.globals = {
+ prefix = "", --- writable_path/names_dir
+ names_dir = luaotfloadconfig.names_dir or "names",
+ index_file = luaotfloadconfig.index_file
+ or "luaotfload-names.lua",
+ lookups_file = "luaotfload-lookup-cache.lua",
+}
+
+--- string -> (string * string)
+local make_luanames = function (path)
+ return filereplacesuffix(path, "lua"),
+ filereplacesuffix(path, "luc")
+end
+
+--- The “termwidth” value is only considered when printing
+--- short status messages, e.g. when building the database
+--- online.
+if not luaotfloadconfig.termwidth then
+ local tw = 79
+ if not ( os.type == "windows" --- Assume broken terminal.
+ or osgetenv "TERM" == "dumb")
+ then
+ local p = iopopen "tput cols"
+ if p then
+ result = tonumber (p:read "*all")
+ p:close ()
+ if result then
+ tw = result
+ else
+ report ("log", 2, "db", "tput returned non-number.")
+ end
+ else
+ report ("log", 2, "db", "Shell escape disabled or tput executable missing.")
+ report ("log", 2, "db", "Assuming 79 cols terminal width.")
+ end
+ end
+ luaotfloadconfig.termwidth = tw
+end
+
+local format_precedence = {
+ "otf", "ttc", "ttf",
+ "dfont", "afm", "pfb",
+ "pfa",
+}
+
+local location_precedence = {
+ "local", "system", "texmf",
+}
+
+local set_location_precedence = function (precedence)
+ location_precedence = precedence
+end
+
+--[[doc--
+ We use the functions in the cache.* namespace that come with the
+ fontloader (see luat-basics-gen). it’s safe to use for the most part
+ since most checks and directory creations are already done. It
+ uses TEXMFCACHE or TEXMFVAR as starting points.
+
+ There is one quirk, though: ``getwritablepath()`` will always
+ assume that files in subdirectories of the cache tree are writable.
+ It gives no feedback at all if it fails to open a file in write
+ mode. This may cause trouble when the index or lookup cache were
+ created by different user.
+--doc]]--
+
+if not runasscript then
+ local globals = names.path.globals
+ local names_dir = globals.names_dir
+
+ prefix = getwritablepath (names_dir, "")
+ if not prefix then
+ luaotfload.error
+ ("Impossible to find a suitable writeable cache...")
+ else
+ prefix = lpegmatch (stripslashes, prefix)
+ report ("log", 0, "db",
+ "Root cache directory is %s.", prefix)
+ end
+
+ globals.prefix = prefix
+ local lookup_path = names.path.lookups
+ local index = names.path.index
+ local lookups_file = filejoin (prefix, globals.lookups_file)
+ local index_file = filejoin (prefix, globals.index_file)
+ lookup_path.lua, lookup_path.luc = make_luanames (lookups_file)
+ index.lua, index.luc = make_luanames (index_file)
+else --- running as script, inject some dummies
+ caches = { }
+ local dummy_function = function () end
+ log = { report = dummy_function,
+ report_status = dummy_function,
+ report_status_start = dummy_function,
+ report_status_stop = dummy_function, }
+end
+
+
+--[[doc--
+Auxiliary functions
+--doc]]--
+
+--- fontnames contain all kinds of garbage; as a precaution we
+--- lowercase and strip them of non alphanumerical characters
+
+--- string -> string
+
+local invalidchars = "[^%a%d]"
+
+local sanitize_fontname = function (str)
+ if str ~= nil then
+ str = utf8gsub (utf8lower (str), invalidchars, "")
+ return str
+ end
+ return nil
+end
+
+local sanitize_fontnames = function (rawnames)
+ local result = { }
+ for category, namedata in next, rawnames do
+
+ if type (namedata) == "string" then
+ result [category] = utf8gsub (utf8lower (namedata),
+ invalidchars,
+ "")
+ else
+ local target = { }
+ for field, name in next, namedata do
+ target [field] = utf8gsub (utf8lower (name),
+ invalidchars,
+ "")
+ end
+ result [category] = target
+ end
+ end
+ return result
+end
+
+local find_files_indeed
+find_files_indeed = function (acc, dirs, filter)
+ if not next (dirs) then --- done
+ return acc
+ end
+
+ local pwd = lfscurrentdir ()
+ local dir = dirs[#dirs]
+ dirs[#dirs] = nil
+
+ if lfschdir (dir) then
+ lfschdir (pwd)
+
+ local newfiles = { }
+ for ent in lfsdir (dir) do
+ if ent ~= "." and ent ~= ".." then
+ local fullpath = dir .. "/" .. ent
+ if filter (fullpath) == true then
+ if lfsisdir (fullpath) then
+ dirs[#dirs+1] = fullpath
+ elseif lfsisfile (fullpath) then
+ newfiles[#newfiles+1] = fullpath
+ end
+ end
+ end
+ end
+ return find_files_indeed (tableappend (acc, newfiles),
+ dirs, filter)
+ end
+ --- could not cd into, so we skip it
+ return find_files_indeed (acc, dirs, filter)
+end
+
+local dummyfilter = function () return true end
+
+--- the optional filter function receives the full path of a file
+--- system entity. a filter applies if the first argument it returns is
+--- true.
+
+--- string -> function? -> string list
+local find_files = function (root, filter)
+ if lfsisdir (root) then
+ return find_files_indeed ({}, { root }, filter or dummyfilter)
+ end
+end
+
+
+--[[doc--
+This is a sketch of the luaotfload db:
+
+ type dbobj = {
+ families : familytable;
+ files : filemap;
+ status : filestatus;
+ mappings : fontentry list;
+ meta : metadata;
+ names : namedata; // TODO: check for relevance after db is finalized
+ }
+ and familytable = {
+ local : (format, familyentry) hash; // specified with include dir
+ texmf : (format, familyentry) hash;
+ system : (format, familyentry) hash;
+ }
+ and familyentry = {
+ regular : sizes;
+ italic : sizes;
+ bold : sizes;
+ bolditalic : sizes;
+ }
+ and sizes = {
+ default : int; // points into mappings or names
+ optical : (int, int) list; // design size -> index entry
+ }
+ and metadata = {
+ formats : string list; // { "otf", "ttf", "ttc", "dfont" }
+ statistics : TODO;
+ version : float;
+ }
+ and filemap = {
+ base : {
+ local : (string, int) hash; // basename -> idx
+ system : (string, int) hash;
+ texmf : (string, int) hash;
+ };
+ bare : {
+ local : (string, (string, int) hash) hash; // location -> (barename -> idx)
+ system : (string, (string, int) hash) hash;
+ texmf : (string, (string, int) hash) hash;
+ };
+ full : (int, string) hash; // idx -> full path
+ }
+ and fontentry = {
+ barename : string;
+ familyname : string;
+ filename : string;
+ fontname : string; // <- metadata
+ fullname : string; // <- metadata
+ sanitized : {
+ family : string;
+ fontstyle_name : string; // <- new in 2.4
+ fontname : string; // <- metadata
+ fullname : string; // <- namedata.names
+ metafamily : string;
+ pfullname : string;
+ prefmodifiers : string;
+ psname : string;
+ subfamily : string;
+ };
+ size : int list;
+ slant : int;
+ subfont : int;
+ location : local | system | texmf;
+ weight : int;
+ width : int;
+ units_per_em : int; // mainly 1000, but also 2048 or 256
+ }
+ and filestatus = (string, // fullname
+ { index : int list; // pointer into mappings
+ timestamp : int; }) dict
+
+beware that this is a reconstruction and may be incomplete.
+
+mtx-fonts has in names.tma:
+
+ type names = {
+ cache_uuid : uuid;
+ cache_version : float;
+ datastate : uuid list;
+ fallbacks : (filetype, (basename, int) hash) hash;
+ families : (basename, int list) hash;
+ files : (filename, fullname) hash;
+ indices : (fullname, int) hash;
+ mappings : (filetype, (basename, int) hash) hash;
+ names : ? (empty hash) ?;
+ rejected : (basename, int) hash;
+ specifications: fontentry list;
+ }
+ and fontentry = {
+ designsize : int;
+ familyname : string;
+ filename : string;
+ fontname : string;
+ format : string;
+ fullname : string;
+ maxsize : int;
+ minsize : int;
+ modification : int;
+ rawname : string;
+ style : string;
+ subfamily : string;
+ variant : string;
+ weight : string;
+ width : string;
+ }
+
+--doc]]--
+
+local initialize_namedata = function (formats) --- returns dbobj
+ return {
+ --families = { },
+ status = { }, -- was: status; map abspath -> mapping
+ mappings = { }, -- TODO: check if still necessary after rewrite
+ names = { },
+-- files = { }, -- created later
+ meta = {
+ formats = formats,
+ statistics = { },
+ version = names.version,
+ },
+ }
+end
+
+--[[doc--
+
+ Since Luaotfload does not depend on the lualibs anymore we
+ have to put our own small wrappers for the gzip library in
+ place.
+
+ load_gzipped -- Read and decompress and entire gzipped file.
+ Returns the uncompressed content as a string.
+
+--doc]]--
+
+local load_gzipped = function (filename)
+ local gh = gzipopen (filename,"rb")
+ if gh then
+ local data = gh:read "*all"
+ gh:close ()
+ return data
+ end
+end
+
+--[[doc--
+
+ save_gzipped -- Compress and write a string to file. The return
+ value is the number of bytes written. Zlib parameters are: best
+ compression and default strategy.
+
+--doc]]--
+
+local save_gzipped = function (filename, data)
+ local gh = gzipopen (filename, "wb9")
+ if gh then
+ gh:write (data)
+ local bytes = gh:seek ()
+ gh:close ()
+ return bytes
+ end
+end
+
+--- When loading a lua file we try its binary complement first, which
+--- is assumed to be located at an identical path, carrying the suffix
+--- .luc.
+
+--- string -> (string * table)
+local load_lua_file = function (path)
+ local foundname = filereplacesuffix (path, "luc")
+ local code = nil
+
+ local fh = ioopen (foundname, "rb") -- try bin first
+ if fh then
+ local chunk = fh:read"*all"
+ fh:close()
+ code = load (chunk, "b")
+ end
+
+ if not code then --- fall back to text file
+ foundname = filereplacesuffix (path, "lua")
+ fh = ioopen(foundname, "rb")
+ if fh then
+ local chunk = fh:read"*all"
+ fh:close()
+ code = load (chunk, "t")
+ end
+ end
+
+ if not code then --- probe gzipped file
+ foundname = filereplacesuffix (path, "lua.gz")
+ local chunk = load_gzipped (foundname)
+ if chunk then
+ code = load (chunk, "t")
+ end
+ end
+
+ if not code then return nil, nil end
+ return foundname, code ()
+end
+
+--- define locals in scope
+local crude_file_lookup
+local crude_file_lookup_verbose
+local find_closest
+local flush_lookup_cache
+local ot_fullinfo
+local t1_fullinfo
+local load_names
+local load_lookups
+local read_blacklist
+local reload_db
+local resolve_name
+local resolve_cached
+local resolve_fullpath
+local save_names
+local save_lookups
+local update_names
+local get_font_filter
+local set_font_filter
+
+--- state of the database
+local fonts_reloaded = false
+local fonts_read = 0
+
+--- limit output when approximate font matching (luaotfload-tool -F)
+local fuzzy_limit = 1 --- display closest only
+
+--- bool? -> dbobj
+load_names = function (dry_run)
+ local starttime = osgettimeofday ()
+ local foundname, data = load_lua_file (names.path.index.lua)
+
+ if data then
+ report ("both", 2, "db",
+ "Font names database loaded", "%s", foundname)
+ report ("info", 3, "db", "Loading took %0.f ms.",
+ 1000 * (osgettimeofday () - starttime))
+
+ local db_version, nms_version
+ if data.meta then
+ db_version = data.meta.version
+ else
+ --- Compatibility branch; the version info used to be
+ --- stored in the table root which is why updating from
+ --- an earlier index version broke.
+ db_version = data.version or -42 --- invalid
+ end
+ nms_version = names.version
+ if db_version ~= nms_version then
+ report ("both", 0, "db",
+ [[Version mismatch; expected %4.3f, got %4.3f.]],
+ nms_version, db_version)
+ if not fonts_reloaded then
+ report ("both", 0, "db", [[Force rebuild.]])
+ data = update_names ({ }, true, false)
+ if not data then
+ report ("both", 0, "db",
+ "Database creation unsuccessful.")
+ end
+ end
+ end
+ else
+ report ("both", 0, "db",
+ [[Font names database not found, generating new one.]])
+ report ("both", 0, "db",
+ [[This can take several minutes; please be patient.]])
+ data = update_names (initialize_namedata (get_font_filter ()),
+ nil, dry_run)
+ if not data then
+ report ("both", 0, "db", "Database creation unsuccessful.")
+ end
+ end
+ return data
+end
+
+--- unit -> unit
+load_lookups = function ( )
+ local foundname, data = load_lua_file(names.path.lookups.lua)
+ if data then
+ report("both", 3, "cache",
+ "Lookup cache loaded from %s.", foundname)
+ else
+ report("both", 1, "cache",
+ "No lookup cache, creating empty.")
+ data = { }
+ end
+ lookup_cache = data
+end
+
+local regular_synonym = {
+ book = "r",
+ normal = "r",
+ plain = "r",
+ regular = "r",
+ roman = "r",
+}
+
+local italic_synonym = {
+ oblique = true,
+ slanted = true,
+ italic = true,
+}
+
+local style_category = {
+ regular = "r",
+ bold = "b",
+ bolditalic = "bi",
+ italic = "i",
+ r = "regular",
+ b = "bold",
+ bi = "bolditalic",
+ i = "italic",
+}
+
+local type1_formats = { "tfm", "ofm", }
+
+local dummy_findfile = resolvers.findfile -- from basics-gen
+
+--- filemap -> string -> string -> (string | bool)
+local verbose_lookup = function (data, kind, filename)
+ local found = data[kind][filename]
+ if found ~= nil then
+ found = data.full[found]
+ if found == nil then --> texmf
+ report("info", 0, "db",
+ "Crude file lookup: req=%s; hit=%s => kpse.",
+ filename, kind)
+ found = dummy_findfile(filename)
+ else
+ report("info", 0, "db",
+ "Crude file lookup: req=%s; hit=%s; ret=%s.",
+ filename, kind, found)
+ end
+ return found
+ end
+ return false
+end
+
+--- string -> (string * string * bool)
+crude_file_lookup_verbose = function (filename)
+ if not name_index then name_index = load_names() end
+ local mappings = name_index.mappings
+ local files = name_index.files
+ local found
+
+ --- look up in db first ...
+ found = verbose_lookup(files, "bare", filename)
+ if found then
+ return found, nil, true
+ end
+ found = verbose_lookup(files, "base", filename)
+ if found then
+ return found, nil, true
+ end
+
+ --- ofm and tfm, returns pair
+ for i=1, #type1_formats do
+ local format = type1_formats[i]
+ if resolvers.findfile(filename, format) then
+ return file.addsuffix(filename, format), format, true
+ end
+ end
+ return filename, nil, false
+end
+
+local lookup_filename = function (filename)
+ if not name_index then name_index = load_names () end
+ local files = name_index.files
+ local basedata = files.base
+ local baredata = files.bare
+ for i = 1, #location_precedence do
+ local location = location_precedence [i]
+ local basenames = basedata [location]
+ local barenames = baredata [location]
+ local idx
+ if basenames ~= nil then
+ idx = basenames [filename]
+ if idx then
+ goto done
+ end
+ end
+ if barenames ~= nil then
+ for j = 1, #format_precedence do
+ local format = format_precedence [j]
+ local filemap = barenames [format]
+ if filemap then
+ idx = barenames [format] [filename]
+ if idx then
+ break
+ end
+ end
+ end
+ end
+::done::
+ if idx then
+ return files.full [idx]
+ end
+ end
+end
+
+--- string -> (string * string * bool)
+crude_file_lookup = function (filename)
+ local found = lookup_filename (filename)
+
+ if not found then
+ found = dummy_findfile(filename)
+ end
+
+ if found then
+ return found, nil, true
+ end
+
+ for i=1, #type1_formats do
+ local format = type1_formats[i]
+ if resolvers.findfile(filename, format) then
+ return file.addsuffix(filename, format), format, true
+ end
+ end
+
+ return filename, nil, false
+end
+
+--[[doc--
+Existence of the resolved file name is verified differently depending
+on whether the index entry has a texmf flag set.
+--doc]]--
+
+local get_font_file = function (index)
+ local entry = name_index.mappings [index]
+ if not entry then
+ return false
+ end
+ local basename = entry.basename
+ if entry.location == "texmf" then
+ if kpselookup(basename) then
+ return true, basename, entry.subfont
+ end
+ else --- system, local
+ local fullname = name_index.files.full [index]
+ if lfsisfile (fullname) then
+ return true, basename, entry.subfont
+ end
+ end
+ return false
+end
+
+--[[doc--
+We need to verify if the result of a cached lookup actually exists in
+the texmf or filesystem. Again, due to the schizoprenic nature of the
+font managment we have to check both the system path and the texmf.
+--doc]]--
+
+local verify_font_file = function (basename)
+ local path = resolve_fullpath (basename)
+ if path and lfsisfile(path) then
+ return true
+ end
+ if kpsefind_file(basename) then
+ return true
+ end
+ return false
+end
+
+--[[doc--
+Lookups can be quite costly, more so the less specific they are.
+Even if we find a matching font eventually, the next time the
+user compiles Eir document E will have to stand through the delay
+again.
+Thus, some caching of results -- even between runs -- is in order.
+We’ll just store successful name: lookups in a separate cache file.
+
+type lookup_cache = (string, (string * num)) dict
+
+The spec is expected to be modified in place (ugh), so we’ll have to
+catalogue what fields actually influence its behavior.
+
+Idk what the “spec” resolver is for.
+
+ lookup inspects modifies
+ ---------- ----------------- ---------------------------
+ file: name forced, name
+ name:[*] name, style, sub, resolved, sub, name, forced
+ optsize, size
+ spec: name, sub resolved, sub, name, forced
+
+[*] name: contains both the name resolver from luatex-fonts and
+ resolve_name() below
+
+From my reading of font-def.lua, what a resolver does is
+basically rewrite the “name” field of the specification record
+with the resolution.
+Also, the fields “resolved”, “sub”, “force” etc. influence the outcome.
+
+--doc]]--
+
+local concat_char = "#"
+local hash_fields = {
+ --- order is important
+ "specification", "style", "sub", "optsize", "size",
+}
+local n_hash_fields = #hash_fields
+
+--- spec -> string
+local hash_request = function (specification)
+ local key = { } --- segments of the hash
+ for i=1, n_hash_fields do
+ local field = specification[hash_fields[i]]
+ if field then
+ key[#key+1] = field
+ end
+ end
+ return tableconcat(key, concat_char)
+end
+
+--- 'a -> 'a -> table -> (string * int|boolean * boolean)
+resolve_cached = function (specification)
+ if not lookup_cache then load_lookups () end
+ local request = hash_request(specification)
+ report("both", 4, "cache", "Looking for %q in cache ...",
+ request)
+
+ local found = lookup_cache [request]
+
+ --- case 1) cache positive ----------------------------------------
+ if found then --- replay fields from cache hit
+ report("info", 4, "cache", "Found!")
+ local basename = found[1]
+ --- check the presence of the file in case it’s been removed
+ local success = verify_font_file (basename)
+ if success == true then
+ return basename, found[2], true
+ end
+ report("both", 4, "cache", "Cached file not found; resolving again.")
+ else
+ report("both", 4, "cache", "Not cached; resolving.")
+ end
+
+ --- case 2) cache negative ----------------------------------------
+ --- first we resolve normally ...
+ local filename, subfont = resolve_name (specification)
+ if not filename then
+ return nil, nil
+ end
+ --- ... then we add the fields to the cache ... ...
+ local entry = { filename, subfont }
+ report("both", 4, "cache", "New entry: %s.", request)
+ lookup_cache [request] = entry
+
+ --- obviously, the updated cache needs to be stored.
+ --- TODO this should trigger a save only once the
+ --- document is compiled (finish_pdffile callback?)
+ report("both", 5, "cache", "Saving updated cache.")
+ local success = save_lookups ()
+ if not success then --- sad, but not critical
+ report("both", 0, "cache", "Error writing cache.")
+ end
+ return filename, subfont
+end
+
+--- this used to be inlined; with the lookup cache we don’t
+--- have to be parsimonious wrt function calls anymore
+--- “found” is the match accumulator
+local add_to_match = function (found, size, face)
+
+ local continue = true
+
+ local optsize = face.size
+
+ if optsize and next (optsize) then
+ local dsnsize, maxsize, minsize
+ dsnsize = optsize[1]
+ maxsize = optsize[2]
+ minsize = optsize[3]
+
+ if size ~= nil
+ and (dsnsize == size or (size > minsize and size <= maxsize))
+ then
+ found[1] = face
+ continue = false ---> break
+ else
+ found[#found+1] = face
+ end
+ else
+ found[1] = face
+ continue = false ---> break
+ end
+
+ return found, continue
+end
+
+local choose_closest = function (distances)
+ local closest = 2^51
+ local match
+ for i = 1, #distances do
+ local d, index = unpack (distances [i])
+ if d < closest then
+ closest = d
+ match = index
+ end
+ end
+ return match
+end
+
+--[[doc--
+
+ choose_size -- Pick a font face of appropriate size from the list
+ of family members with matching style. There are three categories:
+
+ 1. exact matches: if there is a face whose design size equals
+ the asked size, it is returned immediately and no further
+ candidates are inspected.
+
+ 2. range matches: of all faces in whose design range the
+ requested size falls the one whose center the requested
+ size is closest to is returned.
+
+ 3. out-of-range matches: of all other faces (i. e. whose range
+ is above or below the asked size) the one is chosen whose
+ boundary (upper or lower) is closest to the requested size.
+
+ 4. default matches: if no design size or a design size of zero
+ is requested, the face with the default size is returned.
+
+--doc]]--
+
+--- int * int * int * int list -> int -> int
+local choose_size = function (sizes, askedsize)
+ local mappings = name_index.mappings
+ local match = sizes.default
+ local exact
+ local inrange = { } --- distance * index list
+ local norange = { } --- distance * index list
+ local fontname, subfont
+ if askedsize ~= 0 then
+ --- firstly, look for an exactly matching design size or
+ --- matching range
+ for i = 1, #sizes do
+ local dsnsize, high, low, index = unpack (sizes [i])
+ if dsnsize == askedsize then
+ --- exact match, this is what we were looking for
+ exact = index
+ goto skip
+ elseif askedsize < low then
+ --- below range, add to the norange table
+ local d = low - askedsize
+ norange [#norange + 1] = { d, index }
+ elseif askedsize > high then
+ --- beyond range, add to the norange table
+ local d = askedsize - high
+ norange [#norange + 1] = { d, index }
+ else
+ --- range match
+ local d = ((low + high) / 2) - askedsize
+ if d < 0 then
+ d = -d
+ end
+ inrange [#inrange + 1] = { d, index }
+ end
+ end
+ end
+::skip::
+ if exact then
+ match = exact
+ elseif #inrange > 0 then
+ match = choose_closest (inrange)
+ elseif #norange > 0 then
+ match = choose_closest (norange)
+ end
+ return match
+end
+
+--[[doc--
+
+ resolve_familyname -- Query the families table for an entry
+ matching the specification.
+ The parameters “name” and “style” are pre-sanitized.
+
+--doc]]--
+--- spec -> string -> string -> int -> string * int
+local resolve_familyname = function (specification, name, style, askedsize)
+ local families = name_index.families
+ local mappings = name_index.mappings
+ local candidates = nil
+ --- arrow code alert
+ for i = 1, #location_precedence do
+ local location = location_precedence [i]
+ local locgroup = families [location]
+ for j = 1, #format_precedence do
+ local format = format_precedence [j]
+ local fmtgroup = locgroup [format]
+ if fmtgroup then
+ local familygroup = fmtgroup [name]
+ if familygroup then
+ local stylegroup = familygroup [style]
+ if stylegroup then --- suitable match
+ candidates = stylegroup
+ goto done
+ end
+ end
+ end
+ end
+ end
+ if true then
+ return nil, nil
+ end
+::done::
+ index = choose_size (candidates, askedsize)
+ local success, resolved, subfont = get_font_file (index)
+ if not success then
+ return nil, nil
+ end
+ report ("info", 2, "db", "Match found: %s(%d).",
+ resolved, subfont or 0)
+ return resolved, subfont
+end
+
+local resolve_fontname = function (specification, name, style)
+ local mappings = name_index.mappings
+ local fallback = nil
+ local lastresort = nil
+ style = style_category [style]
+ for i = 1, #mappings do
+ local face = mappings [i]
+ local prefmodifiers = face.prefmodifiers
+ local subfamily = face.subfamily
+ if face.fontname == name
+ or face.splainname == name
+ or face.fullname == name
+ or face.psname == name
+ then
+ return face.basename, face.subfont
+ elseif face.familyname == name then
+ if prefmodifiers == style
+ or subfamily == style
+ then
+ fallback = face
+ elseif regular_synonym [prefmodifiers]
+ or regular_synonym [subfamily]
+ then
+ lastresort = face
+ end
+ elseif face.metafamily == name
+ and (regular_synonym [prefmodifiers]
+ or regular_synonym [subfamily])
+ then
+ lastresort = face
+ end
+ end
+ if fallback then
+ return fallback.basename, fallback.subfont
+ end
+ if lastresort then
+ return lastresort.basename, lastresort.subfont
+ end
+ return nil, nil
+end
+
+--[[doc--
+
+ resolve_name -- Perform a name: lookup. This first queries the
+ font families table and, if there is no match for the spec, the
+ font names table.
+ The return value is a pair consisting of the file name and the
+ subfont index if appropriate..
+
+ the request specification has the fields:
+
+ · features: table
+ · normal: set of { ccmp clig itlc kern liga locl mark mkmk rlig }
+ · ???
+ · forced: string
+ · lookup: "name"
+ · method: string
+ · name: string
+ · resolved: string
+ · size: int
+ · specification: string (== <lookup> ":" <name>)
+ · sub: string
+
+ The “size” field deserves special attention: if its value is
+ negative, then it actually specifies a scalefactor of the
+ design size of the requested font. This happens e.g. if a font is
+ requested without an explicit “at size”. If the font is part of a
+ larger collection with different design sizes, this complicates
+ matters a bit: Normally, the resolver prefers fonts that have a
+ design size as close as possible to the requested size. If no
+ size specified, then the design size is implied. But which design
+ size should that be? Xetex appears to pick the “normal” (unmarked)
+ size: with Adobe fonts this would be the one that is neither
+ “caption” nor “subhead” nor “display” &c ... For fonts by Adobe this
+ seems to be the one that does not receive a “prefmodifiers” field.
+ (IOW Adobe uses the “prefmodifiers” field to encode the design size
+ in more or less human readable format.) However, this is not true
+ of LM and EB Garamond. As this matters only where there are
+ multiple design sizes to a given font/style combination, we put a
+ workaround in place that chooses that unmarked version.
+
+ The first return value of “resolve_name” is the file name of the
+ requested font (string). It can be passed to the fullname resolver
+ get_font_file().
+ The second value is either “false” or an integer indicating the
+ subfont index in a TTC.
+
+--doc]]--
+
+--- table -> string * (int | bool)
+resolve_name = function (specification)
+ local resolved, subfont
+ if not name_index then name_index = load_names () end
+ local name = sanitize_fontname (specification.name)
+ local style = sanitize_fontname (specification.style) or "r"
+ local askedsize = specification.optsize
+
+ if askedsize then
+ askedsize = tonumber (askedsize)
+ else
+ askedsize = specification.size
+ if askedsize and askedsize >= 0 then
+ askedsize = askedsize / 65536
+ else
+ askedsize = 0
+ end
+ end
+
+ resolved, subfont = resolve_familyname (specification,
+ name,
+ style,
+ askedsize)
+ if not resolved then
+ resolved, subfont = resolve_fontname (specification,
+ name,
+ style)
+ end
+ if not resolved then
+ resolved = specification.name, false
+ end
+
+ if not resolved then
+ if not fonts_reloaded then
+ return reload_db ("Font not found.",
+ resolve_name,
+ specification)
+ end
+ end
+ return resolved, subfont
+end
+
+resolve_fullpath = function (fontname, ext) --- getfilename()
+ if not name_index then name_index = load_names () end
+ local files = name_index.files
+ local basedata = files.base
+ local baredata = files.bare
+ for i = 1, #location_precedence do
+ local location = location_precedence [i]
+ local basenames = basedata [location]
+ local idx
+ if basenames ~= nil then
+ idx = basenames [fontname]
+ end
+ if ext then
+ local barenames = baredata [location] [ext]
+ if not idx and barenames ~= nil then
+ idx = barenames [fontname]
+ end
+ end
+ if idx then
+ return files.full [idx]
+ end
+ end
+ return ""
+end
+
+--- when reload is triggered we update the database
+--- and then re-run the caller with the arg list
+
+--- string -> ('a -> 'a) -> 'a list -> 'a
+reload_db = function (why, caller, ...)
+ local namedata = name_index
+ local formats = tableconcat (namedata.meta.formats, ",")
+
+ report ("both", 1, "db",
+ "Reload initiated (formats: %s); reason: %q.",
+ formats, why)
+
+ set_font_filter (formats)
+ namedata = update_names (namedata, false, false)
+
+ if namedata then
+ fonts_reloaded = true
+ name_index = namedata
+ return caller (...)
+ end
+
+ report ("both", 0, "db", "Database update unsuccessful.")
+end
+
+--- string -> string -> int
+local iterative_levenshtein = function (s1, s2)
+
+ local costs = { }
+ local len1, len2 = #s1, #s2
+
+ for i = 0, len1 do
+ local last = i
+ for j = 0, len2 do
+ if i == 0 then
+ costs[j] = j
+ else
+ if j > 0 then
+ local current = costs[j-1]
+ if stringsub(s1, i, i) ~= stringsub(s2, j, j) then
+ current = mathmin(current, last, costs[j]) + 1
+ end
+ costs[j-1] = last
+ last = current
+ end
+ end
+ end
+ if i > 0 then costs[len2] = last end
+ end
+
+ return costs[len2]--- lower right has the distance
+end
+
+--- string -> int -> bool
+find_closest = function (name, limit)
+ local name = sanitize_fontname (name)
+ limit = limit or fuzzy_limit
+
+ if not name_index then name_index = load_names () end
+ if not name_index or type (name_index) ~= "table" then
+ if not fonts_reloaded then
+ return reload_db("no database", find_closest, name)
+ end
+ return false
+ end
+
+ local by_distance = { } --- (int, string list) dict
+ local distances = { } --- int list
+ local cached = { } --- (string, int) dict
+ local mappings = name_index.mappings
+ local n_fonts = #mappings
+
+ for n = 1, n_fonts do
+ local current = mappings[n]
+ --[[
+ This is simplistic but surpisingly fast.
+ Matching is performed against the “fullname” field
+ of a db record in preprocessed form. We then store the
+ raw “fullname” at its edit distance.
+ We should probably do some weighting over all the
+ font name categories as well as whatever agrep
+ does.
+ --]]
+ local fullname = current.plainname
+ local sfullname = current.fullname
+ local dist = cached[sfullname]--- maybe already calculated
+
+ if not dist then
+ dist = iterative_levenshtein(name, sfullname)
+ cached[sfullname] = dist
+ end
+ local namelst = by_distance[dist]
+ if not namelst then --- first entry
+ namelst = { fullname }
+ distances[#distances+1] = dist
+ else --- append
+ namelst[#namelst+1] = fullname
+ end
+ by_distance[dist] = namelst
+ end
+
+ --- print the matches according to their distance
+ local n_distances = #distances
+ if n_distances > 0 then --- got some data
+ tablesort(distances)
+ limit = mathmin(n_distances, limit)
+ report(false, 1, "query",
+ "Displaying %d distance levels.", limit)
+
+ for i = 1, limit do
+ local dist = distances[i]
+ local namelst = by_distance[dist]
+ report(false, 0, "query",
+ "Distance from \"%s\": %s\n "
+ .. tableconcat (namelst, "\n "),
+ name, dist)
+ end
+
+ return true
+ end
+ return false
+end --- find_closest()
+
+--[[doc--
+
+ load_font_file -- Safely open a font file. See
+ <http://www.ntg.nl/pipermail/ntg-context/2013/075885.html>
+ regarding the omission of ``fontloader.close()``.
+
+ TODO -- check if fontloader.info() is ready for prime in 0.78+
+ -- fields /tables needed:
+ -- names
+ -- postscriptname
+ -- validation_state
+ -- ..
+
+--doc]]--
+
+local load_font_file = function (filename, subfont)
+ local rawfont, _msg = fontloaderopen (filename, subfont)
+ if not rawfont then
+ report ("log", 1, "db", "ERROR: failed to open %s.", filename)
+ return
+ end
+ return rawfont
+end
+
+--- rawdata -> (int * int * int | bool)
+
+local get_size_info = function (metadata)
+ local design_size = metadata.design_size
+ local design_range_top = metadata.design_range_top
+ local design_range_bottom = metadata.design_range_bottom
+
+ local fallback_size = design_size ~= 0 and design_size
+ or design_range_bottom ~= 0 and design_range_bottom
+ or design_range_top ~= 0 and design_range_top
+
+ if fallback_size then
+ design_size = (design_size or fallback_size) / 10
+ design_range_top = (design_range_top or fallback_size) / 10
+ design_range_bottom = (design_range_bottom or fallback_size) / 10
+ return {
+ design_size, design_range_top, design_range_bottom,
+ }
+ end
+
+ return false
+end
+
+local get_english_names = function (metadata)
+ local names = metadata.names
+ local english_names
+
+ if names then
+ --inspect(names)
+ for _, raw_namedata in next, names do
+ if raw_namedata.lang == "English (US)" then
+ return raw_namedata.names
+ end
+ end
+ end
+
+ -- no (English) names table, probably a broken font
+ report("both", 3, "db",
+ "%s: missing or broken English names table.", basename)
+ return { fontname = metadata.fontname,
+ fullname = metadata.fullname, }
+end
+
+--[[--
+ In case of broken PS names we set some dummies. However, we cannot
+ directly modify the font data as returned by fontloader.open() because
+ it is a userdata object.
+
+ For this reason we copy what is necessary whilst keeping the table
+ structure the same as in the tfmdata.
+--]]--
+local get_raw_info = function (metadata, basename)
+ local fullname
+ local fontname
+ local psname
+
+ local validation_state = metadata.validation_state
+ if validation_state
+ and tablecontains (validation_state, "bad_ps_fontname")
+ then
+ --- Broken names table, e.g. avkv.ttf with UTF-16 strings;
+ --- we put some dummies in place like the fontloader
+ --- (font-otf.lua) does.
+ report("both", 3, "db",
+ "%s has invalid postscript font names, using dummies.",
+ basename)
+ fontname = "bad-fontname-" .. basename
+ fullname = "bad-fullname-" .. basename
+ else
+ fontname = metadata.fontname
+ fullname = metadata.fullname
+ end
+
+ return {
+ familyname = metadata.familyname,
+ fontname = fontname,
+ fontstyle_name = metadata.fontstyle_name,
+ fullname = fullname,
+ italicangle = metadata.italicangle,
+ names = metadata.names,
+ pfminfo = metadata.pfminfo,
+ units_per_em = metadata.units_per_em,
+ version = metadata.version,
+ design_size = metadata.design_size,
+ design_range_top = metadata.design_range_top,
+ design_range_bottom = metadata.design_range_bottom,
+ }
+end
+
+local organize_namedata = function (rawinfo,
+ english_names,
+ basename,
+ info)
+ local default_name = english_names.compatfull
+ or english_names.fullname
+ or english_names.postscriptname
+ or rawinfo.fullname
+ or rawinfo.fontname
+ or info.fullname
+ or info.fontname
+ local default_family = english_names.preffamily
+ or english_names.family
+ or rawinfo.familyname
+ or info.familyname
+-- local default_modifier = english_names.prefmodifiers
+-- or english_names.subfamily
+ local fontnames = {
+ --- see
+ --- https://developer.apple.com/fonts/TTRefMan/RM06/Chap6name.html
+ --- http://www.microsoft.com/typography/OTSPEC/name.htm#NameIDs
+ english = {
+ --- where a “compatfull” field is given, the value of “fullname” is
+ --- either identical or differs by separating the style
+ --- with a hyphen and omitting spaces. (According to the
+ --- spec, “compatfull” is “Macintosh only”.)
+ --- Of the three “fullname” fields, this one appears to be the one
+ --- with the entire name given in a legible,
+ --- non-abbreviated fashion, for most fonts at any rate.
+ --- However, in some fonts (e.g. CMU) all three fields are
+ --- identical.
+ fullname = --[[ 18 ]] english_names.compatfull
+ or --[[ 4 ]] english_names.fullname
+ or default_name,
+ --- we keep both the “preferred family” and the “family”
+ --- values around since both are valid but can turn out
+ --- quite differently, e.g. with Latin Modern:
+ --- preffamily: “Latin Modern Sans”,
+ --- family: “LM Sans 10”
+ preffamily = --[[ 16 ]] english_names.preffamilyname,
+ family = --[[ 1 ]] english_names.family or default_family,
+ prefmodifiers = --[[ 17 ]] english_names.prefmodifiers,
+ subfamily = --[[ 2 ]] english_names.subfamily,
+ psname = --[[ 6 ]] english_names.postscriptname,
+ },
+
+ metadata = {
+ fullname = rawinfo.fullname,
+ fontname = rawinfo.fontname,
+ familyname = rawinfo.familyname,
+ },
+
+ info = {
+ fullname = info.fullname,
+ familyname = info.familyname,
+ fontname = info.fontname,
+ },
+ }
+
+ -- see http://www.microsoft.com/typography/OTSPEC/features_pt.htm#size
+ if rawinfo.fontstyle_name then
+ --- not present in all fonts, often differs from the preferred
+ --- subfamily as well as subfamily fields, e.g. with
+ --- LMSans10-BoldOblique:
+ --- subfamily: “Bold Italic”
+ --- prefmodifiers: “10 Bold Oblique”
+ --- fontstyle_name: “Bold Oblique”
+ for _, name in next, rawinfo.fontstyle_name do
+ if name.lang == 1033 then --- I hate magic numbers
+ fontnames.fontstyle_name = name.name
+ end
+ end
+ end
+
+ return {
+ sanitized = sanitize_fontnames (fontnames),
+ fontname = rawinfo.fontname,
+ fullname = rawinfo.fullname,
+ familyname = rawinfo.familyname,
+ }
+end
+
+
+local dashsplitter = lpegsplitat "-"
+
+local split_fontname = function (fontname)
+ --- sometimes the style hides in the latter part of the
+ --- fontname, separated by a dash, e.g. “Iwona-Regular”,
+ --- “GFSSolomos-Regular”
+ local splitted = { lpegmatch (dashsplitter, fontname) }
+ if next (splitted) then
+ return sanitize_fontname (splitted [#splitted])
+ end
+end
+
+local organize_styledata = function (fontname,
+ metadata,
+ english_names,
+ info)
+ local pfminfo = metadata.pfminfo
+ local names = metadata.names
+
+ return {
+ --- see http://www.microsoft.com/typography/OTSPEC/features_pt.htm#size
+ size = get_size_info (metadata),
+ weight = pfminfo.weight or 400,
+ split = split_fontname (fontname),
+ width = pfminfo.width,
+ italicangle = metadata.italicangle,
+ --- this is for querying, see www.ntg.nl/maps/40/07.pdf for details
+ units_per_em = metadata.units_per_em,
+ version = metadata.version,
+ }
+end
+
+--[[doc--
+The data inside an Opentype font file can be quite heterogeneous.
+Thus in order to get the relevant information, parts of the original
+table as returned by the font file reader need to be relocated.
+--doc]]--
+
+--- string -> int -> bool -> string -> fontentry
+
+ot_fullinfo = function (filename,
+ subfont,
+ location,
+ basename,
+ format,
+ info)
+
+ local metadata = load_font_file (filename, subfont)
+ if not metadata then
+ return nil
+ end
+
+ local rawinfo = get_raw_info (metadata, basename)
+ --- Closing the file manually is a tad faster and more memory
+ --- efficient than having it closed by the gc
+ fontloaderclose (metadata)
+
+ local english_names = get_english_names (rawinfo)
+ local namedata = organize_namedata (rawinfo,
+ english_names,
+ basename,
+ info)
+ local style = organize_styledata (namedata.fontname,
+ rawinfo,
+ english_names,
+ info)
+
+ local res = {
+ file = { base = basename,
+ full = filename,
+ subfont = subfont,
+ location = location or "system" },
+ format = format,
+ names = namedata,
+ style = style,
+ version = rawinfo.version,
+ }
+ return res
+end
+
+--[[doc--
+
+ Type1 font inspector. In comparison with OTF, PFB’s contain a good
+ deal less name fields which makes it tricky in some parts to find a
+ meaningful representation for the database.
+
+ Good read: http://www.adobe.com/devnet/font/pdfs/5004.AFM_Spec.pdf
+
+--doc]]--
+
+--- string -> int -> bool -> string -> fontentry
+
+t1_fullinfo = function (filename, _subfont, location, basename, format)
+ local sanitized
+ local metadata = load_font_file (filename)
+ local fontname = metadata.fontname
+ local fullname = metadata.fullname
+ local familyname = metadata.familyname
+ local italicangle = metadata.italicangle
+ local splitstyle = split_fontname (fontname)
+ local style = ""
+ local weight
+
+ sanitized = sanitize_fontnames ({
+ fontname = fontname,
+ psname = fullname,
+ pfullname = fullname,
+ metafamily = family,
+ familyname = familyname,
+ weight = metadata.weight, --- string identifier
+ prefmodifiers = style,
+ })
+
+ weight = sanitized.weight
+
+ if weight == "bold" then
+ style = weight
+ end
+
+ if italicangle ~= 0 then
+ style = style .. "italic"
+ end
+
+ return {
+ basename = basename,
+ fullpath = filename,
+ subfont = false,
+ location = location or "system",
+ format = format,
+ fullname = sanitized.fullname,
+ fontname = sanitized.fontname,
+ familyname = sanitized.familyname,
+ plainname = fullname,
+ splainname = sanitized.fullname,
+ psname = sanitized.fontname,
+ version = metadata.version,
+ size = false,
+ splitstyle = splitstyle,
+ fontstyle_name = style ~= "" and style or weight,
+ weight = metadata.pfminfo.weight or 400,
+ italicangle = italicangle,
+ }
+end
+
+local loaders = {
+ dfont = ot_fullinfo,
+ otf = ot_fullinfo,
+ ttc = ot_fullinfo,
+ ttf = ot_fullinfo,
+
+ pfb = t1_fullinfo,
+ pfa = t1_fullinfo,
+}
+
+--- not side-effect free!
+
+local compare_timestamps = function (fullname,
+ currentstatus,
+ currententrystatus,
+ currentmappings,
+ targetstatus,
+ targetentrystatus,
+ targetmappings)
+
+ local currenttimestamp = currententrystatus
+ and currententrystatus.timestamp
+ local targettimestamp = lfsattributes (fullname, "modification")
+
+ if targetentrystatus ~= nil
+ and targetentrystatus.timestamp == targettimestamp then
+ report ("log", 3, "db", "Font %q already read.", fullname)
+ return false
+ end
+
+ targetentrystatus.timestamp = targettimestamp
+ targetentrystatus.index = targetentrystatus.index or { }
+
+ if currenttimestamp == targettimestamp
+ and not targetentrystatus.index [1]
+ then
+ --- copy old namedata into new
+
+ for _, currentindex in next, currententrystatus.index do
+
+ local targetindex = #targetentrystatus.index
+ local fullinfo = currentmappings [currentindex]
+ local location = #targetmappings + 1
+
+ targetmappings [location] = fullinfo
+ targetentrystatus.index [targetindex + 1] = location
+ end
+
+ report ("log", 3, "db", "Font %q already indexed.", fullname)
+
+ return false
+ end
+
+ return true
+end
+
+local insert_fullinfo = function (fullname,
+ basename,
+ n_font,
+ loader,
+ format,
+ location,
+ targetmappings,
+ targetentrystatus,
+ info)
+
+ local subfont
+ if n_font ~= false then
+ subfont = n_font - 1
+ else
+ subfont = false
+ n_font = 1
+ end
+
+ local fullinfo = loader (fullname, subfont,
+ location, basename,
+ format, info)
+
+ if not fullinfo then
+ return false
+ end
+
+ local index = targetentrystatus.index [n_font]
+
+ if not index then
+ index = #targetmappings + 1
+ end
+
+ targetmappings [index] = fullinfo
+ targetentrystatus.index [n_font] = index
+
+ return true
+end
+
+
+
+--- we return true if the font is new or re-indexed
+--- string -> dbobj -> dbobj -> bool
+
+local read_font_names = function (fullname,
+ currentnames,
+ targetnames,
+ location)
+
+ local targetmappings = targetnames.mappings
+ local targetstatus = targetnames.status --- by full path
+ local targetentrystatus = targetstatus [fullname]
+
+ if targetentrystatus == nil then
+ targetentrystatus = { }
+ targetstatus [fullname] = targetentrystatus
+ end
+
+ local currentmappings = currentnames.mappings
+ local currentstatus = currentnames.status
+ local currententrystatus = currentstatus [fullname]
+
+ local basename = filebasename (fullname)
+ local barename = filenameonly (fullname)
+ local entryname = fullname
+
+ if location == "texmf" then
+ entryname = basename
+ end
+
+ --- 1) skip if blacklisted
+
+ if names.blacklist[fullname] or names.blacklist[basename] then
+ report("log", 2, "db",
+ "Ignoring blacklisted font %q.", fullname)
+ return false
+ end
+
+ --- 2) skip if known with same timestamp
+
+ if not compare_timestamps (fullname,
+ currentstatus,
+ currententrystatus,
+ currentmappings,
+ targetstatus,
+ targetentrystatus,
+ targetmappings)
+ then
+ return false
+ end
+
+ --- 3) new font; choose a loader, abort if unknown
+
+ local format = stringlower (filesuffix (basename))
+ local loader = loaders [format] --- ot_fullinfo, t1_fullinfo
+
+ if not loader then
+ report ("both", 0, "db",
+ "Unknown format: %q, skipping.", format)
+ return false
+ end
+
+ --- 4) get basic info, abort if fontloader can’t read it
+
+ local info = fontloaderinfo (fullname)
+
+ if not info then
+ report ("log", 1, "db",
+ "Failed to read basic information from %q", basename)
+ return false
+ end
+
+
+ --- 5) check for subfonts and process each of them
+
+ if type (info) == "table" and #info > 1 then --- ttc
+
+ local success = false --- true if at least one subfont got read
+
+ for n_font = 1, #info do
+ if insert_fullinfo (fullname, basename, n_font,
+ loader, format, location,
+ targetmappings, targetentrystatus,
+ info)
+ then
+ success = true
+ end
+ end
+
+ return success
+ end
+
+ return insert_fullinfo (fullname, basename, false,
+ loader, format, location,
+ targetmappings, targetentrystatus,
+ info)
+end
+
+local path_normalize
+do
+ --- os.type and os.name are constants so we
+ --- choose a normalization function in advance
+ --- instead of testing with every call
+ local os_type, os_name = os.type, os.name
+ local filecollapsepath = filecollapsepath
+ local lfsreadlink = lfs.readlink
+
+ --- windows and dos
+ if os_type == "windows" or os_type == "msdos" then
+ --- ms platfom specific stuff
+ path_normalize = function (path)
+ path = stringgsub(path, '\\', '/')
+ path = stringlower(path)
+ path = filecollapsepath(path)
+ return path
+ end
+--[[doc--
+ The special treatment for cygwin was removed with a patch submitted
+ by Ken Brown.
+ Reference: http://cygwin.com/ml/cygwin/2013-05/msg00006.html
+--doc]]--
+
+ else -- posix
+ path_normalize = function (path)
+ local dest = lfsreadlink(path)
+ if dest then
+ if kpsereadable_file(dest) then
+ path = dest
+ elseif kpsereadable_file(filejoin(filedirname(path), dest)) then
+ path = filejoin(file.dirname(path), dest)
+ else
+ -- broken symlink?
+ end
+ end
+ path = filecollapsepath(path)
+ return path
+ end
+ end
+end
+
+fonts.path_normalize = path_normalize
+
+names.blacklist = { }
+
+local blacklist = names.blacklist
+local p_blacklist --- prefixes of dirs
+
+--- string list -> string list
+local collapse_prefixes = function (lst)
+ --- avoid redundancies in blacklist
+ if #lst < 2 then
+ return lst
+ end
+
+ tablesort(lst)
+ local cur = lst[1]
+ local result = { cur }
+ for i=2, #lst do
+ local elm = lst[i]
+ if stringsub(elm, 1, #cur) ~= cur then
+ --- different prefix
+ cur = elm
+ result[#result+1] = cur
+ end
+ end
+ return result
+end
+
+--- string list -> string list -> (string, bool) hash_t
+local create_blacklist = function (blacklist, whitelist)
+ local result = { }
+ local dirs = { }
+
+ report("info", 2, "db", "Blacklisting %d files and directories.",
+ #blacklist)
+ for i=1, #blacklist do
+ local entry = blacklist[i]
+ if lfsisdir(entry) then
+ dirs[#dirs+1] = entry
+ else
+ result[blacklist[i]] = true
+ end
+ end
+
+ report("info", 2, "db", "Whitelisting %d files.", #whitelist)
+ for i=1, #whitelist do
+ result[whitelist[i]] = nil
+ end
+
+ dirs = collapse_prefixes(dirs)
+
+ --- build the disjunction of the blacklisted directories
+ for i=1, #dirs do
+ local p_dir = P(dirs[i])
+ if p_blacklist then
+ p_blacklist = p_blacklist + p_dir
+ else
+ p_blacklist = p_dir
+ end
+ end
+
+ if p_blacklist == nil then
+ --- always return false
+ p_blacklist = Cc(false)
+ end
+
+ return result
+end
+
+--- unit -> unit
+read_blacklist = function ()
+ local files = {
+ kpselookup ("luaotfload-blacklist.cnf",
+ {all=true, format="tex"})
+ }
+ local blacklist = { }
+ local whitelist = { }
+
+ if files and type(files) == "table" then
+ for _, path in next, files do
+ for line in iolines (path) do
+ line = stringstrip(line) -- to get rid of lines like " % foo"
+ local first_chr = stringsub(line, 1, 1)
+ if first_chr == "%" or stringis_empty(line) then
+ -- comment or empty line
+ elseif first_chr == "-" then
+ report ("both", 3, "db",
+ "Whitelisted file %q via %q.",
+ line, path)
+ whitelist[#whitelist+1] = stringsub(line, 2, -1)
+ else
+ local cmt = stringfind(line, "%%")
+ if cmt then
+ line = stringsub(line, 1, cmt - 1)
+ end
+ line = stringstrip(line)
+ report ("both", 3, "db",
+ "Blacklisted file %q via %q.",
+ line, path)
+ blacklist[#blacklist+1] = line
+ end
+ end
+ end
+ end
+ names.blacklist = create_blacklist(blacklist, whitelist)
+end
+
+local p_font_filter
+
+do
+ local current_formats = { }
+
+ local extension_pattern = function (list)
+ local pat
+ for i=#list, 1, -1 do
+ local e = list[i]
+ if not pat then
+ pat = P(e)
+ else
+ pat = pat + P(e)
+ end
+ end
+ pat = pat * P(-1)
+ return (1 - pat)^1 * pat
+ end
+
+ --- small helper to adjust the font filter pattern (--formats
+ --- option)
+
+ set_font_filter = function (formats)
+
+ if not formats or type (formats) ~= "string" then
+ return
+ end
+
+ if stringsub (formats, 1, 1) == "+" then -- add
+ formats = lpegmatch (splitcomma, stringsub (formats, 2))
+ if formats then
+ current_formats = tableappend (current_formats, formats)
+ end
+ elseif stringsub (formats, 1, 1) == "-" then -- add
+ formats = lpegmatch (splitcomma, stringsub (formats, 2))
+ if formats then
+ local newformats = { }
+ for i = 1, #current_formats do
+ local fmt = current_formats[i]
+ local include = true
+ for j = 1, #formats do
+ if current_formats[i] == formats[j] then
+ include = false
+ goto skip
+ end
+ end
+ newformats[#newformats+1] = fmt
+ ::skip::
+ end
+ current_formats = newformats
+ end
+ else -- set
+ formats = lpegmatch (splitcomma, formats)
+ if formats then
+ current_formats = formats
+ end
+ end
+
+ p_font_filter = extension_pattern (current_formats)
+ end
+
+ get_font_filter = function (formats)
+ return tablefastcopy (current_formats)
+ end
+
+ --- initialize
+ set_font_filter (luaotfloadconfig.formats)
+end
+
+local process_dir_tree
+process_dir_tree = function (acc, dirs)
+ if not next (dirs) then --- done
+ return acc
+ end
+
+ local pwd = lfscurrentdir ()
+ local dir = dirs[#dirs]
+ dirs[#dirs] = nil
+
+ if lfschdir (dir) then
+ lfschdir (pwd)
+
+ local newfiles = { }
+ local blacklist = names.blacklist
+ for ent in lfsdir (dir) do
+ --- filter right away
+ if ent ~= "." and ent ~= ".." and not blacklist[ent] then
+ local fullpath = dir .. "/" .. ent
+ if lfsisdir (fullpath)
+ and not lpegmatch (p_blacklist, fullpath)
+ then
+ dirs[#dirs+1] = fullpath
+ elseif lfsisfile (fullpath) then
+ ent = stringlower (ent)
+
+ if lpegmatch (p_font_filter, ent) then
+ if filesuffix (ent) == "afm" then
+ --- fontloader.open() will load the afm
+ --- iff both files are in the same directory
+ local pfbpath = filereplacesuffix
+ (fullpath, "pfb")
+ if lfsisfile (pfbpath) then
+ newfiles[#newfiles+1] = pfbpath
+ end
+ else
+ newfiles[#newfiles+1] = fullpath
+ end
+ end
+
+ end
+ end
+ end
+ return process_dir_tree (tableappend (acc, newfiles), dirs)
+ end
+ --- cannot cd; skip
+ return process_dir_tree (acc, dirs)
+end
+
+local process_dir = function (dir)
+ local pwd = lfscurrentdir ()
+ if lfschdir (dir) then
+ lfschdir (pwd)
+
+ local files = { }
+ local blacklist = names.blacklist
+ for ent in lfsdir (dir) do
+ if ent ~= "." and ent ~= ".." and not blacklist[ent] then
+ local fullpath = dir .. "/" .. ent
+ if lfsisfile (fullpath) then
+ ent = stringlower (ent)
+ if lpegmatch (p_font_filter, ent)
+ then
+ if filesuffix (ent) == "afm" then
+ --- fontloader.open() will load the afm
+ --- iff both files are in the same
+ --- directory
+ local pfbpath = filereplacesuffix
+ (fullpath, "pfb")
+ if lfsisfile (pfbpath) then
+ files[#files+1] = pfbpath
+ end
+ else
+ files[#files+1] = fullpath
+ end
+ end
+ end
+ end
+ end
+ return files
+ end
+ return { }
+end
+
+--- string -> bool -> string list
+local find_font_files = function (root, recurse)
+ if lfsisdir (root) then
+ if recurse == true then
+ return process_dir_tree ({}, { root })
+ else --- kpathsea already delivered the necessary subdirs
+ return process_dir (root)
+ end
+ end
+end
+
+--- truncate_string -- Cut the first part of a string to fit it
+--- into a given terminal width. The parameter “restrict” (int)
+--- indicates the number of characters already consumed on the
+--- line.
+local truncate_string = function (str, restrict)
+ local tw = luaotfloadconfig.termwidth
+ local wd = tw - restrict
+ local len = utf8len (str)
+ if wd - len < 0 then
+ --- combined length exceeds terminal,
+ str = ".." .. stringsub(str, len - wd + 2)
+ end
+ return str
+end
+
+--[[doc--
+
+ scan_dir() scans a directory and populates the list of fonts
+ with all the fonts it finds.
+
+ · dirname : name of the directory to scan
+ · currentnames : current font db object
+ · targetnames : font db object to fill
+ · dry_run : don’t touch anything
+
+--doc]]--
+
+--- string -> dbobj -> dbobj -> bool -> bool -> (int * int)
+
+local scan_dir = function (dirname, currentnames, targetnames,
+ dry_run, location)
+ if lpegmatch (p_blacklist, dirname) then
+ report ("both", 4, "db",
+ "Skipping blacklisted directory %s.", dirname)
+ --- ignore
+ return 0, 0
+ end
+ local found = find_font_files (dirname, location ~= "texmf")
+ if not found then
+ report ("both", 4, "db",
+ "No such directory: %q; skipping.", dirname)
+ return 0, 0
+ end
+ report ("both", 4, "db", "Scanning directory %s.", dirname)
+
+ local n_new = 0 --- total of fonts collected
+ local n_found = #found
+ local max_fonts = luaotfloadconfig.max_fonts
+
+ report ("both", 4, "db", "%d font files detected.", n_found)
+ for j=1, n_found do
+ if max_fonts and fonts_read >= max_fonts then
+ break
+ end
+
+ local fullname = found[j]
+ fullname = path_normalize(fullname)
+ local new
+
+ if dry_run == true then
+ local truncated = truncate_string (fullname, 43)
+ report ("log", 2, "db",
+ "Would have been loading %s.", fullname)
+ report_status ("term", "db",
+ "Would have been loading %s", truncated)
+ else
+ local truncated = truncate_string (fullname, 32)
+ report ("log", 2, "db", "Loading font %s.", fullname)
+ report_status ("term", "db", "Loading font %s", truncated)
+ local new = read_font_names (fullname, currentnames,
+ targetnames, texmf)
+ if new == true then
+ fonts_read = fonts_read + 1
+ n_new = n_new + 1
+ end
+ end
+ end
+ report ("both", 4, "db", "Done. %d fonts indexed in %q.",
+ n_found, dirname)
+ return n_found, n_new
+end
+
+--- string list -> string list
+local filter_out_pwd = function (dirs)
+ local result = { }
+ local pwd = path_normalize (lpegmatch (stripslashes,
+ lfscurrentdir ()))
+ for i = 1, #dirs do
+ --- better safe than sorry
+ local dir = path_normalize (lpegmatch (stripslashes, dirs[i]))
+ if not (dir == "." or dir == pwd) then
+ result[#result+1] = dir
+ end
+ end
+ return result
+end
+
+local path_separator = ostype == "windows" and ";" or ":"
+
+--[[doc--
+
+ scan_texmf_fonts() scans all fonts in the texmf tree through the
+ kpathsea variables OPENTYPEFONTS and TTFONTS of texmf.cnf.
+ The current working directory comes as “.” (texlive) or absolute
+ path (miktex) and will always be filtered out.
+
+--doc]]--
+
+--- dbobj -> dbobj -> bool? -> (int * int)
+
+local scan_texmf_fonts = function (currentnames, targetnames, dry_run)
+
+ local n_scanned, n_new, fontdirs = 0, 0
+ local osfontdir = kpseexpand_path "$OSFONTDIR"
+
+ if stringis_empty (osfontdir) then
+ report ("info", 1, "db", "Scanning TEXMF fonts...")
+ else
+ report ("info", 1, "db", "Scanning TEXMF and OS fonts...")
+ if log.get_loglevel () > 3 then
+ local osdirs = filesplitpath (osfontdir)
+ report ("info", 0, "db",
+ "$OSFONTDIR has %d entries:", #osdirs)
+ for i = 1, #osdirs do
+ report ("info", 0, "db", "[%d] %s", i, osdirs[i])
+ end
+ end
+ end
+
+ fontdirs = kpseexpand_path "$OPENTYPEFONTS"
+ fontdirs = fontdirs .. path_separator .. kpseexpand_path "$TTFONTS"
+ fontdirs = fontdirs .. path_separator .. kpseexpand_path "$T1FONTS"
+
+ if not stringis_empty (fontdirs) then
+ local tasks = filter_out_pwd (filesplitpath (fontdirs))
+ report ("info", 3, "db",
+ "Initiating scan of %d directories.", #tasks)
+ report_status_start (2, 4)
+ for _, d in next, tasks do
+ local found, new = scan_dir (d, currentnames, targetnames,
+ dry_run, "texmf")
+ n_scanned = n_scanned + found
+ n_new = n_new + new
+ end
+ report_status_stop ("term", "db", "Scanned %d files, %d new.", n_scanned, n_new)
+ end
+
+ return n_scanned, n_new
+end
+
+--- TODO stuff those paths into some writable table
+--- unit -> string list
+local function get_os_dirs ()
+ if os.name == 'macosx' then
+ return {
+ filejoin(kpseexpand_path('~'), "Library/Fonts"),
+ "/Library/Fonts",
+ "/System/Library/Fonts",
+ "/Network/Library/Fonts",
+ }
+ elseif os.type == "windows" or os.type == "msdos" then
+ local windir = osgetenv("WINDIR")
+ return { filejoin(windir, 'Fonts') }
+ else
+ local fonts_conves = { --- plural, much?
+ "/usr/local/etc/fonts/fonts.conf",
+ "/etc/fonts/fonts.conf",
+ }
+ local os_dirs = read_fonts_conf(fonts_conves, find_files)
+ return os_dirs
+ end
+ return {}
+end
+
+--[[doc--
+
+ scan_os_fonts() scans the OS fonts through
+ - fontconfig for Unix (reads the fonts.conf file[s] and scans the
+ directories)
+ - a static set of directories for Windows and MacOSX
+
+ **NB**: If $OSFONTDIR is nonempty, as it appears to be by default
+ on Windows setups, the system fonts will have already been
+ processed while scanning the TEXMF. Thus, this function is
+ never called.
+
+--doc]]--
+
+--- dbobj -> dbobj -> bool? -> (int * int)
+local scan_os_fonts = function (currentnames,
+ targetnames,
+ dry_run)
+
+ local n_scanned, n_new = 0, 0
+ report ("info", 1, "db", "Scanning OS fonts...")
+ report ("info", 2, "db",
+ "Searching in static system directories...")
+
+ report_status_start (2, 4)
+ for _, d in next, get_os_dirs () do
+ local found, new = scan_dir (d, currentnames,
+ targetnames, dry_run)
+ n_scanned = n_scanned + found
+ n_new = n_new + new
+ end
+ report_status_stop ("term", "db", "Scanned %d files, %d new.", n_scanned, n_new)
+
+ return n_scanned, n_new
+end
+
+--- unit -> (bool, lookup_cache)
+flush_lookup_cache = function ()
+ lookup_cache = { }
+ collectgarbage "collect"
+ return true, lookup_cache
+end
+
+
+--- fontentry list -> filemap
+
+local generate_filedata = function (mappings)
+
+ report ("both", 2, "db", "Creating filename map.")
+
+ local nmappings = #mappings
+
+ local files = {
+ bare = {
+ ["local"] = { },
+ system = { }, --- mapped to mapping format -> index in full
+ texmf = { }, --- mapped to mapping format -> “true”
+ },
+ base = {
+ ["local"] = { },
+ system = { }, --- mapped to index in “full”
+ texmf = { }, --- set; all values are “true”
+ },
+ full = { }, --- non-texmf
+ }
+
+ local base = files.base
+ local bare = files.bare
+ local full = files.full
+
+ local conflicts = {
+ basenames = 0,
+ barenames = 0,
+ }
+
+ for index = 1, nmappings do
+ local entry = mappings [index]
+
+ local filedata = entry.file
+ local format
+ local location
+ local fullpath
+ local basename
+ local barename
+ local subfont
+
+ if filedata then --- new entry
+ format = entry.format --- otf, afm, ...
+ location = filedata.location --- texmf, system, ...
+ fullpath = filedata.full
+ basename = filedata.base
+ barename = filenameonly (fullpath)
+ subfont = filedata.subfont
+ else
+ format = entry.format --- otf, afm, ...
+ location = entry.location --- texmf, system, ...
+ fullpath = entry.fullpath
+ basename = entry.basename
+ barename = filenameonly (fullpath)
+ subfont = entry.subfont
+ end
+
+ entry.index = index
+
+ --- 1) add to basename table
+
+ local inbase = base [location] --- no format since the suffix is known
+
+ if inbase then
+ local present = inbase [basename]
+ if present then
+ report ("both", 4, "db",
+ "Conflicting basename: %q already indexed \z
+ in category %s, ignoring.",
+ barename, location)
+ conflicts.basenames = conflicts.basenames + 1
+
+ --- track conflicts per font
+ local conflictdata = entry.conflicts
+
+ if not conflictdata then
+ entry.conflicts = { basename = present }
+ else -- some conflicts already detected
+ conflictdata.basename = present
+ end
+
+ else
+ inbase [basename] = index
+ end
+ else
+ inbase = { basename = index }
+ base [location] = inbase
+ end
+
+ --- 2) add to barename table
+
+ local inbare = bare [location] [format]
+
+ if inbare then
+ local present = inbare [barename]
+ if present then
+ report ("both", 4, "db",
+ "Conflicting barename: %q already indexed \z
+ in category %s/%s, ignoring.",
+ barename, location, format)
+ conflicts.barenames = conflicts.barenames + 1
+
+ --- track conflicts per font
+ local conflictdata = entry.conflicts
+
+ if not conflictdata then
+ entry.conflicts = { barename = present }
+ else -- some conflicts already detected
+ conflictdata.barename = present
+ end
+
+ else
+ inbare [barename] = index
+ end
+ else
+ inbare = { [barename] = index }
+ bare [location] [format] = inbare
+ end
+
+ --- 3) add to fullpath map
+
+ full [index] = fullpath
+ end
+
+ return files
+end
+
+local pick_style
+local check_regular
+
+do
+ local splitfontname = lpeg.splitat "-"
+
+ local choose_exact = function (field)
+ --- only clean matches, without guessing
+ if italic_synonym [field] then
+ return "i"
+ end
+
+ if field == "bold" then
+ return "b"
+ end
+
+ if field == "bolditalic" or field == "boldoblique" then
+ return "bi"
+ end
+
+ return false
+ end
+
+ pick_style = function (fontstyle_name,
+ prefmodifiers,
+ subfamily,
+ splitstyle)
+ local style
+ if fontstyle_name then
+ style = choose_exact (fontstyle_name)
+ end
+ if not style then
+ if prefmodifiers then
+ style = choose_exact (prefmodifiers)
+ elseif subfamily then
+ style = choose_exact (subfamily)
+ end
+ end
+ return style
+ end
+
+ pick_fallback_style = function (italicangle, weight)
+ --- more aggressive, but only to determine bold faces
+ if weight > 500 then --- bold spectrum matches
+ if italicangle == 0 then
+ return tostring (weight)
+ else
+ return tostring (weight) .. "i"
+ end
+ end
+ return false
+ end
+
+ --- we use only exact matches here since there are constructs
+ --- like “regularitalic” (Cabin, Bodoni Old Fashion)
+
+ check_regular = function (fontstyle_name,
+ prefmodifiers,
+ subfamily,
+ splitstyle,
+ italicangle,
+ weight)
+
+ if fontstyle_name then
+ return regular_synonym [fontstyle_name]
+ elseif prefmodifiers then
+ return regular_synonym [prefmodifiers]
+ elseif subfamily then
+ return regular_synonym [subfamily]
+ elseif splitstyle then
+ return regular_synonym [splitstyle]
+ elseif italicangle == 0 and weight == 400 then
+ return true
+ end
+
+ return nil
+ end
+end
+
+local pull_values = function (entry)
+ local file = entry.file
+ local names = entry.names
+ local style = entry.style
+ local sanitized = names.sanitized
+ local english = sanitized.english
+ local info = sanitized.info
+ local metadata = sanitized.metadata
+
+ --- pull file info ...
+ entry.basename = file.base
+ entry.fullpath = file.full
+ entry.location = file.location
+ entry.subfont = file.subfont
+
+ --- pull name info ...
+ entry.psname = english.psname
+ entry.fontname = info.fontname or metadata.fontname
+ entry.fullname = english.fullname or info.fullname
+ entry.splainname = metadata.fullname
+ entry.prefmodifiers = english.prefmodifiers
+ local metafamily = metadata.familyname
+ local familyname = english.preffamily or english.family
+ entry.familyname = familyname
+ if familyname ~= metafamily then
+ entry.metafamily = metadata.familyname
+ end
+ entry.fontstyle_name = sanitized.fontstyle_name
+ entry.plainname = names.fullname
+ entry.subfamily = english.subfamily
+
+ --- pull style info ...
+ entry.italicangle = style.italicangle
+ entry.size = style.size
+ entry.splitstyle = style.split
+ entry.weight = style.weight
+
+ if luaotfloadconfig.strip == true then
+ entry.file = nil
+ entry.names = nil
+ entry.style = nil
+ end
+end
+
+local add_family = function (name, subtable, modifier, entry)
+ if not name then --- probably borked font
+ return
+ end
+ local familytable = subtable [name]
+ if not familytable then
+ familytable = { }
+ subtable [name] = familytable
+ end
+
+ local size = entry.size
+
+ familytable [#familytable + 1] = {
+ index = entry.index,
+ modifier = modifier,
+ }
+end
+
+local get_subtable = function (families, entry)
+ local location = entry.location
+ local format = entry.format
+ local subtable = families [location] [format]
+ if not subtable then
+ subtable = { }
+ families [location] [format] = subtable
+ end
+ return subtable
+end
+
+local collect_families = function (mappings)
+
+ report ("info", 2, "db", "Analyzing families.")
+
+ local families = {
+ ["local"] = { },
+ system = { },
+ texmf = { },
+ }
+
+ for i = 1, #mappings do
+
+ local entry = mappings [i]
+
+ if entry.file then
+ pull_values (entry)
+ end
+
+ local subtable = get_subtable (families, entry)
+
+ local familyname = entry.familyname
+ local metafamily = entry.metafamily
+ local fontstyle_name = entry.fontstyle_name
+ local prefmodifiers = entry.prefmodifiers
+ local subfamily = entry.subfamily
+
+ local weight = entry.weight
+ local italicangle = entry.italicangle
+ local splitstyle = entry.splitstyle
+
+ local modifier = pick_style (fontstyle_name,
+ prefmodifiers,
+ subfamily,
+ splitstyle)
+
+ if not modifier then --- regular, exact only
+ modifier = check_regular (fontstyle_name,
+ prefmodifiers,
+ subfamily,
+ splitstyle,
+ italicangle,
+ weight)
+ end
+
+ if modifier then
+ add_family (familyname, subtable, modifier, entry)
+ --- registering the metafamilies is unreliable within the
+ --- same table as identifiers might interfere with an
+ --- unmarked style that lacks a metafamily, e.g.
+ ---
+ --- iwona condensed regular ->
+ --- family: iwonacond
+ --- metafamily: iwona
+ --- iwona regular ->
+ --- family: iwona
+ --- metafamily: ø
+ ---
+ --- Both would be registered as under the same family,
+ --- i.e. “iwona”, and depending on the loading order
+ --- the query “name:iwona” can resolve to the condensed
+ --- version instead of the actual unmarked one. The only
+ --- way around this would be to introduce a separate
+ --- table for metafamilies and do fallback queries on it.
+ --- At the moment this is not pressing enough to justify
+ --- further increasing the index size, maybe if need
+ --- arises from the user side.
+-- if metafamily and metafamily ~= familyname then
+-- add_family (metafamily, subtable, modifier, entry)
+-- end
+ elseif weight > 500 then -- in bold spectrum
+ modifier = pick_fallback_style (italicangle, weight)
+ if modifier then
+ add_family (familyname, subtable, modifier, entry)
+ end
+ end
+ end
+
+ collectgarbage "collect"
+ return families
+end
+
+--[[doc--
+
+ add_bold_spectrum -- For not-quite-bold faces, determine whether
+ they can fill in for a missing bold face slot in a matching family.
+
+ Some families like Lucida do not contain real bold / bold italic
+ members. Instead, they have semibold variants at weight 600 which
+ we must add in a separate pass.
+
+--doc]]--
+
+local bold_spectrum_low = 501 --- 500 is medium, 900 heavy/black
+local bold_weight = 700
+local style_categories = { "r", "b", "i", "bi" }
+local bold_categories = { "b", "bi" }
+
+local group_modifiers = function (mappings, families)
+ report ("info", 2, "db", "Analyzing shapes, weights, and styles.")
+ for location, location_data in next, families do
+ for format, format_data in next, location_data do
+ for familyname, collected in next, format_data do
+ local styledata = { } --- will replace the “collected” table
+ --- First, fill in the ordinary style data that
+ --- fits neatly into the four relevant modifier
+ --- categories.
+ for _, modifier in next, style_categories do
+ local entries
+ for key, info in next, collected do
+ if info.modifier == modifier then
+ if not entries then
+ entries = { }
+ end
+ local index = info.index
+ local entry = mappings [index]
+ local size = entry.size
+ if size then
+ entries [#entries + 1] = {
+ size [1],
+ size [2],
+ size [3],
+ index,
+ }
+ else
+ entries.default = index
+ end
+ collected [key] = nil
+ end
+ styledata [modifier] = entries
+ end
+ end
+
+ --- At this point the family set may still lack
+ --- entries for bold or bold italic. We will fill
+ --- those in using the modifier with the numeric
+ --- weight that is closest to bold (700).
+ if next (collected) then --- there are uncategorized entries
+ for _, modifier in next, bold_categories do
+ if not styledata [modifier] then
+ local closest
+ local minimum = 2^51
+ for key, info in next, collected do
+ local info_modifier = tonumber (info.modifier) and "b" or "bi"
+ if modifier == info_modifier then
+ local index = info.index
+ local entry = mappings [index]
+ local weight = entry.weight
+ local diff = weight < 700 and 700 - weight or weight - 700
+ if diff < minimum then
+ minimum = diff
+ closest = weight
+ end
+ end
+ end
+ if closest then
+ --- We know there is a substitute face for the modifier.
+ --- Now we scan the list again to extract the size data
+ --- in case the shape is available at multiple sizes.
+ local entries = { }
+ for key, info in next, collected do
+ local info_modifier = tonumber (info.modifier) and "b" or "bi"
+ if modifier == info_modifier then
+ local index = info.index
+ local entry = mappings [index]
+ local size = entry.size
+ if entry.weight == closest then
+ if size then
+ entries [#entries + 1] = {
+ size [1],
+ size [2],
+ size [3],
+ index,
+ }
+ else
+ entries.default = index
+ end
+ end
+ end
+ end
+ styledata [modifier] = entries
+ end
+ end
+ end
+ end
+ format_data [familyname] = styledata
+ end
+ end
+ end
+ return families
+end
+
+local cmp_sizes = function (a, b)
+ return a [1] < b [1]
+end
+
+local order_design_sizes = function (families)
+
+ report ("info", 2, "db", "Ordering design sizes.")
+
+ for location, data in next, families do
+ for format, data in next, data do
+ for familyname, data in next, data do
+ for style, data in next, data do
+ tablesort (data, cmp_sizes)
+ end
+ end
+ end
+ end
+
+ return families
+end
+
+local retrieve_namedata = function (currentnames,
+ targetnames,
+ dry_run,
+ n_rawnames,
+ n_newnames)
+
+ local rawnames, new = scan_texmf_fonts (currentnames,
+ targetnames,
+ dry_run)
+
+ n_rawnames = n_rawnames + rawnames
+ n_newnames = n_newnames + new
+
+ rawnames, new = scan_os_fonts (currentnames, targetnames, dry_run)
+
+ n_rawnames = n_rawnames + rawnames
+ n_newnames = n_newnames + new
+
+ return n_rawnames, n_newnames
+end
+
+
+--- dbobj -> stats
+
+local collect_statistics = function (mappings)
+ local sum_dsnsize, n_dsnsize = 0, 0
+
+ local fullname, family, families = { }, { }, { }
+ local subfamily, prefmodifiers, fontstyle_name = { }, { }, { }
+
+ local addtohash = function (hash, item)
+ if item then
+ local times = hash [item]
+ if times then
+ hash [item] = times + 1
+ else
+ hash [item] = 1
+ end
+ end
+ end
+
+ local appendtohash = function (hash, key, value)
+ if key and value then
+ local entry = hash [key]
+ if entry then
+ entry [#entry + 1] = value
+ else
+ hash [key] = { value }
+ end
+ end
+ end
+
+ local addtoset = function (hash, key, value)
+ if key and value then
+ local set = hash [key]
+ if set then
+ set [value] = true
+ else
+ hash [key] = { [value] = true }
+ end
+ end
+ end
+
+ local setsize = function (set)
+ local n = 0
+ for _, _ in next, set do
+ n = n + 1
+ end
+ return n
+ end
+
+ local hashsum = function (hash)
+ local n = 0
+ for _, m in next, hash do
+ n = n + m
+ end
+ return n
+ end
+
+ for _, entry in next, mappings do
+ local style = entry.style
+ local names = entry.names.sanitized
+ local englishnames = names.english
+
+ addtohash (fullname, englishnames.fullname)
+ addtohash (family, englishnames.family)
+ addtohash (subfamily, englishnames.subfamily)
+ addtohash (prefmodifiers, englishnames.prefmodifiers)
+ addtohash (fontstyle_name, names.fontstyle_name)
+
+ addtoset (families, englishnames.family, englishnames.fullname)
+
+ local sizeinfo = entry.style.size
+ if sizeinfo then
+ sum_dsnsize = sum_dsnsize + sizeinfo [1]
+ n_dsnsize = n_dsnsize + 1
+ end
+ end
+
+ --inspect (families)
+
+ local n_fullname = setsize (fullname)
+ local n_family = setsize (family)
+
+ if log.get_loglevel () > 1 then
+ local pprint_top = function (hash, n, set)
+
+ local freqs = { }
+ local items = { }
+
+ for item, value in next, hash do
+ if set then
+ freq = setsize (value)
+ else
+ freq = value
+ end
+ local ifreq = items [freq]
+ if ifreq then
+ ifreq [#ifreq + 1] = item
+ else
+ items [freq] = { item }
+ freqs [#freqs + 1] = freq
+ end
+ end
+
+ tablesort (freqs)
+
+ local from = #freqs
+ local to = from - (n - 1)
+ if to < 1 then
+ to = 1
+ end
+
+ for i = from, to, -1 do
+ local freq = freqs [i]
+ local itemlist = items [freq]
+
+ if type (itemlist) == "table" then
+ itemlist = tableconcat (itemlist, ", ")
+ end
+
+ report ("both", 0, "db",
+ " · %4d × %s.",
+ freq, itemlist)
+ end
+ end
+
+ report ("both", 0, "", "~~~~ font index statistics ~~~~")
+ report ("both", 0, "db",
+ " · Collected %d fonts (%d names) in %d families.",
+ #mappings, n_fullname, n_family)
+ pprint_top (families, 4, true)
+
+ report ("both", 0, "db",
+ " · %d different “subfamily” kinds.",
+ setsize (subfamily))
+ pprint_top (subfamily, 4)
+
+ report ("both", 0, "db",
+ " · %d different “prefmodifiers” kinds.",
+ setsize (prefmodifiers))
+ pprint_top (prefmodifiers, 4)
+
+ report ("both", 0, "db",
+ " · %d different “fontstyle_name” kinds.",
+ setsize (fontstyle_name))
+ pprint_top (fontstyle_name, 4)
+ end
+
+ local mean_dsnsize = 0
+ if n_dsnsize > 0 then
+ mean_dsnsize = sum_dsnsize / n_dsnsize
+ end
+
+ return {
+ mean_dsnsize = mean_dsnsize,
+ names = {
+ fullname = n_fullname,
+ families = n_family,
+ },
+-- style = {
+-- subfamily = subfamily,
+-- prefmodifiers = prefmodifiers,
+-- fontstyle_name = fontstyle_name,
+-- },
+ }
+end
+
+--- force: dictate rebuild from scratch
+--- dry_dun: don’t write to the db, just scan dirs
+
+--- dbobj? -> bool? -> bool? -> dbobj
+update_names = function (currentnames, force, dry_run)
+
+ local targetnames
+
+ if luaotfloadconfig.update_live == false then
+ report ("info", 2, "db",
+ "Skipping database update.")
+ --- skip all db updates
+ return currentnames or name_index
+ end
+
+ local starttime = osgettimeofday ()
+ local n_rawnames, n_newnames = 0, 0
+
+ --[[
+ The main function, scans everything
+ - “targetnames” is the final table to return
+ - force is whether we rebuild it from scratch or not
+ ]]
+ report("both", 1, "db", "Updating the font names database"
+ .. (force and " forcefully." or "."))
+
+ --- pass 1 get raw data: read font files (normal case) or reuse
+ --- information present in index
+
+ if luaotfloadconfig.skip_read == true then
+ --- the difference to a “dry run” is that we don’t search
+ --- for font files entirely. we also ignore the “force”
+ --- parameter since it concerns only the font files.
+ report ("info", 2, "db",
+ "Ignoring font files, reusing old data.")
+ currentnames = load_names (false)
+ targetnames = currentnames
+ else
+ if force then
+ currentnames = initialize_namedata (get_font_filter ())
+ else
+ if not currentnames then
+ currentnames = load_names (dry_run)
+ end
+ if currentnames.meta.version ~= names.version then
+ report ("both", 1, "db", "No font names database or old "
+ .. "one found; generating new one.")
+ currentnames = initialize_namedata (get_font_filter ())
+ end
+ end
+
+ targetnames = initialize_namedata (get_font_filter ())
+
+ read_blacklist ()
+
+ local n_raw, n_new= retrieve_namedata (currentnames,
+ targetnames,
+ dry_run,
+ n_rawnames,
+ n_newnames)
+ report ("info", 3, "db",
+ "Scanned %d font files; %d new entries.",
+ n_rawnames, n_newnames)
+ end
+
+ --- pass 2 (optional): collect some stats about the raw font info
+ if luaotfloadconfig.statistics == true then
+ targetnames.meta.statistics = collect_statistics
+ (targetnames.mappings)
+ end
+
+ --- we always generate the file lookup tables because
+ --- non-texmf entries are redirected there and the mapping
+ --- needs to be 100% consistent
+
+ --- pass 3: build filename table
+ targetnames.files = generate_filedata (targetnames.mappings)
+
+ --- pass 4: build family lookup table
+ targetnames.families = collect_families (targetnames.mappings)
+
+ --- pass 5: arrange style and size info
+ targetnames.families = group_modifiers (targetnames.mappings,
+ targetnames.families)
+
+ --- pass 6: order design size tables
+ targetnames.families = order_design_sizes (targetnames.families)
+
+
+ report ("info", 3, "db",
+ "Rebuilt in %0.f ms.",
+ 1000 * (osgettimeofday () - starttime))
+ name_index = targetnames
+
+ if dry_run ~= true then
+
+ save_names ()
+
+ local success, _lookups = flush_lookup_cache ()
+ if success then
+ local success = save_lookups ()
+ if success then
+ report ("info", 2, "cache",
+ "Lookup cache emptied.")
+ return targetnames
+ end
+ end
+ end
+ return targetnames
+end
+
+--- unit -> bool
+save_lookups = function ( )
+ local path = names.path.lookups
+ local luaname, lucname = path.lua, path.luc
+ if fileiswritable (luaname) and fileiswritable (lucname) then
+ tabletofile (luaname, lookup_cache, true)
+ osremove (lucname)
+ caches.compile (lookup_cache, luaname, lucname)
+ --- double check ...
+ if lfsisfile (luaname) and lfsisfile (lucname) then
+ report ("both", 3, "cache", "Lookup cache saved.")
+ return true
+ end
+ report ("info", 0, "cache", "Could not compile lookup cache.")
+ return false
+ end
+ report ("info", 0, "cache", "Lookup cache file not writable.")
+ if not fileiswritable (luaname) then
+ report ("info", 0, "cache", "Failed to write %s.", luaname)
+ end
+ if not fileiswritable (lucname) then
+ report ("info", 0, "cache", "Failed to write %s.", lucname)
+ end
+ return false
+end
+
+--- save_names() is usually called without the argument
+--- dbobj? -> bool
+save_names = function (currentnames)
+ if not currentnames then
+ currentnames = name_index
+ end
+ local path = names.path.index
+ local luaname, lucname = path.lua, path.luc
+ if fileiswritable (luaname) and fileiswritable (lucname) then
+ osremove (lucname)
+ local gzname = luaname .. ".gz"
+ if luaotfloadconfig.compress then
+ local serialized = tableserialize (currentnames, true)
+ save_gzipped (gzname, serialized)
+ caches.compile (currentnames, "", lucname)
+ else
+ tabletofile (luaname, currentnames, true)
+ caches.compile (currentnames, luaname, lucname)
+ end
+ report ("info", 2, "db", "Font index saved at ...")
+ local success = false
+ if lfsisfile (luaname) then
+ report ("info", 2, "db", "Text: " .. luaname)
+ success = true
+ end
+ if lfsisfile (gzname) then
+ report ("info", 2, "db", "Gzip: " .. gzname)
+ success = true
+ end
+ if lfsisfile (lucname) then
+ report ("info", 2, "db", "Byte: " .. lucname)
+ success = true
+ end
+ if success then
+ return true
+ else
+ report ("info", 0, "db", "Could not compile font index.")
+ return false
+ end
+ end
+ report ("info", 0, "db", "Index file not writable")
+ if not fileiswritable (luaname) then
+ report ("info", 0, "db", "Failed to write %s.", luaname)
+ end
+ if not fileiswritable (lucname) then
+ report ("info", 0, "db", "Failed to write %s.", lucname)
+ end
+ return false
+end
+
+--[[doc--
+
+ Below set of functions is modeled after mtx-cache.
+
+--doc]]--
+
+--- string -> string -> string list -> string list -> string list -> unit
+local print_cache = function (category, path, luanames, lucnames, rest)
+ local report_indeed = function (...)
+ report("info", 0, "cache", ...)
+ end
+ report_indeed("Luaotfload cache: %s", category)
+ report_indeed("location: %s", path)
+ report_indeed("[raw] %4i", #luanames)
+ report_indeed("[compiled] %4i", #lucnames)
+ report_indeed("[other] %4i", #rest)
+ report_indeed("[total] %4i", #luanames + #lucnames + #rest)
+end
+
+--- string -> string -> string list -> bool -> bool
+local purge_from_cache = function (category, path, list, all)
+ report("info", 1, "cache", "Luaotfload cache: %s %s",
+ (all and "erase" or "purge"), category)
+ report("info", 1, "cache", "location: %s",path)
+ local n = 0
+ for i=1,#list do
+ local filename = list[i]
+ if stringfind(filename,"luatex%-cache") then -- safeguard
+ if all then
+ report("info", 5, "cache", "Removing %s.", filename)
+ osremove(filename)
+ n = n + 1
+ else
+ local suffix = filesuffix(filename)
+ if suffix == "lua" then
+ local checkname = file.replacesuffix(
+ filename, "lua", "luc")
+ if lfsisfile(checkname) then
+ report("info", 5, "cache", "Removing %s.", filename)
+ osremove(filename)
+ n = n + 1
+ end
+ end
+ end
+ end
+ end
+ report("info", 1, "cache", "Removed lua files : %i", n)
+ return true
+end
+
+--- string -> string list -> int -> string list -> string list -> string list ->
+--- (string list * string list * string list * string list)
+local collect_cache collect_cache = function (path, all, n, luanames,
+ lucnames, rest)
+ if not all then
+ local all = find_files (path)
+
+ local luanames, lucnames, rest = { }, { }, { }
+ return collect_cache(nil, all, 1, luanames, lucnames, rest)
+ end
+
+ local filename = all[n]
+ if filename then
+ local suffix = filesuffix(filename)
+ if suffix == "lua" then
+ luanames[#luanames+1] = filename
+ elseif suffix == "luc" then
+ lucnames[#lucnames+1] = filename
+ else
+ rest[#rest+1] = filename
+ end
+ return collect_cache(nil, all, n+1, luanames, lucnames, rest)
+ end
+ return luanames, lucnames, rest, all
+end
+
+local getwritablecachepath = function ( )
+ --- fonts.handlers.otf doesn’t exist outside a Luatex run,
+ --- so we have to improvise
+ local writable = getwritablepath (luaotfloadconfig.cache_dir)
+ if writable then
+ return writable
+ end
+end
+
+local getreadablecachepaths = function ( )
+ local readables = caches.getreadablepaths
+ (luaotfloadconfig.cache_dir)
+ local result = { }
+ if readables then
+ for i=1, #readables do
+ local readable = readables[i]
+ if lfsisdir (readable) then
+ result[#result+1] = readable
+ end
+ end
+ end
+ return result
+end
+
+--- unit -> unit
+local purge_cache = function ( )
+ local writable_path = getwritablecachepath ()
+ local luanames, lucnames, rest = collect_cache(writable_path)
+ if log.get_loglevel() > 1 then
+ print_cache("writable path", writable_path, luanames, lucnames, rest)
+ end
+ local success = purge_from_cache("writable path", writable_path, luanames, false)
+ return success
+end
+
+--- unit -> unit
+local erase_cache = function ( )
+ local writable_path = getwritablecachepath ()
+ local luanames, lucnames, rest, all = collect_cache(writable_path)
+ if log.get_loglevel() > 1 then
+ print_cache("writable path", writable_path, luanames, lucnames, rest)
+ end
+ local success = purge_from_cache("writable path", writable_path, all, true)
+ return success
+end
+
+local separator = function ( )
+ report("info", 0, string.rep("-", 67))
+end
+
+--- unit -> unit
+local show_cache = function ( )
+ local readable_paths = getreadablecachepaths ()
+ local writable_path = getwritablecachepath ()
+ local luanames, lucnames, rest = collect_cache(writable_path)
+
+ separator ()
+ print_cache ("writable path", writable_path,
+ luanames, lucnames, rest)
+ texio.write_nl""
+ for i=1,#readable_paths do
+ local readable_path = readable_paths[i]
+ if readable_path ~= writable_path then
+ local luanames, lucnames = collect_cache (readable_path)
+ print_cache ("readable path",
+ readable_path, luanames, lucnames, rest)
+ end
+ end
+ separator()
+ return true
+end
+
+-----------------------------------------------------------------------
+--- export functionality to the namespace “fonts.names”
+-----------------------------------------------------------------------
+
+names.scan_dir = scan_dir
+names.set_font_filter = set_font_filter
+names.flush_lookup_cache = flush_lookup_cache
+names.save_lookups = save_lookups
+names.load = load_names
+names.data = function () return name_index end
+names.save = save_names
+names.update = update_names
+names.crude_file_lookup = crude_file_lookup
+names.crude_file_lookup_verbose = crude_file_lookup_verbose
+names.read_blacklist = read_blacklist
+names.sanitize_fontname = sanitize_fontname
+names.getfilename = resolve_fullpath
+names.set_location_precedence = set_location_precedence
+
+--- font cache
+names.purge_cache = purge_cache
+names.erase_cache = erase_cache
+names.show_cache = show_cache
+
+--- replace the resolver from luatex-fonts
+if luaotfloadconfig.resolver == "cached" then
+ report("both", 2, "cache", "Caching of name: lookups active.")
+ names.resolvespec = resolve_cached
+ names.resolve_name = resolve_cached
+else
+ names.resolvespec = resolve_name
+ names.resolve_name = resolve_name
+end
+
+names.find_closest = find_closest
+
+-- for testing purpose
+names.read_fonts_conf = read_fonts_conf
+
+-- vim:tw=71:sw=4:ts=4:expandtab