if not modules then modules = { } end modules ['font-nms'] = {
version = 2.2,
comment = "companion to luaotfload.lua",
author = "Khaled Hosny and Elie Roux",
copyright = "Luaotfload Development Team",
license = "GNU GPL v2"
}
--- Luatex builtins
local load = load
local next = next
local pcall = pcall
local require = require
local tonumber = tonumber
local iolines = io.lines
local ioopen = io.open
local kpseexpand_path = kpse.expand_path
local mathabs = math.abs
local mathmin = math.min
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 tablecopy = table.copy
local tablesort = table.sort
local tabletofile = table.tofile
local texiowrite_nl = texio.write_nl
local utf8gsub = unicode.utf8.gsub
local utf8lower = unicode.utf8.lower
--- these come from Lualibs/Context
local dirglob = dir.glob
local dirmkdirs = dir.mkdirds
local filebasename = file.basename
local filecollapsepath = file.collapsepath
local fileextname = file.extname
local filejoin = file.join
local filereplacesuffix = file.replacesuffix
local filesplitpath = file.splitpath
local stringis_empty = string.is_empty
local stringsplit = string.split
local stringstrip = string.strip
local tableappend = table.append
local tabletohash = table.tohash
--- the font loader namespace is “fonts”, same as in Context
fonts = fonts or { }
fonts.names = fonts.names or { }
local names = fonts.names
local names_dir = "luatex-cache/generic/names"
names.version = 2.2
names.data = nil
names.path = {
basename = "otfl-names.lua",
dir = filejoin(kpse.expand_var("$TEXMFVAR"), names_dir),
}
----
---
--- these lines load some binary module called “lualatex-platform”
--- that doesn’t appear to build with Lua 5.2. I’m going ahead and
--- disable it for the time being until someone clarifies what it
--- is supposed to do and whether we should care to fix it.
---
--local success = pcall(require, "luatexbase.modutils")
--if success then
-- success = pcall(luatexbase.require_module,
-- "lualatex-platform", "2011/03/30")
-- print(success)
--end
--local get_installed_fonts
--if success then
-- get_installed_fonts = lualatex.platform.get_installed_fonts
--else
-- function get_installed_fonts()
-- end
--end
----
local get_installed_fonts = nil
--[[doc--
Auxiliary functions
--doc]]--
local report = logs.names_report
local sanitize_string = function (str)
if str ~= nil then
return utf8gsub(utf8lower(str), "[^%a%d]", "")
end
return nil
end
local fontnames_init = function ( )
return {
mappings = { },
status = { },
version = names.version,
}
end
local make_name = function (path)
return filereplacesuffix(path, "lua"), filereplacesuffix(path, "luc")
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.
local code_cache = { }
--- string -> (string * table)
local load_lua_file = function (path)
local code = code_cache[path]
if code then return path, code() end
local foundname = filereplacesuffix(path, "luc")
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 return nil, nil end
code_cache[path] = code --- insert into memo
return foundname, code()
end
--- define locals in scope
local find_closest
local font_fullinfo
local load_names
local read_fonts_conf
local reload_db
local resolve
local save_names
local scan_external_dir
local update_names
load_names = function ( )
local path = filejoin(names.path.dir, names.path.basename)
local foundname, data = load_lua_file(path)
if data then
report("info", 0, "Font names database loaded", "%s", foundname)
else
report("info", 0,
[[Font names database not found, generating new one.
This can take several minutes; please be patient.]])
data = update_names(fontnames_init())
save_names(data)
end
texiowrite_nl""
return data
end
local fuzzy_limit = 1 --- display closest only
local style_synonyms = { set = { } }
do
style_synonyms.list = {
regular = { "normal", "roman",
"plain", "book",
"medium", },
--- TODO note from Élie Roux
--- boldregular was for old versions of Linux Libertine, is it still useful?
--- semibold is in new versions of Linux Libertine, but there is also a bold,
--- not sure it's useful here...
bold = { "demi", "demibold",
"semibold", "boldregular",},
italic = { "regularitalic", "normalitalic",
"oblique", "slanted", },
bolditalic = { "boldoblique", "boldslanted",
"demiitalic", "demioblique",
"demislanted", "demibolditalic",
"semibolditalic", },
}
for category, synonyms in next, style_synonyms.list do
style_synonyms.set[category] = tabletohash(synonyms, true)
end
end
--- state of the database
local fonts_loaded = false
local fonts_reloaded = false
--[[doc--
Luatex-fonts, the font-loader package luaotfload imports, comes with
basic file location facilities (see luatex-fonts-syn.lua).
However, the builtin functionality is too limited to be of more than
basic use, which is why we supply our own resolver that accesses the
font database created by the mkluatexfontdb script.
--doc]]--
---
--- the request specification has the fields:
---
--- · features: table
--- · normal: set of { ccmp clig itlc kern liga locl mark mkmk rlig }
--- · ???
--- · forced: string
--- · lookup: "name" | "file"
--- · method: string
--- · name: string
--- · resolved: string
--- · size: int
--- · specification: string (== ":" )
--- · sub: string
---
--- the return value of “resolve” is the file name of the requested
--- font
--- 'a -> 'a -> table -> (string * string | bool * bool)
---
--- note by phg: I added a third return value that indicates a
--- successful lookup as this cannot be inferred from the other
--- values.
---
resolve = function (_,_,specification) -- the 1st two parameters are used by ConTeXt
local name = sanitize_string(specification.name)
local style = sanitize_string(specification.style) or "regular"
local size
if specification.optsize then
size = tonumber(specification.optsize)
elseif specification.size then
size = specification.size / 65536
end
if not fonts_loaded then
names.data = load_names()
fonts_loaded = true
end
local data = names.data
if type(data) == "table" then
local db_version, nms_version = data.version, names.version
if data.version ~= names.version then
report("log", 0, "db",
[[version mismatch; expected %4.3f, got %4.3f]],
nms_version, db_version
)
return reload_db(resolve, nil, nil, specification)
end
if data.mappings then
local found = { }
local synonym_set = style_synonyms.set
for _,face in next, data.mappings do
--- TODO we really should store those in dedicated
--- .sanitized field
local family = sanitize_string(face.names and face.names.family)
local subfamily = sanitize_string(face.names and face.names.subfamily)
local fullname = sanitize_string(face.names and face.names.fullname)
local psname = sanitize_string(face.names and face.names.psname)
local fontname = sanitize_string(face.fontname)
local pfullname = sanitize_string(face.fullname)
local optsize, dsnsize, maxsize, minsize
if #face.size > 0 then
optsize = face.size
dsnsize = optsize[1] and optsize[1] / 10
-- can be nil
maxsize = optsize[2] and optsize[2] / 10 or dsnsize
minsize = optsize[3] and optsize[3] / 10 or dsnsize
end
if name == family then
if subfamily == style then
if optsize then
if dsnsize == size
or (size > minsize and size <= maxsize) then
found[1] = face
break
else
found[#found+1] = face
end
else
found[1] = face
break
end
elseif synonym_set[style] and
synonym_set[style][subfamily] then
if optsize then
if dsnsize == size
or (size > minsize and size <= maxsize) then
found[1] = face
break
else
found[#found+1] = face
end
else
found[1] = face
break
end
elseif subfamily == "regular" or
synonym_set.regular[subfamily] then
found.fallback = face
end
else
if name == fullname
or name == pfullname
or name == fontname
or name == psname then
if optsize then
if dsnsize == size
or (size > minsize and size <= maxsize) then
found[1] = face
break
else
found[#found+1] = face
end
else
found[1] = face
break
end
end
end
end
if #found == 1 then
if kpse.lookup(found[1].filename[1]) then
report("log", 0, "load font",
"font family='%s', subfamily='%s' found: %s",
name, style, found[1].filename[1]
)
return found[1].filename[1], found[1].filename[2], true
end
elseif #found > 1 then
-- we found matching font(s) but not in the requested optical
-- sizes, so we loop through the matches to find the one with
-- least difference from the requested size.
local closest
local least = math.huge -- initial value is infinity
for i,face in next, found do
local dsnsize = face.size[1]/10
local difference = mathabs(dsnsize-size)
if difference < least then
closest = face
least = difference
end
end
if kpse.lookup(closest.filename[1]) then
report("log", 0, "load font",
"font family='%s', subfamily='%s' found: %s",
name, style, closest.filename[1]
)
return closest.filename[1], closest.filename[2], true
end
elseif found.fallback then
return found.fallback.filename[1], found.fallback.filename[2], true
end
--- no font found so far
if not fonts_reloaded then
--- last straw: try reloading the database
return reload_db(resolve, nil, nil, specification)
else
--- else, fallback to requested name
--- XXX: specification.name is empty with absolute paths, looks
--- like a bug in the specification parser
return specification.name, false, false
end
end
else --- no db or outdated; reload names and retry
if not fonts_reloaded then
return reload_db(resolve, nil, nil, specification)
else --- unsucessfully reloaded; bail
return specification.name, false, false
end
end
end --- resolve()
--- when reload is triggered we update the database
--- and then re-run the caller with the arg list
--- ('a -> 'a) -> 'a list -> 'a
reload_db = function (caller, ...)
report("log", 1, "db", "reload initiated")
names.data = update_names()
save_names(names.data)
fonts_reloaded = true
return caller(...)
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_string(name)
limit = limit or fuzzy_limit
if not fonts_loaded then
names.data = load_names()
fonts_loaded = true
end
local data = names.data
if type(data) == "table" then
local by_distance = { } --- (int, string list) dict
local distances = { } --- int list
local cached = { } --- (string, int) dict
local mappings = data.mappings
local n_fonts = #mappings
for n = 1, n_fonts do
local current = mappings[n]
local cnames = current.names
--[[
This is simplistic but surpisingly fast.
Matching is performed against the “family” name
of a db record. We then store its “fullname” at
it edit distance.
We should probably do some weighting over all the
font name categories as well as whatever agrep
does.
--]]
if cnames then
local fullname, family = cnames.fullname, cnames.family
family = sanitize_string(family)
local dist = cached[family]--- maybe already calculated
if not dist then
dist = iterative_levenshtein(name, family)
cached[family] = 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
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 “" .. name .. "”: " .. dist
.. "\n " .. tableconcat(namelst, "\n ")
)
end
return true
end
return false
else --- need reload
return reload_db(find_closest, name)
end
return false
end --- find_closest()
--[[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]]--
font_fullinfo = function (filename, subfont, texmf)
local tfmdata = { }
local rawfont = fontloader.open(filename, subfont)
if not rawfont then
report("log", 1, "error", "failed to open %s", filename)
return
end
local metadata = fontloader.to_table(rawfont)
fontloader.close(rawfont)
collectgarbage("collect")
-- see http://www.microsoft.com/typography/OTSPEC/features_pt.htm#size
if metadata.fontstyle_name then
for _, name in next, metadata.fontstyle_name do
if name.lang == 1033 then --- I hate magic numbers
tfmdata.fontstyle_name = name.name
end
end
end
if metadata.names then
for _, namedata in next, metadata.names do
if namedata.lang == "English (US)" then
tfmdata.names = {
--- see
--- https://developer.apple.com/fonts/TTRefMan/RM06/Chap6name.html
fullname = namedata.names.compatfull
or namedata.names.fullname,
family = namedata.names.preffamilyname
or namedata.names.family,
subfamily= tfmdata.fontstyle_name
or namedata.names.prefmodifiers
or namedata.names.subfamily,
psname = namedata.names.postscriptname
}
end
end
else
-- no names table, propably a broken font
report("log", 1, "broken font rejected", "%s", basefile)
return
end
tfmdata.fontname = metadata.fontname
tfmdata.fullname = metadata.fullname
tfmdata.familyname = metadata.familyname
tfmdata.filename = {
texmf and filebasename(filename) or filename,
subfont
}
tfmdata.weight = metadata.pfminfo.weight
tfmdata.width = metadata.pfminfo.width
tfmdata.slant = metadata.italicangle
-- don't waste the space with zero values
tfmdata.size = {
metadata.design_size ~= 0 and metadata.design_size or nil,
metadata.design_range_top ~= 0 and metadata.design_range_top or nil,
metadata.design_range_bottom ~= 0 and metadata.design_range_bottom or nil,
}
return tfmdata
end
local load_font = function (filename, fontnames, newfontnames, texmf)
local newmappings = newfontnames.mappings
local newstatus = newfontnames.status
local mappings = fontnames.mappings
local status = fontnames.status
local basename = filebasename(filename)
local basefile = texmf and basename or filename
if filename then
if names.blacklist[filename] or
names.blacklist[basename] then
report("log", 2, "ignoring font", "%s", filename)
return
end
local timestamp, db_timestamp
db_timestamp = status[basefile] and status[basefile].timestamp
timestamp = lfs.attributes(filename, "modification")
local index_status = newstatus[basefile] or (not texmf and newstatus[basename])
if index_status and index_status.timestamp == timestamp then
-- already indexed this run
return
end
newstatus[basefile] = newstatus[basefile] or { }
newstatus[basefile].timestamp = timestamp
newstatus[basefile].index = newstatus[basefile].index or { }
if db_timestamp == timestamp and not newstatus[basefile].index[1] then
for _,v in next, status[basefile].index do
local index = #newstatus[basefile].index
newmappings[#newmappings+1] = mappings[v]
newstatus[basefile].index[index+1] = #newmappings
end
report("log", 1, "font already indexed", "%s", basefile)
return
end
local info = fontloader.info(filename)
if info then
if type(info) == "table" and #info > 1 then
for i in next, info do
local fullinfo = font_fullinfo(filename, i-1, texmf)
if not fullinfo then
return
end
local index = newstatus[basefile].index[i]
if newstatus[basefile].index[i] then
index = newstatus[basefile].index[i]
else
index = #newmappings+1
end
newmappings[index] = fullinfo
newstatus[basefile].index[i] = index
end
else
local fullinfo = font_fullinfo(filename, false, texmf)
if not fullinfo then
return
end
local index
if newstatus[basefile].index[1] then
index = newstatus[basefile].index[1]
else
index = #newmappings+1
end
newmappings[index] = fullinfo
newstatus[basefile].index[1] = index
end
else
report("log", 1, "failed to load", "%s", basefile)
end
end
end
local function path_normalize(path)
--[[
path normalization:
- a\b\c -> a/b/c
- a/../b -> b
- /cygdrive/a/b -> a:/b
- reading symlinks under non-Win32
- using kpse.readable_file on Win32
]]
if os.type == "windows" or os.type == "msdos" or os.name == "cygwin" then
path = stringgsub(path, '\\', '/')
path = stringlower(path)
path = stringgsub(path, '^/cygdrive/(%a)/', '%1:/')
end
if os.type ~= "windows" and os.type ~= "msdos" then
local dest = lfs.readlink(path)
if dest then
if kpse.readable_file(dest) then
path = dest
elseif kpse.readable_file(filejoin(file.dirname(path), dest)) then
path = filejoin(file.dirname(path), dest)
else
-- broken symlink?
end
end
end
path = filecollapsepath(path)
return path
end
fonts.path_normalize = path_normalize
names.blacklist = { }
local function read_blacklist()
local files = {
kpse.lookup("otfl-blacklist.cnf", {all=true, format="tex"})
}
local blacklist = names.blacklist
local whitelist = { }
if files and type(files) == "table" then
for _,v in next, files do
for line in iolines(v) do
line = stringstrip(line) -- to get rid of lines like " % foo"
local first_chr = stringsub(line, 1, 1) --- faster than find
if first_chr == "%" or stringis_empty(line) then
-- comment or empty line
else
line = stringsplit(line, "%")[1]
line = stringstrip(line)
if stringsub(line, 1, 1) == "-" then
whitelist[stringsub(line, 2, -1)] = true
else
report("log", 2, "blacklisted file", "%s", line)
blacklist[line] = true
end
end
end
end
end
for _,fontname in next, whitelist do
blacklist[fontname] = nil
end
end
local font_extensions = { "otf", "ttf", "ttc", "dfont" }
local font_extensions_set = {}
for key, value in next, font_extensions do
font_extensions_set[value] = true
end
--local installed_fonts_scanned = false --- ugh
--- we already have scan_os_fonts don’t we?
--local function scan_installed_fonts(fontnames, newfontnames)
-- --- Try to query and add font list from operating system.
-- --- This uses the lualatex-platform module.
-- --- what for? why can’t we do this in Lua?
-- report("info", 0, "Scanning fonts known to operating system...")
-- local fonts = get_installed_fonts()
-- if fonts and #fonts > 0 then
-- installed_fonts_scanned = true
-- report("log", 2, "operating system fonts found", "%d", #fonts)
-- for key, value in next, fonts do
-- local file = value.path
-- if file then
-- local ext = fileextname(file)
-- if ext and font_extensions_set[ext] then
-- file = path_normalize(file)
-- report("log", 1, "loading font", "%s", file)
-- load_font(file, fontnames, newfontnames, false)
-- end
-- end
-- end
-- else
-- report("log", 2, "Could not retrieve list of installed fonts")
-- end
--end
local function scan_dir(dirname, fontnames, newfontnames, texmf)
--[[
This function scans a directory and populates the list of fonts
with all the fonts it finds.
- dirname is the name of the directory to scan
- names is the font database to fill
- texmf is a boolean saying if we are scanning a texmf directory
]]
local list, found = { }, { }
local nbfound = 0
report("log", 2, "scanning", "%s", dirname)
for _,i in next, font_extensions do
for _,ext in next, { i, stringupper(i) } do
found = dirglob(stringformat("%s/**.%s$", dirname, ext))
-- note that glob fails silently on broken symlinks, which happens
-- sometimes in TeX Live.
report("log", 2, "fonts found", "%s '%s' fonts found", #found, ext)
nbfound = nbfound + #found
tableappend(list, found)
end
end
report("log", 2, "fonts found", "%d fonts found in '%s'", nbfound, dirname)
for _,file in next, list do
file = path_normalize(file)
report("log", 1, "loading font", "%s", file)
load_font(file, fontnames, newfontnames, texmf)
end
end
local function scan_texmf_fonts(fontnames, newfontnames)
--[[
This function scans all fonts in the texmf tree, through kpathsea
variables OPENTYPEFONTS and TTFONTS of texmf.cnf
]]
if stringis_empty(kpseexpand_path("$OSFONTDIR")) then
report("info", 0, "Scanning TEXMF fonts...")
else
report("info", 0, "Scanning TEXMF and OS fonts...")
end
local fontdirs = stringgsub(kpseexpand_path("$OPENTYPEFONTS"), "^%.", "")
fontdirs = fontdirs .. stringgsub(kpseexpand_path("$TTFONTS"), "^%.", "")
if not stringis_empty(fontdirs) then
for _,d in next, filesplitpath(fontdirs) do
scan_dir(d, fontnames, newfontnames, true)
end
end
end
--[[
For the OS fonts, there are several options:
- if OSFONTDIR is set (which is the case under windows by default but
not on the other OSs), it scans it at the same time as the texmf tree,
in the scan_texmf_fonts.
- if not:
- under Windows and Mac OSX, we take a look at some hardcoded directories
- under Unix, we read /etc/fonts/fonts.conf and read the directories in it
This means that if you have fonts in fancy directories, you need to set them
in OSFONTDIR.
]]
--- (string -> tab -> tab -> tab)
read_fonts_conf = function (path, results, passed_paths)
--[[
This function parses /etc/fonts/fonts.conf and returns all the dir
it finds. The code is minimal, please report any error it may
generate.
]]
local fh = ioopen(path)
passed_paths[#passed_paths+1] = path
passed_paths_set = tabletohash(passed_paths, true)
if not fh then
report("log", 2, "cannot open file", "%s", path)
return results
end
local incomments = false
for line in fh:lines() do
while line and line ~= "" do
-- spaghetti code... hmmm...
if incomments then
local tmp = stringfind(line, '-->') --- wtf?
if tmp then
incomments = false
line = stringsub(line, tmp+3)
else
line = nil
end
else
local tmp = stringfind(line, '