summaryrefslogtreecommitdiff
path: root/luaotfload-tool.lua
diff options
context:
space:
mode:
Diffstat (limited to 'luaotfload-tool.lua')
-rwxr-xr-xluaotfload-tool.lua496
1 files changed, 445 insertions, 51 deletions
diff --git a/luaotfload-tool.lua b/luaotfload-tool.lua
index f1302a7..a353b37 100755
--- a/luaotfload-tool.lua
+++ b/luaotfload-tool.lua
@@ -46,11 +46,12 @@ end
local stringexplode = string.explode
local stringformat = string.format
local stringlower = string.lower
+local stringrep = string.rep
local tableconcat = table.concat
local texiowrite_nl = texio.write_nl
+local texiowrite = texio.write
-
-local C, Ct, P = lpeg.C, lpeg.Ct, lpeg.P
+local C, Ct, P, S = lpeg.C, lpeg.Ct, lpeg.P, lpeg.S
local lpegmatch = lpeg.match
local loader_file = "luatexbase.loader.lua"
@@ -79,9 +80,13 @@ After support for querying the database was added, the latter appeared
to be the more appropriate.
--doc]]--
-config = config or { }
-local config = config
-config.luaotfload = config.luaotfload or { }
+config = config or { }
+local config = config
+config.luaotfload = config.luaotfload or { }
+config.luaotfload.names_dir = config.luaotfload.names_dir or "names"
+config.luaotfload.cache_dir = config.luaotfload.cache_dir or "fonts"
+config.luaotfload.names_file = config.luaotfload.names_file
+ or "luaotfload-names.lua"
do -- we don’t have file.basename and the likes yet, so inline parser ftw
local slash = P"/"
@@ -108,19 +113,6 @@ config.lualibs.load_extended = false
require "lualibs"
---- prepare directories: the cache function in Luatex-Fonts
---- checks for writable directory only on startup, so everything
---- has to be laid out before we load basics-gen
-
-local cachepath = kpse.expand_var "$TEXMFVAR"
-if not lfs.isdir(cachepath) then
- dir.mkdirs(cachepath)
- if not lfs.isdir(cachepath) then
- texiowrite_nl(stringformat(
- "ERROR could not create directory %s", cachepath))
- end
-end
-
--[[doc--
\fileent{luatex-basics-gen.lua} calls functions from the
\luafunction{texio.*} library; too much for our taste.
@@ -141,8 +133,13 @@ require"alt_getopt"
local version = "2.3" -- same version number as luaotfload
local names = fonts.names
-local db_src_out = names.path.dir.."/"..names.path.basename
-local db_bin_out = file.replacesuffix(db_src_out, "luc")
+local sanitize_string = names.sanitize_string
+
+--local db_src_out = names.path.dir.."/"..names.path.basename
+local names_plain = file.join
+ (caches.getwritablepath (config.luaotfload.names_dir),
+ config.luaotfload.names_file)
+local names_bin = file.replacesuffix (names_plain, "luc")
local help_messages = {
["luaotfload-tool"] = [[
@@ -181,7 +178,11 @@ This tool is part of the luaotfload package. Valid options are:
-F --fuzzy look for approximate matches if --find fails
--limit=n limit display of fuzzy matches to <n>
(default: n = 1)
- -i --info display font metadata
+
+ -i --info display basic font metadata
+ -I --inspect display detailed font metadata
+ -w --warnings display warnings generated by the
+ fontloader library
--list=<criterion> output list of entries by field <criterion>
--list=<criterion>:<value> restrict to entries with <criterion>=<value>
@@ -198,6 +199,9 @@ The font database will be saved to
--cache=<directive> operate on font cache, where <directive> is
“show”, “purge”, or “erase”
+The font cache will be written to
+ %s
+
]],
mkluatexfontdb = [[
@@ -226,7 +230,12 @@ The font database will be saved to
local help_msg = function ( )
local template = help_messages[config.luaotfload.self]
or help_messages["luaotfload-tool"]
- texiowrite_nl(stringformat(template, config.luaotfload.self, db_src_out, db_bin_out))
+ texiowrite_nl(stringformat(template,
+ config.luaotfload.self,
+ names_plain,
+ names_bin,
+ caches.getwritablepath
+ (config.luaotfload.cache_dir)))
end
local version_msg = function ( )
@@ -235,34 +244,386 @@ local version_msg = function ( )
config.luaotfload.self, version, names.version))
end
+
+--- makeshift formatting
+
+local head_adornchars = {
+ [1] = "*", [2] = "=", [3] = "~", [4] = "-", [5] = "·",
+}
+
+local textwidth = 80
+local wd_leftcolumn = math.floor(textwidth * .25)
+local key_fmt = stringformat([[%%%ds]], wd_leftcolumn)
+local val_fmt = [[%s]]
+local fieldseparator = ":"
+local info_fmt = key_fmt .. fieldseparator .. " " .. val_fmt
+
+local currentdepth = 0
+local counterstack = { } -- counters per level
+local counterformat = "%d"
+
+local format_counter = function (stack)
+ local acc = { }
+ for lvl=1, #stack do
+ acc[#acc+1] = stringformat(counterformat, stack[lvl])
+ end
+ return tableconcat(acc, ".")
+end
+
+local print_heading = function (title, level)
+ local structuredata
+ if currentdepth == level then -- top is current
+ counterstack[#counterstack] = counterstack[#counterstack] + 1
+ elseif currentdepth < level then -- push new
+ counterstack[#counterstack+1] = 1
+ else -- pop
+ local diff = currentdepth - level
+ while diff > 0 do
+ counterstack[#counterstack] = nil
+ diff = diff - 1
+ end
+ counterstack[#counterstack] = counterstack[#counterstack] + 1
+ end
+ currentdepth = level
+
+ texiowrite_nl ""
+ if not level or level > #head_adornchars then
+ level = #head_adornchars
+ end
+ local adornchar = head_adornchars[level]
+
+ local counter = format_counter(counterstack)
+
+ local s = adornchar .. adornchar .. " "
+ .. counter .. " "
+ .. title .. " "
+ texiowrite_nl (s .. stringrep(adornchar, textwidth-utf.len(s)))
+end
+
local show_info_items = function (fontinfo)
- local items = table.sortedkeys(fontinfo)
+ local items = table.sortedkeys(fontinfo)
+ print_heading(fontinfo.fullname, 1)
+ texiowrite_nl ""
for n = 1, #items do
local item = items[n]
texiowrite_nl(stringformat(
- [[ %11s: %s]], item, fontinfo[item]))
+ info_fmt, item, fontinfo[item]))
+ end
+ texiowrite_nl ""
+end
+
+local p_eol = S"\n\r"^1
+local p_space = S" \t\v"^0
+local p_line = p_space * C((1 - p_eol)^1)^-1
+local p_lines = Ct(p_line * (p_eol^1 * p_line^-1)^0)
+
+local show_fontloader_warnings = function (ws)
+ local nws = #ws
+ print_heading(stringformat(
+ [[the fontloader emitted %d warnings]],
+ nws), 2)
+ texiowrite_nl ""
+ for i=1, nws do
+ local w = ws[i]
+ texiowrite_nl (stringformat("%d:", i))
+ local lines = lpegmatch(p_lines, w)
+ for i=1, #lines do
+ local line = lines[i]
+ texiowrite_nl(" · " .. line)
+ end
+ texiowrite_nl ""
+ end
+end
+
+local p_spacechar = S" \n\r\t\v"
+local p_wordchar = (1 - p_spacechar)
+local p_whitespace = p_spacechar^1
+local p_word = C(p_wordchar^1)
+local p_words = Ct(p_word * (p_whitespace * p_word)^0)
+
+--- string -> int -> string list
+local reflow = function (text, width)
+ local words
+ if type(text) == "string" then
+ words = lpegmatch(p_words, text)
+ if #words < 2 then
+ return { text }
+ end
+ else
+ words = text
+ if #words < 2 then
+ return words
+ end
end
+
+ local space = " "
+ local utflen = utf.len
+ local reflowed = { }
+
+ local first = words[1]
+ local linelen = #first
+ local line = { first }
+
+ for i=2, #words do
+ local word = words[i]
+ local lword = utflen(word)
+ linelen = linelen + lword + 1
+ if linelen > width then
+ reflowed[#reflowed+1] = tableconcat(line)
+ linelen = #word
+ line = { word }
+ else
+ line[#line+1] = space
+ line[#line+1] = word
+ end
+ end
+ reflowed[#reflowed+1] = tableconcat(line)
+ return reflowed
end
-local show_font_info = function (filename)
- local fullname = resolvers.findfile(filename)
+--- string -> 'a -> string list
+local print_field = function (key, val)
+ val = tostring(val)
+ local lhs = stringformat(key_fmt, key) .. fieldseparator .. " "
+ local wd_lhs = #lhs
+ local lines = reflow(val, textwidth - wd_lhs)
+
+ texiowrite_nl(lhs)
+ texiowrite(lines[1])
+ if #lines > 1 then
+ local indent = stringrep(" ", wd_lhs)
+ for i=2, #lines do
+ texiowrite_nl(indent)
+ texiowrite (lines[i])
+ end
+ end
+end
+
+local display_names = function (names)
+ print_heading("Font Metadata", 2)
+ for i=1, #names do
+ local lang, namedata = names[i].lang, names[i].names
+ print_heading(stringformat("Language: %s ", i, lang), 3)
+ texiowrite_nl ""
+ if namedata then
+ for field, value in next, namedata do
+ print_field(field, value)
+ end
+ end
+ end
+end
+
+--- see luafflib.c
+local general_fields = {
+ --- second: l -> literal | n -> length | d -> date
+ { "fullname", "l", "font name" },
+ { "version", "l", "font version" },
+ { "creationtime", "d", "creation time" },
+ { "modificationtime", "d", "modification time" },
+ { "subfonts", "n", "number of subfonts" },
+ { "glyphcnt", "l", "number of glyphs" },
+ { "weight", "l", "weight indicator" },
+ { "design_size", "l", "design size" },
+ { "design_range_bottom", "l", "design size min" },
+ { "design_range_top", "l", "design size max" },
+ { "fontstyle_id", "l", "font style id" },
+ { "fontstyle_name", "l", "font style name" },
+ { "strokewidth", "l", "stroke width" },
+ { "units_per_em", "l", "units per em" },
+ { "ascent", "l", "ascender height" },
+ { "descent", "l", "descender height" },
+ { "comments", "l", "comments" },
+ { "os2_version", "l", "os2 version" },
+ { "sfd_version", "l", "sfd version" },
+}
+
+local display_general = function (fullinfo)
+ texiowrite_nl ""
+ print_heading("General Information", 2)
+ texiowrite_nl ""
+ for i=1, #general_fields do
+ local field = general_fields[i]
+ local key, mode, desc = unpack(field)
+ local val
+ if mode == "l" then
+ val = fullinfo[key]
+ elseif mode == "n" then
+ local v = fullinfo[key]
+ if v then
+ val = #fullinfo[key]
+ end
+ elseif mode == "d" then
+ val = os.date("%F %T", fullinfo[key])
+ end
+ if not val then
+ val = "<none>"
+ end
+ print_field(desc, val)
+ end
+end
+
+local print_features = function (features)
+ for tag, data in next, features do
+ print_heading(tag, 4)
+ for script, languages in next, data do
+ local field = stringformat(key_fmt, script).. fieldseparator .. " "
+ local wd_field = #field
+ --inspect(languages.list)
+ local lines = reflow(languages.list, textwidth - wd_field)
+ local indent = stringrep(" ", wd_field)
+ texiowrite_nl(field)
+ texiowrite(lines[1])
+ if #lines > 1 then
+ for i=1, #lines do
+ texiowrite_nl(indent .. lines[i])
+ end
+ end
+ end
+ end
+end
+
+local extract_feature_info = function (set)
+ local collected = { }
+ for i=1, #set do
+ local features = set[i].features
+ if features then
+ for j=1, #features do
+ local feature = features[j]
+ local scripts = feature.scripts
+ local tagname = stringlower(feature.tag)
+ local entry = collected[tagname] or { }
+
+ for k=1, #scripts do
+ local script = scripts[k]
+ local scriptname = stringlower(script.script)
+ local c_script = entry[scriptname] or {
+ list = { },
+ set = { },
+ }
+ local list, set = c_script.list, c_script.set
+
+ for l=1, #script.langs do
+ local langname = stringlower(script.langs[l])
+ if not set[langname] then
+ list[#list+1] = langname
+ set[langname] = true
+ end
+ end
+ entry[scriptname] = c_script
+ end
+ collected[tagname] = entry
+ end
+ end
+ end
+ return collected
+end
+
+local display_feature_set = function (set)
+ local collected = extract_feature_info(set)
+ print_features(collected)
+end
+
+local display_features = function (gsub, gpos)
+ texiowrite_nl ""
+ print_heading("Features", 2)
+ print_heading("GSUB Features", 3)
+ display_feature_set(gsub)
+ print_heading("GPOS Features", 3)
+ display_feature_set(gpos)
+end
+
+local show_full_info = function (path, subfont, warnings)
+ local rawinfo, warn = fontloader.open(path, subfont)
+ if warnings then
+ show_fontloader_warnings(warn)
+ end
+ if not rawinfo then
+ texiowrite_nl(stringformat([[cannot open font %s]], path))
+ return
+ end
+ local fontdata = { }
+ local fullinfo = fontloader.to_table(rawinfo)
+ local fields = fontloader.fields(rawinfo)
+ fontloader.close(rawinfo)
+ display_names(fullinfo.names)
+ display_general(fullinfo)
+ display_features(fullinfo.gsub, fullinfo.gpos)
+end
+
+--- Subfonts returned by fontloader.info() do not correspond
+--- to the actual indices required by fontloader.open(), so
+--- we try and locate the correct one by matching the request
+--- against the full name.
+
+local subfont_by_name
+subfont_by_name = function (lst, askedname, n)
+ if not n then
+ return subfont_by_name (lst, askedname, 1)
+ end
+
+ local font = lst[n]
+ if font then
+ if sanitize_string(font.fullname) == askedname then
+ return font
+ end
+ return subfont_by_name (lst, askedname, n+1)
+ end
+ return false
+end
+
+--[[doc--
+The font info knows two levels of detail:
+
+ a) basic information returned by fontloader.info(); and
+ b) detailed information that is a subset of the font table
+ returned by fontloader.open().
+--doc]]--
+
+local show_font_info = function (basename, askedname, detail, warnings)
+ local filenames = names.data.filenames
+ local index = filenames.base[basename]
+ local fullname = filenames.full[index]
+ askedname = sanitize_string(askedname)
+ if not fullname then -- texmf
+ fullname = resolvers.findfile(basename)
+ end
if fullname then
- local fontinfo = fontloader.info(fullname)
- local nfonts = #fontinfo
+ local shortinfo = fontloader.info(fullname)
+ local nfonts = #shortinfo
if nfonts > 0 then -- true type collection
- logs.names_report(true, 1, "resolve",
- [[%s is a font collection]], filename)
- for n = 1, nfonts do
+ local subfont
+ if askedname then
+ logs.names_report(true, 1, "resolve",
+ [[%s is part of the font collection %s]],
+ askedname, basename)
+ subfont = subfont_by_name(shortinfo, askedname)
+ end
+ if subfont then
+ show_info_items(subfont)
+ if detail == true then
+ show_full_info(fullname, subfont, warnings)
+ end
+ else -- list all subfonts
logs.names_report(true, 1, "resolve",
- [[showing info for font no. %d]], n)
- show_info_items(fontinfo[n])
+ [[%s is a font collection]], basename)
+ for subfont = 1, nfonts do
+ logs.names_report(true, 1, "resolve",
+ [[Showing info for font no. %d]], n)
+ show_info_items(shortinfo[subfont])
+ if detail == true then
+ show_full_info(fullname, subfont, warnings)
+ end
+ end
end
else
- show_info_items(fontinfo)
+ show_info_items(shortinfo)
+ if detail == true then
+ show_full_info(fullname, subfont, warnings)
+ end
end
else
logs.names_report(true, 1, "resolve",
- "font %s not found", filename)
+ "Font %s not found", filename)
end
end
@@ -286,8 +647,8 @@ local actions = { } --- (jobspec -> (bool * bool)) list
actions.loglevel = function (job)
logs.set_loglevel(job.log_level)
logs.names_report("info", 3, "util",
- "setting log level", "%d", job.log_level)
- logs.names_report("log", 0, "util", "lua=%s", _VERSION)
+ "Setting log level", "%d", job.log_level)
+ logs.names_report("log", 0, "util", "Lua=%s", _VERSION)
return true, true
end
@@ -374,11 +735,11 @@ actions.query = function (job)
"Resolved file name “%s”, subfont nr. “%s”",
foundname, subfont)
else
- logs.names_report(false, 0,
- "resolve", "Resolved file name “%s”", foundname)
+ logs.names_report(false, 0, "resolve",
+ "Resolved file name “%s”", foundname)
end
if job.show_info then
- show_font_info(foundname)
+ show_font_info(foundname, query, job.full_info, job.warnings)
end
else
logs.names_report(false, 0,
@@ -469,7 +830,7 @@ actions.list = function (job)
local nmappings = #mappings
if criterion == "*" then
- logs.names_report(false, 1, "list", "all %d entries", nmappings)
+ logs.names_report(false, 1, "list", "All %d entries", nmappings)
for i=1, nmappings do
local entry = mappings[i]
local fields = get_fields(entry, asked_fields)
@@ -484,12 +845,12 @@ actions.list = function (job)
criterion = criterion[1]
asked_fields = set_primary_field(asked_fields, criterion)
- logs.names_report(false, 1, "list", "by %s", criterion)
+ logs.names_report(false, 1, "list", "By %s", criterion)
--- firstly, build a list of fonts to operate on
local targets = { }
if asked_value then --- only those whose value matches
- logs.names_report(false, 2, "list", "restricting to value %s", asked_value)
+ logs.names_report(false, 2, "list", "Restricting to value %s", asked_value)
for i=1, nmappings do
local entry = mappings[i]
if entry[criterion]
@@ -540,6 +901,22 @@ actions.list = function (job)
return true, true
end
+--- stuff to be carried out prior to exit
+
+local finalizers = { }
+
+--- returns false if at least one of the actions failed, mainly
+--- for closing io channels
+local finalize = function ()
+ local success = true
+ for _, fun in next, finalizers do
+ if type (fun) == "function" then
+ if fun () == false then success = false end
+ end
+ end
+ return success
+end
+
--[[--
Command-line processing.
mkluatexfontdb.lua relies on the script alt_getopt to process argv and
@@ -553,6 +930,8 @@ alt_getopt.
local process_cmdline = function ( ) -- unit -> jobspec
local result = { -- jobspec
force_reload = nil,
+ full_info = false,
+ warnings = false,
criterion = "",
query = "",
log_level = 0, --- 2 is approx. the old behavior
@@ -569,6 +948,7 @@ local process_cmdline = function ( ) -- unit -> jobspec
fuzzy = "F",
help = "h",
info = "i",
+ inspect = "I",
limit = 1,
list = 1,
log = 1,
@@ -578,9 +958,10 @@ local process_cmdline = function ( ) -- unit -> jobspec
update = "u",
verbose = 1 ,
version = "V",
+ warnings = "w",
}
- local short_options = "bDfFilpquvVh"
+ local short_options = "bDfFiIlpquvVhw"
local options, _, optarg =
alt_getopt.get_ordered_opts (arg, short_options, long_options)
@@ -608,12 +989,18 @@ local process_cmdline = function ( ) -- unit -> jobspec
elseif v == "verbose" then
local lvl = optarg[n]
if lvl then
- result.log_level = tonumber(lvl)
+ lvl = tonumber(lvl)
+ result.log_level = lvl
+ if lvl > 2 then
+ result.warnings = true
+ end
end
+ elseif v == "w" then
+ result.warnings = true
elseif v == "log" then
local str = optarg[n]
if str then
- logs.set_logout(str)
+ finalizers = logs.set_logout(str, finalizers)
end
elseif v == "find" then
action_pending["query"] = true
@@ -627,6 +1014,9 @@ local process_cmdline = function ( ) -- unit -> jobspec
end
elseif v == "i" then
result.show_info = true
+ elseif v == "I" then
+ result.show_info = true
+ result.full_info = true
elseif v == "alias" then
config.luaotfload.self = optarg[n]
elseif v == "l" then
@@ -667,7 +1057,7 @@ local main = function ( ) -- unit -> int
local actionname = action_sequence[i]
local exit = false
if action_pending[actionname] then
- logs.names_report("log", 3, "util", "preparing for task",
+ logs.names_report("log", 3, "util", "Preparing for task",
"%s", actionname)
local action = actions[actionname]
@@ -675,22 +1065,26 @@ local main = function ( ) -- unit -> int
if not success then
logs.names_report(false, 0, "util",
- "could not finish task", "%s", actionname)
+ "Could not finish task", "%s", actionname)
retval = -1
exit = true
elseif not continue then
logs.names_report(false, 3, "util",
- "task completed, exiting", "%s", actionname)
+ "Task completed, exiting", "%s", actionname)
exit = true
else
logs.names_report(false, 3, "util",
- "task completed successfully", "%s", actionname)
+ "Task completed successfully", "%s", actionname)
end
end
if exit then break end
end
- texiowrite_nl""
+ if finalize () == false then
+ retval = -1
+ end
+
+ --texiowrite_nl""
return retval
end