summaryrefslogtreecommitdiff
path: root/tex/context/base/mkiv/font-syn.lua
diff options
context:
space:
mode:
Diffstat (limited to 'tex/context/base/mkiv/font-syn.lua')
-rw-r--r--tex/context/base/mkiv/font-syn.lua2102
1 files changed, 2102 insertions, 0 deletions
diff --git a/tex/context/base/mkiv/font-syn.lua b/tex/context/base/mkiv/font-syn.lua
new file mode 100644
index 000000000..5e2d3a1ef
--- /dev/null
+++ b/tex/context/base/mkiv/font-syn.lua
@@ -0,0 +1,2102 @@
+if not modules then modules = { } end modules ['font-syn'] = {
+ version = 1.001,
+ comment = "companion to font-ini.mkiv",
+ author = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+ copyright = "PRAGMA ADE / ConTeXt Development Team",
+ license = "see context related readme files"
+}
+
+-- todo: subs in lookups requests
+-- todo: see if the (experimental) lua reader (on my machine) be used (it's a bit slower so maybe wait till lua 5.3)
+
+-- identifying ttf/otf/ttc/afm : 2200 fonts:
+--
+-- old ff loader: 140 sec
+-- new lua loader: 5 sec
+
+local next, tonumber, type, tostring = next, tonumber, type, tostring
+local sub, gsub, match, find, lower, upper = string.sub, string.gsub, string.match, string.find, string.lower, string.upper
+local concat, sort = table.concat, table.sort
+local serialize, sortedhash = table.serialize, table.sortedhash
+local lpegmatch = lpeg.match
+local unpack = unpack or table.unpack
+local formatters, topattern = string.formatters, string.topattern
+local round = math.round
+local P, R, S, C, Cc, Ct, Cs = lpeg.P, lpeg.R, lpeg.S, lpeg.C, lpeg.Cc, lpeg.Ct, lpeg.Cs
+local lpegmatch, lpegpatterns = lpeg.match, lpeg.patterns
+
+local allocate = utilities.storage.allocate
+local sparse = utilities.storage.sparse
+local setmetatableindex = table.setmetatableindex
+
+local removesuffix = file.removesuffix
+local splitbase = file.splitbase
+local splitname = file.splitname
+local basename = file.basename
+local nameonly = file.nameonly
+local pathpart = file.pathpart
+local filejoin = file.join
+local is_qualified_path = file.is_qualified_path
+local exists = io.exists
+
+local findfile = resolvers.findfile
+local cleanpath = resolvers.cleanpath
+local resolveprefix = resolvers.resolve
+
+----- fontloader = fontloader -- still needed for pfb (now)
+----- get_font_info = fontloader.info
+
+local settings_to_hash = utilities.parsers.settings_to_hash_tolerant
+
+local trace_names = false trackers.register("fonts.names", function(v) trace_names = v end)
+local trace_warnings = false trackers.register("fonts.warnings", function(v) trace_warnings = v end)
+local trace_specifications = false trackers.register("fonts.specifications", function(v) trace_specifications = v end)
+local trace_rejections = false trackers.register("fonts.rejections", function(v) trace_rejections = v end)
+
+local report_names = logs.reporter("fonts","names")
+
+--[[ldx--
+<p>This module implements a name to filename resolver. Names are resolved
+using a table that has keys filtered from the font related files.</p>
+--ldx]]--
+
+fonts = fonts or { } -- also used elsewhere
+
+local names = fonts.names or allocate { }
+fonts.names = names
+
+local filters = names.filters or { }
+names.filters = filters
+
+local treatments = fonts.treatments or { }
+fonts.treatments = treatments
+
+names.data = names.data or allocate { }
+
+names.version = 1.125
+names.basename = "names"
+names.saved = false
+names.loaded = false
+names.be_clever = true
+names.enabled = true
+names.cache = containers.define("fonts","data",names.version,true)
+
+local usesystemfonts = true
+local autoreload = true
+
+directives.register("fonts.autoreload", function(v) autoreload = toboolean(v) end)
+directives.register("fonts.usesystemfonts", function(v) usesystemfonts = toboolean(v) end)
+
+--[[ldx--
+<p>A few helpers.</p>
+--ldx]]--
+
+-- -- what to do with these -- --
+--
+-- thin -> thin
+--
+-- regu -> regular -> normal
+-- norm -> normal -> normal
+-- stan -> standard -> normal
+-- medi -> medium
+-- ultr -> ultra
+-- ligh -> light
+-- heav -> heavy
+-- blac -> black
+-- thin
+-- book
+-- verylight
+--
+-- buch -> book
+-- buchschrift -> book
+-- halb -> demi
+-- halbfett -> demi
+-- mitt -> medium
+-- mittel -> medium
+-- fett -> bold
+-- mage -> light
+-- mager -> light
+-- nord -> normal
+-- gras -> normal
+
+local weights = Cs ( -- not extra
+ P("demibold")
+ + P("semibold")
+ + P("mediumbold")
+ + P("ultrabold")
+ + P("extrabold")
+ + P("ultralight")
+ + P("extralight")
+ + P("bold")
+ + P("demi")
+ + P("semi")
+ + P("light")
+ + P("medium")
+ + P("heavy")
+ + P("ultra")
+ + P("black")
+--+ P("bol") / "bold" -- blocks
+ + P("bol")
+ + P("regular") / "normal"
+)
+
+-- numeric_weights = {
+-- 200 = "extralight",
+-- 300 = "light",
+-- 400 = "book",
+-- 500 = "medium",
+-- 600 = "demi",
+-- 700 = "bold",
+-- 800 = "heavy",
+-- 900 = "black",
+-- }
+
+local normalized_weights = sparse {
+ regular = "normal",
+}
+
+local styles = Cs (
+ P("reverseoblique") / "reverseitalic"
+ + P("regular") / "normal"
+ + P("italic")
+ + P("oblique") / "italic"
+ + P("slanted")
+ + P("roman") / "normal"
+ + P("ital") / "italic" -- might be tricky
+ + P("ita") / "italic" -- might be tricky
+--+ P("obli") / "oblique"
+)
+
+local normalized_styles = sparse {
+ reverseoblique = "reverseitalic",
+ regular = "normal",
+ oblique = "italic",
+}
+
+local widths = Cs(
+ P("condensed")
+ + P("thin")
+ + P("expanded")
+ + P("cond") / "condensed"
+--+ P("expa") / "expanded"
+ + P("normal")
+ + P("book") / "normal"
+)
+
+local normalized_widths = sparse()
+
+local variants = Cs( -- fax casual
+ P("smallcaps")
+ + P("oldstyle")
+ + P("caps") / "smallcaps"
+)
+
+local normalized_variants = sparse()
+
+names.knownweights = {
+ "black",
+ "bold",
+ "demi",
+ "demibold",
+ "extrabold",
+ "heavy",
+ "light",
+ "medium",
+ "mediumbold",
+ "normal",
+ "regular",
+ "semi",
+ "semibold",
+ "ultra",
+ "ultrabold",
+ "ultralight",
+}
+
+names.knownstyles = {
+ "italic",
+ "normal",
+ "oblique",
+ "regular",
+ "reverseitalic",
+ "reverseoblique",
+ "roman",
+ "slanted",
+}
+
+names.knownwidths = {
+ "book",
+ "condensed",
+ "expanded",
+ "normal",
+ "thin",
+}
+
+names.knownvariants = {
+ "normal",
+ "oldstyle",
+ "smallcaps",
+}
+
+local remappedweights = {
+ [""] = "normal",
+ ["bol"] = "bold",
+}
+
+local remappedstyles = {
+ [""] = "normal",
+}
+
+local remappedwidths = {
+ [""] = "normal",
+}
+
+local remappedvariants = {
+ [""] = "normal",
+}
+
+names.remappedweights = remappedweights setmetatableindex(remappedweights ,"self")
+names.remappedstyles = remappedstyles setmetatableindex(remappedstyles ,"self")
+names.remappedwidths = remappedwidths setmetatableindex(remappedwidths ,"self")
+names.remappedvariants = remappedvariants setmetatableindex(remappedvariants,"self")
+
+local any = P(1)
+
+local analyzed_table
+
+local analyzer = Cs (
+ (
+ weights / function(s) analyzed_table[1] = s return "" end
+ + styles / function(s) analyzed_table[2] = s return "" end
+ + widths / function(s) analyzed_table[3] = s return "" end
+ + variants / function(s) analyzed_table[4] = s return "" end
+ + any
+ )^0
+)
+
+local splitter = lpeg.splitat("-")
+
+function names.splitspec(askedname)
+ local name, weight, style, width, variant = lpegmatch(splitter,askedname)
+ weight = weight and lpegmatch(weights, weight) or weight
+ style = style and lpegmatch(styles, style) or style
+ width = width and lpegmatch(widths, width) or width
+ variant = variant and lpegmatch(variants,variant) or variant
+ if trace_names then
+ report_names("requested name %a split in name %a, weight %a, style %a, width %a and variant %a",
+ askedname,name,weight,style,width,variant)
+ end
+ if not weight or not weight or not width or not variant then
+ weight, style, width, variant = weight or "normal", style or "normal", width or "normal", variant or "normal"
+ if trace_names then
+ report_names("request %a normalized to '%s-%s-%s-%s-%s'",
+ askedname,name,weight,style,width,variant)
+ end
+ end
+ return name or askedname, weight, style, width, variant
+end
+
+local function analyzespec(somename)
+ if somename then
+ analyzed_table = { }
+ local name = lpegmatch(analyzer,somename)
+ return name, analyzed_table[1], analyzed_table[2], analyzed_table[3], analyzed_table[4]
+ end
+end
+
+--[[ldx--
+<p>It would make sense to implement the filters in the related modules,
+but to keep the overview, we define them here.</p>
+--ldx]]--
+
+-- filters.dfont = get_font_info
+
+filters.otf = fonts.handlers.otf.readers.getinfo
+filters.ttf = filters.otf
+filters.ttc = filters.otf
+-- filters.ttx = filters.otf
+
+local function normalize(t)
+ local boundingbox = t.fontbbox
+ if boundingbox then
+ for i=1,#boundingbox do
+ boundingbox[i] = tonumber(boundingbox[i])
+ end
+ else
+ boundingbox = { 0, 0, 0, 0 }
+ end
+ return {
+ copyright = t.copyright,
+ fontname = t.fontname,
+ fullname = t.fullname,
+ familyname = t.familyname,
+ weight = t.weight,
+ widtht = t.width,
+ italicangle = tonumber(t.italicangle) or 0,
+ monospaced = toboolean(t.isfixedpitch) or false,
+ boundingbox = boundingbox,
+ version = t.version,
+ capheight = tonumber(t.capheight),
+ xheight = tonumber(t.xheight),
+ ascender = tonumber(t.ascender),
+ descender = tonumber(t.descender),
+ }
+end
+
+local p_spaces = lpegpatterns.whitespace
+local p_number = (R("09")+S(".-+"))^1 / tonumber
+local p_boolean = P("false") * Cc(false)
+ + P("false") * Cc(false)
+local p_string = P("(") * C((lpegpatterns.nestedparents + 1 - P(")"))^1) * P(")")
+local p_array = P("[") * Ct((p_number + p_boolean + p_string + p_spaces^1)^1) * P("]")
+ + P("{") * Ct((p_number + p_boolean + p_string + p_spaces^1)^1) * P("}")
+
+local p_key = P("/") * C(R("AZ","az")^1)
+local p_value = p_string
+ + p_number
+ + p_boolean
+ + p_array
+
+local p_entry = p_key * p_spaces^0 * p_value
+
+function filters.afm(name)
+ -- we could parse the afm file as well, and then report an error but
+ -- it's not worth the trouble
+ local pfbname = findfile(removesuffix(name)..".pfb","pfb") or ""
+ if pfbname == "" then
+ pfbname = findfile(nameonly(name)..".pfb","pfb") or ""
+ end
+ if pfbname ~= "" then
+ local f = io.open(name)
+ if f then
+ local hash = { }
+ local okay = false
+ for line in f:lines() do -- slow but only a few lines at the beginning
+ if find(line,"StartCharMetrics",1,true) then
+ break
+ else
+ local key, value = match(line,"^(.+)%s+(.+)%s*$")
+ if key and #key > 0 then
+ hash[lower(key)] = value
+ end
+ end
+ end
+ f:close()
+ return normalize(hash)
+ end
+ end
+ return nil, "no matching pfb file"
+end
+
+function filters.pfb(name)
+ local f = io.open(name)
+ if f then
+ local hash = { }
+ local okay = false
+ for line in f:lines() do -- slow but only a few lines at the beginning
+ if find(line,"dict begin") then
+ okay = true
+ elseif not okay then
+ -- go on
+ elseif find(line,"currentdict end") then
+ break
+ else
+ local key, value = lpegmatch(p_entry,line)
+ if key and value then
+ hash[lower(key)] = value
+ end
+ end
+ end
+ f:close()
+ return normalize(hash)
+ end
+end
+
+--[[ldx--
+<p>The scanner loops over the filters using the information stored in
+the file databases. Watch how we check not only for the names, but also
+for combination with the weight of a font.</p>
+--ldx]]--
+
+filters.list = {
+ "otf", "ttf", "ttc", "afm", -- no longer dfont support (for now)
+}
+
+-- to be considered: loop over paths per list entry (so first all otf ttf etc)
+
+names.fontconfigfile = "fonts.conf" -- a bit weird format, bonus feature
+names.osfontdirvariable = "OSFONTDIR" -- the official way, in minimals etc
+
+filters.paths = { }
+filters.names = { }
+
+function names.getpaths(trace)
+ local hash, result, r = { }, { }, 0
+ local function collect(t,where)
+ for i=1,#t do
+ local v = cleanpath(t[i])
+ v = gsub(v,"/+$","") -- not needed any more
+ local key = lower(v)
+ report_names("%a specifies path %a",where,v)
+ if not hash[key] then
+ r = r + 1
+ result[r] = v
+ hash[key] = true
+ end
+ end
+ end
+ local path = names.osfontdirvariable or ""
+ if path ~= "" then
+ collect(resolvers.expandedpathlist(path),path)
+ end
+ if xml then
+ local confname = resolvers.expansion("FONTCONFIG_FILE") or ""
+ if confname == "" then
+ confname = names.fontconfigfile or ""
+ end
+ if confname ~= "" then
+ -- first look in the tex tree
+ local name = findfile(confname,"fontconfig files") or ""
+ if name == "" then
+ -- after all, fontconfig is a unix thing
+ name = filejoin("/etc",confname)
+ if not lfs.isfile(name) then
+ name = "" -- force quit
+ end
+ end
+ if name ~= "" and lfs.isfile(name) then
+ if trace_names then
+ report_names("%s fontconfig file %a","loading",name)
+ end
+ local xmldata = xml.load(name)
+ -- begin of untested mess
+ xml.include(xmldata,"include","",true,function(incname)
+ if not is_qualified_path(incname) then
+ local path = pathpart(name) -- main name
+ if path ~= "" then
+ incname = filejoin(path,incname)
+ end
+ end
+ if lfs.isfile(incname) then
+ if trace_names then
+ report_names("%s fontconfig file %a","merging included",incname)
+ end
+ return io.loaddata(incname)
+ elseif trace_names then
+ report_names("%s fontconfig file: %a","ignoring included",incname)
+ end
+ end)
+ -- end of untested mess
+ local fontdirs = xml.collect_texts(xmldata,"dir",true)
+ if trace_names then
+ report_names("%s dirs found in fontconfig",#fontdirs)
+ end
+ collect(fontdirs,"fontconfig file")
+ end
+ end
+ end
+ function names.getpaths()
+ return result
+ end
+ return result
+end
+
+local function cleanname(name)
+ return (gsub(lower(name),"[^%a%d]",""))
+end
+
+local function cleanfilename(fullname,defaultsuffix)
+ local path, name, suffix = splitname(fullname)
+ name = gsub(lower(name),"[^%a%d]","")
+ if suffix and suffix ~= "" then
+ return name .. ".".. suffix
+ elseif defaultsuffix and defaultsuffix ~= "" then
+ return name .. ".".. defaultsuffix
+ else
+ return name
+ end
+end
+
+local sorter = function(a,b)
+ return a > b -- to be checked
+end
+
+names.cleanname = cleanname
+names.cleanfilename = cleanfilename
+
+-- local function check_names(result)
+-- local names = result.names
+-- if names then
+-- for i=1,#names do
+-- local name = names[i]
+-- if name.lang == "English (US)" then
+-- return name.names
+-- end
+-- end
+-- end
+-- return result
+-- end
+
+local function walk_tree(pathlist,suffix,identify)
+ if pathlist then
+ for i=1,#pathlist do
+ local path = pathlist[i]
+ path = cleanpath(path .. "/")
+ path = gsub(path,"/+","/")
+ local pattern = path .. "**." .. suffix -- ** forces recurse
+ report_names("globbing path %a",pattern)
+ local t = dir.glob(pattern)
+ sort(t,sorter)
+ for j=1,#t do
+ local completename = t[j]
+ identify(completename,basename(completename),suffix,completename)
+ end
+ end
+ end
+end
+
+-- "typographicfamily", -- preffamilyname
+-- "typographicsubfamily", -- prefmodifiers
+
+local function check_name(data,result,filename,modification,suffix,subfont)
+ -- shortcuts
+ local specifications = data.specifications
+ -- fetch
+ local familyname = result.familyname
+ local fullname = result.fullname
+ local fontname = result.fontname
+ local subfamily = result.subfamily
+ local modifiers = result.modifiers
+ local weight = result.weight
+ local italicangle = tonumber(result.italicangle)
+ local subfont = subfont
+ local rawname = fullname or fontname or familyname
+ local filebase = removesuffix(basename(filename))
+ local cleanfilename = cleanname(filebase) -- for WS
+ -- normalize
+ familyname = familyname and cleanname(familyname)
+ fullname = fullname and cleanname(fullname)
+ fontname = fontname and cleanname(fontname)
+ subfamily = subfamily and cleanname(subfamily)
+ modifiers = modifiers and cleanname(modifiers)
+ weight = weight and cleanname(weight)
+ italicangle = italicangle == 0 and nil
+ -- analyze
+ local a_name, a_weight, a_style, a_width, a_variant = analyzespec(fullname or fontname or familyname)
+ -- check
+ local width = a_width
+ local variant = a_variant
+ local style = modifiers and gsub(modifiers,"[^%a]","")
+ if not style and italicangle then
+ style = "italic"
+ end
+ if not variant or variant == "" then
+ variant = "normal"
+ end
+ if not weight or weight == "" then
+ weight = a_weight
+ end
+ if not style or style == "" then
+ style = a_style
+ end
+ if not familyname then
+ familyname = a_name
+ end
+ fontname = fontname or fullname or familyname or filebase -- maybe cleanfilename
+ fullname = fullname or fontname
+ familyname = familyname or fontname
+ -- we do these sparse -- todo: check table type or change names in ff loader
+ local units = result.units or 1000 -- can be zero too
+ local designsize = result.designsize or 0
+ local minsize = result.mindesign or 0
+ local maxsize = result.maxdesign or 0
+ local angle = result.italicangle or 0
+ local pfmwidth = result.pfmwidth or 0
+ local pfmweight = result.pfmweight or 0
+ --
+ specifications[#specifications + 1] = {
+ filename = filename, -- unresolved
+ cleanfilename = cleanfilename,
+ -- subfontindex = subfont,
+ format = lower(suffix),
+ subfont = subfont,
+ rawname = rawname,
+ familyname = familyname,
+ fullname = fullname,
+ fontname = fontname,
+ subfamily = subfamily,
+ modifiers = modifiers,
+ weight = weight,
+ style = style,
+ width = width,
+ variant = variant,
+ units = units ~= 1000 and units or nil,
+ pfmwidth = pfmwidth ~= 0 and pfmwidth or nil,
+ pfmweight = pfmweight ~= 0 and pfmweight or nil,
+ angle = angle ~= 0 and angle or nil,
+ minsize = minsize ~= 0 and minsize or nil,
+ maxsize = maxsize ~= 0 and maxsize or nil,
+ designsize = designsize ~= 0 and designsize or nil,
+ modification = modification ~= 0 and modification or nil,
+ }
+end
+
+local function cleanupkeywords()
+ local data = names.data
+ local specifications = names.data.specifications
+ if specifications then
+ local weights = { }
+ local styles = { }
+ local widths = { }
+ local variants = { }
+ for i=1,#specifications do
+ local s = specifications[i]
+ -- fix (sofar styles are taken from the name, and widths from the specification)
+ local _, b_weight, b_style, b_width, b_variant = analyzespec(s.weight)
+ local _, c_weight, c_style, c_width, c_variant = analyzespec(s.style)
+ local _, d_weight, d_style, d_width, d_variant = analyzespec(s.width)
+ local _, e_weight, e_style, e_width, e_variant = analyzespec(s.variant)
+ local _, f_weight, f_style, f_width, f_variant = analyzespec(s.fullname or "")
+ local weight = b_weight or c_weight or d_weight or e_weight or f_weight or "normal"
+ local style = b_style or c_style or d_style or e_style or f_style or "normal"
+ local width = b_width or c_width or d_width or e_width or f_width or "normal"
+ local variant = b_variant or c_variant or d_variant or e_variant or f_variant or "normal"
+ weight = remappedweights [weight or ""]
+ style = remappedstyles [style or ""]
+ width = remappedwidths [width or ""]
+ variant = remappedvariants[variant or ""]
+ weights [weight ] = (weights [weight ] or 0) + 1
+ styles [style ] = (styles [style ] or 0) + 1
+ widths [width ] = (widths [width ] or 0) + 1
+ variants[variant] = (variants[variant] or 0) + 1
+ if weight ~= s.weight then
+ s.fontweight = s.weight
+ end
+ s.weight, s.style, s.width, s.variant = weight, style, width, variant
+ end
+ local stats = data.statistics
+ stats.used_weights, stats.used_styles, stats.used_widths, stats.used_variants = weights, styles, widths, variants
+ end
+end
+
+local function collectstatistics()
+ local data = names.data
+ local specifications = data.specifications
+ if specifications then
+ local f_w = formatters["%i"]
+ local f_a = formatters["%0.2f"]
+ -- normal stuff
+ local weights = { }
+ local styles = { }
+ local widths = { }
+ local variants = { }
+ -- weird stuff
+ local angles = { }
+ -- extra stuff
+ local pfmweights = { } setmetatableindex(pfmweights,"table")
+ local pfmwidths = { } setmetatableindex(pfmwidths, "table")
+ -- main loop
+ for i=1,#specifications do
+ local s = specifications[i]
+ -- normal stuff
+ local weight = s.weight
+ local style = s.style
+ local width = s.width
+ local variant = s.variant
+ if weight then weights [weight ] = (weights [weight ] or 0) + 1 end
+ if style then styles [style ] = (styles [style ] or 0) + 1 end
+ if width then widths [width ] = (widths [width ] or 0) + 1 end
+ if variant then variants[variant] = (variants[variant] or 0) + 1 end
+ -- weird stuff
+ local angle = f_a(tonumber(s.angle) or 0)
+ angles[angle] = (angles[angles] or 0) + 1
+ -- extra stuff
+ local pfmweight = f_w(s.pfmweight or 0)
+ local pfmwidth = f_w(s.pfmwidth or 0)
+ local tweights = pfmweights[pfmweight]
+ local twidths = pfmwidths [pfmwidth]
+ tweights[pfmweight] = (tweights[pfmweight] or 0) + 1
+ twidths[pfmwidth] = (twidths [pfmwidth] or 0) + 1
+ end
+ --
+ local stats = data.statistics
+ stats.weights = weights
+ stats.styles = styles
+ stats.widths = widths
+ stats.variants = variants
+ stats.angles = angles
+ stats.pfmweights = pfmweights
+ stats.pfmwidths = pfmwidths
+ stats.fonts = #specifications
+ --
+ setmetatableindex(pfmweights,nil)
+ setmetatableindex(pfmwidths, nil)
+ --
+ report_names("")
+ report_names("weights")
+ report_names("")
+ report_names(formatters[" %T"](weights))
+ report_names("")
+ report_names("styles")
+ report_names("")
+ report_names(formatters[" %T"](styles))
+ report_names("")
+ report_names("widths")
+ report_names("")
+ report_names(formatters[" %T"](widths))
+ report_names("")
+ report_names("variants")
+ report_names("")
+ report_names(formatters[" %T"](variants))
+ report_names("")
+ report_names("angles")
+ report_names("")
+ report_names(formatters[" %T"](angles))
+ report_names("")
+ report_names("pfmweights")
+ report_names("")
+ for k, v in sortedhash(pfmweights) do
+ report_names(formatters[" %-10s: %T"](k,v))
+ end
+ report_names("")
+ report_names("pfmwidths")
+ report_names("")
+ for k, v in sortedhash(pfmwidths) do
+ report_names(formatters[" %-10s: %T"](k,v))
+ end
+ report_names("")
+ end
+end
+
+local function collecthashes()
+ local data = names.data
+ local mappings = data.mappings
+ local fallbacks = data.fallbacks
+ local specifications = data.specifications
+ local nofmappings = 0
+ local noffallbacks = 0
+ if specifications then
+ -- maybe multiple passes
+ for index=1,#specifications do
+ local s = specifications[index]
+ local format, fullname, fontname, familyname, weight, subfamily = s.format, s.fullname, s.fontname, s.familyname, s.weight, s.subfamily
+ local mf, ff = mappings[format], fallbacks[format]
+ if fullname and not mf[fullname] then
+ mf[fullname], nofmappings = index, nofmappings + 1
+ end
+ if fontname and not mf[fontname] then
+ mf[fontname], nofmappings = index, nofmappings + 1
+ end
+ if familyname and weight and weight ~= sub(familyname,#familyname-#weight+1,#familyname) then
+ local madename = familyname .. weight
+ if not mf[madename] and not ff[madename] then
+ ff[madename], noffallbacks = index, noffallbacks + 1
+ end
+ end
+ if familyname and subfamily and subfamily ~= sub(familyname,#familyname-#subfamily+1,#familyname) then
+ local extraname = familyname .. subfamily
+ if not mf[extraname] and not ff[extraname] then
+ ff[extraname], noffallbacks = index, noffallbacks + 1
+ end
+ end
+ if familyname and not mf[familyname] and not ff[familyname] then
+ ff[familyname], noffallbacks = index, noffallbacks + 1
+ end
+ end
+ end
+ return nofmappings, noffallbacks
+end
+
+local function collectfamilies()
+ local data = names.data
+ local specifications = data.specifications
+ local families = data.families
+ for index=1,#specifications do
+ local familyname = specifications[index].familyname
+ local family = families[familyname]
+ if not family then
+ families[familyname] = { index }
+ else
+ family[#family+1] = index
+ end
+ end
+end
+
+local function checkduplicate(where) -- fails on "Romantik" but that's a border case anyway
+ local data = names.data
+ local mapping = data[where]
+ local specifications = data.specifications
+ local loaded = { }
+ if specifications and mapping then
+ -- was: for _, m in sortedhash(mapping) do
+ local order = filters.list
+ for i=1,#order do
+ local m = mapping[order[i]]
+ for k, v in sortedhash(m) do
+ local s = specifications[v]
+ local hash = formatters["%s-%s-%s-%s-%s"](s.familyname,s.weight or "*",s.style or "*",s.width or "*",s.variant or "*")
+ local h = loaded[hash]
+ if h then
+ local ok = true
+ local fn = s.filename
+ for i=1,#h do
+ local hn = s.filename
+ if h[i] == fn then
+ ok = false
+ break
+ end
+ end
+ if ok then
+ h[#h+1] = fn
+ end
+ else
+ loaded[hash] = { s.filename }
+ end
+ end
+ end
+ end
+ local n = 0
+ for k, v in sortedhash(loaded) do
+ local nv = #v
+ if nv > 1 then
+ if trace_warnings then
+ report_names("lookup %a clashes with %a",k,v)
+ end
+ n = n + nv
+ end
+ end
+ report_names("%a double lookups in %a",n,where)
+end
+
+local function checkduplicates()
+ checkduplicate("mappings")
+ checkduplicate("fallbacks")
+end
+
+local function sorthashes()
+ local data = names.data
+ local list = filters.list
+ local mappings = data.mappings
+ local fallbacks = data.fallbacks
+ local sorted_mappings = { }
+ local sorted_fallbacks = { }
+ data.sorted_mappings = sorted_mappings
+ data.sorted_fallbacks = sorted_fallbacks
+ for i=1,#list do
+ local l = list[i]
+ sorted_mappings [l] = table.keys(mappings[l])
+ sorted_fallbacks[l] = table.keys(fallbacks[l])
+ sort(sorted_mappings [l],sorter)
+ sort(sorted_fallbacks[l],sorter)
+ end
+ data.sorted_families = table.keys(data.families)
+ sort(data.sorted_families,sorter)
+end
+
+local function unpackreferences()
+ local data = names.data
+ local specifications = data.specifications
+ if specifications then
+-- for k, v in next, data.families do
+ for k, v in sortedhash(data.families) do
+ for i=1,#v do
+ v[i] = specifications[v[i]]
+ end
+ end
+ local mappings = data.mappings
+ if mappings then
+-- for _, m in next, mappings do
+ for _, m in sortedhash(mappings) do
+-- for k, v in next, m do
+ for k, v in sortedhash(m) do
+ m[k] = specifications[v]
+ end
+ end
+ end
+ local fallbacks = data.fallbacks
+ if fallbacks then
+-- for _, f in next, fallbacks do
+ for _, f in sortedhash(fallbacks) do
+-- for k, v in next, f do
+ for k, v in sortedhash(f) do
+ f[k] = specifications[v]
+ end
+ end
+ end
+ end
+end
+
+local function analyzefiles(olddata)
+ if not trace_warnings then
+ report_names("warnings are disabled (tracker 'fonts.warnings')")
+ end
+ local data = names.data
+ local done = { }
+ local totalnofread = 0
+ local totalnofskipped = 0
+ local totalnofduplicates = 0
+ local nofread = 0
+ local nofskipped = 0
+ local nofduplicates = 0
+ local skip_paths = filters.paths
+ local skip_names = filters.names
+ local specifications = data.specifications
+ local oldindices = olddata and olddata.indices or { }
+ local oldspecifications = olddata and olddata.specifications or { }
+ local oldrejected = olddata and olddata.rejected or { }
+ local treatmentdata = treatments.data or { } -- when used outside context
+
+ local function identify(completename,name,suffix,storedname)
+ local pathpart, basepart = splitbase(completename)
+ nofread = nofread + 1
+ local treatment = treatmentdata[completename] or treatmentdata[basepart]
+ if treatment and treatment.ignored then
+ if trace_names or trace_rejections then
+ report_names("%s font %a is ignored, reason %a",suffix,completename,treatment.comment or "unknown")
+ end
+ nofskipped = nofskipped + 1
+ elseif done[name] then
+ if lower(completename) ~= lower(done[name]) then
+ -- already done (avoid otf afm clash)
+ if trace_names or trace_rejections then
+ report_names("%s font %a already done as %a",suffix,completename,done[name])
+ end
+ nofduplicates = nofduplicates + 1
+ nofskipped = nofskipped + 1
+ end
+ elseif not exists(completename) then
+ -- weird error
+ if trace_names or trace_rejections then
+ report_names("%s font %a does not really exist",suffix,completename)
+ end
+ nofskipped = nofskipped + 1
+ elseif not is_qualified_path(completename) and findfile(completename,suffix) == "" then
+ -- not locatable by backend anyway
+ if trace_names or trace_rejections then
+ report_names("%s font %a cannot be found by backend",suffix,completename)
+ end
+ nofskipped = nofskipped + 1
+ else
+ if #skip_paths > 0 then
+ for i=1,#skip_paths do
+ if find(pathpart,skip_paths[i]) then
+ if trace_names or trace_rejections then
+ report_names("rejecting path of %s font %a",suffix,completename)
+ end
+ nofskipped = nofskipped + 1
+ return
+ end
+ end
+ end
+ if #skip_names > 0 then
+ for i=1,#skip_paths do
+ if find(basepart,skip_names[i]) then
+ done[name] = true
+ if trace_names or trace_rejections then
+ report_names("rejecting name of %s font %a",suffix,completename)
+ end
+ nofskipped = nofskipped + 1
+ return
+ end
+ end
+ end
+ if trace_names then
+ report_names("identifying %s font %a",suffix,completename)
+ end
+ -- needs checking with ttc / ttx : date not updated ?
+ local result = nil
+ local modification = lfs.attributes(completename,"modification")
+ if olddata and modification and modification > 0 then
+ local oldindex = oldindices[storedname] -- index into specifications
+ if oldindex then
+ local oldspecification = oldspecifications[oldindex]
+ if oldspecification and oldspecification.filename == storedname then -- double check for out of sync
+ local oldmodification = oldspecification.modification
+ if oldmodification == modification then
+ result = oldspecification
+ specifications[#specifications + 1] = result
+ else
+ -- ??
+ end
+ else
+ -- ??
+ end
+ elseif oldrejected[storedname] == modification then
+ result = false
+ end
+ end
+ if result == nil then
+ local lsuffix = lower(suffix)
+ local result, message = filters[lsuffix](completename)
+ if result then
+ if #result > 0 then
+ for r=1,#result do
+ local ok = check_name(data,result[r],storedname,modification,suffix,r) -- subfonts start at zero
+ -- if not ok then
+ -- nofskipped = nofskipped + 1
+ -- end
+ end
+ else
+ local ok = check_name(data,result,storedname,modification,suffix)
+ -- if not ok then
+ -- nofskipped = nofskipped + 1
+ -- end
+ end
+ if trace_warnings and message and message ~= "" then
+ report_names("warning when identifying %s font %a, %s",suffix,completename,message)
+ end
+ elseif trace_warnings then
+ nofskipped = nofskipped + 1
+ report_names("error when identifying %s font %a, %s",suffix,completename,message or "unknown")
+ end
+ end
+ done[name] = completename
+ end
+ logs.flush() -- a bit overkill for each font, maybe not needed here
+ end
+ local function traverse(what, method)
+ local list = filters.list
+ for n=1,#list do
+ local suffix = list[n]
+ local t = os.gettimeofday() -- use elapser
+ nofread, nofskipped, nofduplicates = 0, 0, 0
+ suffix = lower(suffix)
+ report_names("identifying %s font files with suffix %a",what,suffix)
+ method(suffix)
+ suffix = upper(suffix)
+ report_names("identifying %s font files with suffix %a",what,suffix)
+ method(suffix)
+ totalnofread, totalnofskipped, totalnofduplicates = totalnofread + nofread, totalnofskipped + nofskipped, totalnofduplicates + nofduplicates
+ local elapsed = os.gettimeofday() - t
+ report_names("%s %s files identified, %s skipped, %s duplicates, %s hash entries added, runtime %0.3f seconds",nofread,what,nofskipped,nofduplicates,nofread-nofskipped,elapsed)
+ end
+ logs.flush()
+ end
+ -- problem .. this will not take care of duplicates
+ local function withtree(suffix)
+ resolvers.dowithfilesintree(".*%." .. suffix .. "$", function(method,root,path,name)
+ if method == "file" or method == "tree" then
+ local completename = root .."/" .. path .. "/" .. name
+ completename = resolveprefix(completename) -- no shortcut
+ identify(completename,name,suffix,name)
+ return true
+ end
+ end, function(blobtype,blobpath,pattern)
+ blobpath = resolveprefix(blobpath) -- no shortcut
+ report_names("scanning path %a for %s files",blobpath,suffix)
+ end, function(blobtype,blobpath,pattern,total,checked,done)
+ blobpath = resolveprefix(blobpath) -- no shortcut
+ report_names("%s entries found, %s %s files checked, %s okay",total,checked,suffix,done)
+ end)
+ end
+ local function withlsr(suffix) -- all trees
+ -- we do this only for a stupid names run, not used for context itself,
+ -- using the vars is too clumsy so we just stick to a full scan instead
+ local pathlist = resolvers.splitpath(resolvers.showpath("ls-R") or "")
+ walk_tree(pathlist,suffix,identify)
+ end
+ local function withsystem(suffix) -- OSFONTDIR cum suis
+ walk_tree(names.getpaths(trace),suffix,identify)
+ end
+ traverse("tree",withtree) -- TEXTREE only
+ if not usesystemfonts then
+ report_names("ignoring system fonts")
+ elseif texconfig.kpse_init then
+ traverse("lsr", withlsr)
+ else
+ traverse("system", withsystem)
+ end
+ data.statistics.readfiles = totalnofread
+ data.statistics.skippedfiles = totalnofskipped
+ data.statistics.duplicatefiles = totalnofduplicates
+end
+
+local function addfilenames()
+ local data = names.data
+ local specifications = data.specifications
+ local indices = { }
+ local files = { }
+ for i=1,#specifications do
+ local fullname = specifications[i].filename
+ files[cleanfilename(fullname)] = fullname
+ indices[fullname] = i
+ end
+ data.files = files
+ data.indices = indices
+end
+
+local function rejectclashes() -- just to be sure, so no explicit afm will be found then
+ local specifications = names.data.specifications
+ local used = { }
+ local okay = { }
+ local rejected = { } -- only keep modification
+ local o = 0
+ for i=1,#specifications do
+ local s = specifications[i]
+ local f = s.fontname
+ if f then
+ local fnd = used[f]
+ local fnm = s.filename
+ if fnd then
+ if trace_warnings then
+ report_names("fontname %a clashes, %a rejected in favor of %a",f,fnm,fnd)
+ end
+ rejected[f] = s.modification
+ else
+ used[f] = fnm
+ o = o + 1
+ okay[o] = s
+ end
+ else
+ o = o + 1
+ okay[o] = s
+ end
+ end
+ local d = #specifications - #okay
+ if d > 0 then
+ report_names("%s files rejected due to clashes",d)
+ end
+ names.data.specifications = okay
+ names.data.rejected = rejected
+end
+
+local function resetdata()
+ local mappings = { }
+ local fallbacks = { }
+ for _, k in next, filters.list do
+ mappings [k] = { }
+ fallbacks[k] = { }
+ end
+ names.data = {
+ version = names.version,
+ mappings = mappings,
+ fallbacks = fallbacks,
+ specifications = { },
+ families = { },
+ statistics = { },
+ names = { },
+ indices = { },
+ rejected = { },
+ datastate = resolvers.datastate(),
+ }
+end
+
+function names.identify(force)
+ local starttime = os.gettimeofday() -- use elapser
+ resetdata()
+ analyzefiles(not force and names.readdata(names.basename))
+ rejectclashes()
+ collectfamilies()
+ -- collectstatistics()
+ cleanupkeywords()
+ collecthashes()
+ checkduplicates()
+ addfilenames()
+ -- sorthashes() -- will be resorted when saved
+ collectstatistics()
+ report_names("total scan time %0.3f seconds",os.gettimeofday()-starttime)
+end
+
+function names.is_permitted(name)
+ return containers.is_usable(names.cache, name)
+end
+function names.writedata(name,data)
+ containers.write(names.cache,name,data)
+end
+function names.readdata(name)
+ return containers.read(names.cache,name)
+end
+
+function names.load(reload,force)
+ if not names.loaded then
+ if reload then
+ if names.is_permitted(names.basename) then
+ names.identify(force)
+ names.writedata(names.basename,names.data)
+ else
+ report_names("unable to access database cache")
+ end
+ names.saved = true
+ end
+ local data = names.readdata(names.basename)
+ names.data = data
+ if not names.saved then
+ if not data or not next(data) or not data.specifications or not next(data.specifications) then
+ names.load(true)
+ end
+ names.saved = true
+ end
+ if not data then
+ report_names("accessing the data table failed")
+ else
+ unpackreferences()
+ sorthashes()
+ end
+ names.loaded = true
+ end
+end
+
+local function list_them(mapping,sorted,pattern,t,all)
+ if mapping[pattern] then
+ t[pattern] = mapping[pattern]
+ else
+ for k=1,#sorted do
+ local v = sorted[k]
+ if not t[v] and find(v,pattern) then
+ t[v] = mapping[v]
+ if not all then
+ return
+ end
+ end
+ end
+ end
+end
+
+function names.list(pattern,reload,all) -- here?
+ names.load() -- todo reload
+ if names.loaded then
+ local t = { }
+ local data = names.data
+ if data then
+ local list = filters.list
+ local mappings = data.mappings
+ local sorted_mappings = data.sorted_mappings
+ local fallbacks = data.fallbacks
+ local sorted_fallbacks = data.sorted_fallbacks
+ for i=1,#list do
+ local format = list[i]
+ list_them(mappings[format],sorted_mappings[format],pattern,t,all)
+ if next(t) and not all then
+ return t
+ end
+ list_them(fallbacks[format],sorted_fallbacks[format],pattern,t,all)
+ if next(t) and not all then
+ return t
+ end
+ end
+ end
+ return t
+ end
+end
+
+local reloaded = false
+
+local function is_reloaded()
+ if not reloaded then
+ local data = names.data
+ if autoreload then
+ local c_status = serialize(resolvers.datastate())
+ local f_status = serialize(data.datastate)
+ if c_status == f_status then
+ if trace_names then
+ report_names("font database has matching configuration and file hashes")
+ end
+ return
+ else
+ report_names("font database has mismatching configuration and file hashes")
+ end
+ else
+ report_names("font database is regenerated (controlled by directive 'fonts.autoreload')")
+ end
+ names.loaded = false
+ reloaded = true
+ logs.flush()
+ names.load(true)
+ end
+end
+
+--[[ldx--
+<p>The resolver also checks if the cached names are loaded. Being clever
+here is for testing purposes only (it deals with names prefixed by an
+encoding name).</p>
+--ldx]]--
+
+local function fuzzy(mapping,sorted,name,sub)
+ local condensed = gsub(name,"[^%a%d]","")
+ for k=1,#sorted do
+ local v = sorted[k]
+ if find(v,condensed) then
+ return mapping[v], v
+ end
+ end
+end
+
+-- we could cache a lookup .. maybe some day ... (only when auto loaded!)
+
+local function foundname(name,sub) -- sub is not used currently
+ local data = names.data
+ local mappings = data.mappings
+ local sorted_mappings = data.sorted_mappings
+ local fallbacks = data.fallbacks
+ local sorted_fallbacks = data.sorted_fallbacks
+ local list = filters.list
+ -- dilemma: we lookup in the order otf ttf ttc ... afm but now an otf fallback
+ -- can come after an afm match ... well, one should provide nice names anyway
+ -- and having two lists is not an option
+ for i=1,#list do
+ local l = list[i]
+ local found = mappings[l][name]
+ if found then
+ if trace_names then
+ report_names("resolved via direct name match: %a",name)
+ end
+ return found
+ end
+ end
+ for i=1,#list do
+ local l = list[i]
+ local found, fname = fuzzy(mappings[l],sorted_mappings[l],name,sub)
+ if found then
+ if trace_names then
+ report_names("resolved via fuzzy name match: %a onto %a",name,fname)
+ end
+ return found
+ end
+ end
+ for i=1,#list do
+ local l = list[i]
+ local found = fallbacks[l][name]
+ if found then
+ if trace_names then
+ report_names("resolved via direct fallback match: %a",name)
+ end
+ return found
+ end
+ end
+ for i=1,#list do
+ local l = list[i]
+ local found, fname = fuzzy(sorted_mappings[l],sorted_fallbacks[l],name,sub)
+ if found then
+ if trace_names then
+ report_names("resolved via fuzzy fallback match: %a onto %a",name,fname)
+ end
+ return found
+ end
+ end
+ if trace_names then
+ report_names("font with name %a cannot be found",name)
+ end
+end
+
+function names.resolvedspecification(askedname,sub)
+ if askedname and askedname ~= "" and names.enabled then
+ askedname = cleanname(askedname)
+ names.load()
+ local found = foundname(askedname,sub)
+ if not found and is_reloaded() then
+ found = foundname(askedname,sub)
+ end
+ return found
+ end
+end
+
+function names.resolve(askedname,sub)
+ local found = names.resolvedspecification(askedname,sub)
+ if found then
+ return found.filename, found.subfont and found.rawname, found.subfont
+ end
+end
+
+-- function names.getfilename(askedname,suffix) -- last resort, strip funny chars
+-- names.load()
+-- local files = names.data.files
+-- askedname = files and files[cleanfilename(askedname,suffix)] or ""
+-- if askedname == "" then
+-- return ""
+-- else -- never entered
+-- return resolvers.findbinfile(askedname,suffix) or ""
+-- end
+-- end
+
+function names.getfilename(askedname,suffix) -- last resort, strip funny chars
+ names.load()
+ local files = names.data.files
+ local cleanname = cleanfilename(askedname,suffix)
+ local found = files and files[cleanname] or ""
+ if found == "" and is_reloaded() then
+ files = names.data.files
+ found = files and files[cleanname] or ""
+ end
+ if found and found ~= "" then
+ return resolvers.findbinfile(found,suffix) or "" -- we still need to locate it
+ end
+end
+
+-- specified search
+
+local function s_collect_weight_style_width_variant(found,done,all,weight,style,width,variant,family)
+ if family then
+ for i=1,#family do
+ local f = family[i]
+ if f and weight == f.weight and style == f.style and width == f.width and variant == f.variant then
+ found[#found+1], done[f] = f, true
+ if not all then return end
+ end
+ end
+ end
+end
+local function m_collect_weight_style_width_variant(found,done,all,weight,style,width,variant,families,sorted,strictname)
+ for i=1,#sorted do
+ local k = sorted[i]
+ local family = families[k]
+ for i=1,#family do
+ local f = family[i]
+ if not done[f] and weight == f.weight and style == f.style and width == f.width and variant == f.variant and find(f.fontname,strictname) then
+ found[#found+1], done[f] = f, true
+ if not all then return end
+ end
+ end
+ end
+end
+
+local function s_collect_weight_style_width(found,done,all,weight,style,width,family)
+ if family then
+ for i=1,#family do
+ local f = family[i]
+ if f and weight == f.weight and style == f.style and width == f.width then
+ found[#found+1], done[f] = f, true
+ if not all then return end
+ end
+ end
+ end
+end
+local function m_collect_weight_style_width(found,done,all,weight,style,width,families,sorted,strictname)
+ for i=1,#sorted do
+ local k = sorted[i]
+ local family = families[k]
+ for i=1,#family do
+ local f = family[i]
+ if not done[f] and weight == f.weight and style == f.style and width == f.width and find(f.fontname,strictname) then
+ found[#found+1], done[f] = f, true
+ if not all then return end
+ end
+ end
+ end
+end
+
+local function s_collect_weight_style(found,done,all,weight,style,family)
+ if family then
+ for i=1,#family do local f = family[i]
+ if f and weight == f.weight and style == f.style then
+ found[#found+1], done[f] = f, true
+ if not all then return end
+ end
+ end
+ end
+end
+local function m_collect_weight_style(found,done,all,weight,style,families,sorted,strictname)
+ for i=1,#sorted do
+ local k = sorted[i]
+ local family = families[k]
+ for i=1,#family do
+ local f = family[i]
+ if not done[f] and weight == f.weight and style == f.style and find(f.fontname,strictname) then
+ found[#found+1], done[f] = f, true
+ if not all then return end
+ end
+ end
+ end
+end
+
+local function s_collect_style_width(found,done,all,style,width,family)
+ if family then
+ for i=1,#family do local f = family[i]
+ if f and style == f.style and width == f.width then
+ found[#found+1], done[f] = f, true
+ if not all then return end
+ end
+ end
+ end
+end
+local function m_collect_style_width(found,done,all,style,width,families,sorted,strictname)
+ for i=1,#sorted do
+ local k = sorted[i]
+ local family = families[k]
+ for i=1,#family do
+ local f = family[i]
+ if not done[f] and style == f.style and width == f.width and find(f.fontname,strictname) then
+ found[#found+1], done[f] = f, true
+ if not all then return end
+ end
+ end
+ end
+end
+
+local function s_collect_weight(found,done,all,weight,family)
+ if family then
+ for i=1,#family do local f = family[i]
+ if f and weight == f.weight then
+ found[#found+1], done[f] = f, true
+ if not all then return end
+ end
+ end
+ end
+end
+local function m_collect_weight(found,done,all,weight,families,sorted,strictname)
+ for i=1,#sorted do
+ local k = sorted[i]
+ local family = families[k]
+ for i=1,#family do
+ local f = family[i]
+ if not done[f] and weight == f.weight and find(f.fontname,strictname) then
+ found[#found+1], done[f] = f, true
+ if not all then return end
+ end
+ end
+ end
+end
+
+local function s_collect_style(found,done,all,style,family)
+ if family then
+ for i=1,#family do local f = family[i]
+ if f and style == f.style then
+ found[#found+1], done[f] = f, true
+ if not all then return end
+ end
+ end
+ end
+end
+local function m_collect_style(found,done,all,style,families,sorted,strictname)
+ for i=1,#sorted do
+ local k = sorted[i]
+ local family = families[k]
+ for i=1,#family do
+ local f = family[i]
+ if not done[f] and style == f.style and find(f.fontname,strictname) then
+ found[#found+1], done[f] = f, true
+ if not all then return end
+ end
+ end
+ end
+end
+
+local function s_collect_width(found,done,all,width,family)
+ if family then
+ for i=1,#family do local f = family[i]
+ if f and width == f.width then
+ found[#found+1], done[f] = f, true
+ if not all then return end
+ end
+ end
+ end
+end
+local function m_collect_width(found,done,all,width,families,sorted,strictname)
+ for i=1,#sorted do
+ local k = sorted[i]
+ local family = families[k]
+ for i=1,#family do
+ local f = family[i]
+ if not done[f] and width == f.width and find(f.fontname,strictname) then
+ found[#found+1], done[f] = f, true
+ if not all then return end
+ end
+ end
+ end
+end
+
+local function s_collect(found,done,all,family)
+ if family then
+ for i=1,#family do local f = family[i]
+ if f then
+ found[#found+1], done[f] = f, true
+ if not all then return end
+ end
+ end
+ end
+end
+local function m_collect(found,done,all,families,sorted,strictname)
+ for i=1,#sorted do
+ local k = sorted[i]
+ local family = families[k]
+ for i=1,#family do
+ local f = family[i]
+ if not done[f] and find(f.fontname,strictname) then
+ found[#found+1], done[f] = f, true
+ if not all then return end
+ end
+ end
+ end
+end
+
+local function collect(stage,found,done,name,weight,style,width,variant,all)
+ local data = names.data
+ local families = data.families
+ local sorted = data.sorted_families
+ local strictname = "^".. name -- to be checked
+ local family = families[name]
+ if trace_names then
+ report_names("resolving name %a, weight %a, style %a, width %a, variant %a",name,weight,style,width,variant)
+ end
+ if weight and weight ~= "" then
+ if style and style ~= "" then
+ if width and width ~= "" then
+ if variant and variant ~= "" then
+ if trace_names then
+ report_names("resolving stage %s, name %a, weight %a, style %a, width %a, variant %a",stage,name,weight,style,width,variant)
+ end
+ s_collect_weight_style_width_variant(found,done,all,weight,style,width,variant,family)
+ m_collect_weight_style_width_variant(found,done,all,weight,style,width,variant,families,sorted,strictname)
+ else
+ if trace_names then
+ report_names("resolving stage %s, name %a, weight %a, style %a, width %a",stage,name,weight,style,width)
+ end
+ s_collect_weight_style_width(found,done,all,weight,style,width,family)
+ m_collect_weight_style_width(found,done,all,weight,style,width,families,sorted,strictname)
+ end
+ else
+ if trace_names then
+ report_names("resolving stage %s, name %a, weight %a, style %a",stage,name,weight,style)
+ end
+ s_collect_weight_style(found,done,all,weight,style,family)
+ m_collect_weight_style(found,done,all,weight,style,families,sorted,strictname)
+ end
+ else
+ if trace_names then
+ report_names("resolving stage %s, name %a, weight %a",stage,name,weight)
+ end
+ s_collect_weight(found,done,all,weight,family)
+ m_collect_weight(found,done,all,weight,families,sorted,strictname)
+ end
+ elseif style and style ~= "" then
+ if width and width ~= "" then
+ if trace_names then
+ report_names("resolving stage %s, name %a, style %a, width %a",stage,name,style,width)
+ end
+ s_collect_style_width(found,done,all,style,width,family)
+ m_collect_style_width(found,done,all,style,width,families,sorted,strictname)
+ else
+ if trace_names then
+ report_names("resolving stage %s, name %a, style %a",stage,name,style)
+ end
+ s_collect_style(found,done,all,style,family)
+ m_collect_style(found,done,all,style,families,sorted,strictname)
+ end
+ elseif width and width ~= "" then
+ if trace_names then
+ report_names("resolving stage %s, name %a, width %a",stage,name,width)
+ end
+ s_collect_width(found,done,all,width,family)
+ m_collect_width(found,done,all,width,families,sorted,strictname)
+ else
+ if trace_names then
+ report_names("resolving stage %s, name %a",stage,name)
+ end
+ s_collect(found,done,all,family)
+ m_collect(found,done,all,families,sorted,strictname)
+ end
+end
+
+local function heuristic(name,weight,style,width,variant,all) -- todo: fallbacks
+ local found, done = { }, { }
+--~ print(name,weight,style,width,variant)
+ weight, style, width, variant = weight or "normal", style or "normal", width or "normal", variant or "normal"
+ name = cleanname(name)
+ collect(1,found,done,name,weight,style,width,variant,all)
+ -- still needed ?
+ if #found == 0 and variant ~= "normal" then -- not weight
+ variant = "normal"
+ collect(4,found,done,name,weight,style,width,variant,all)
+ end
+ if #found == 0 and width ~= "normal" then
+ width = "normal"
+ collect(2,found,done,name,weight,style,width,variant,all)
+ end
+ if #found == 0 and weight ~= "normal" then -- not style
+ weight = "normal"
+ collect(3,found,done,name,weight,style,width,variant,all)
+ end
+ if #found == 0 and style ~= "normal" then -- not weight
+ style = "normal"
+ collect(4,found,done,name,weight,style,width,variant,all)
+ end
+ --
+ local nf = #found
+ if trace_names then
+ if nf then
+ local t = { }
+ for i=1,nf do
+ t[i] = formatters["%a"](found[i].fontname)
+ end
+ report_names("name %a resolved to %s instances: % t",name,nf,t)
+ else
+ report_names("name %a unresolved",name)
+ end
+ end
+ if all then
+ return nf > 0 and found
+ else
+ return found[1]
+ end
+end
+
+function names.specification(askedname,weight,style,width,variant,reload,all)
+ if askedname and askedname ~= "" and names.enabled then
+ askedname = cleanname(askedname) -- or cleanname
+ names.load(reload)
+ local found = heuristic(askedname,weight,style,width,variant,all)
+ if not found and is_reloaded() then
+ found = heuristic(askedname,weight,style,width,variant,all)
+ if not filename then
+ found = foundname(askedname) -- old method
+ end
+ end
+ return found
+ end
+end
+
+function names.collect(askedname,weight,style,width,variant,reload,all)
+ if askedname and askedname ~= "" and names.enabled then
+ askedname = cleanname(askedname) -- or cleanname
+ names.load(reload)
+ local list = heuristic(askedname,weight,style,width,variant,true)
+ if not list or #list == 0 and is_reloaded() then
+ list = heuristic(askedname,weight,style,width,variant,true)
+ end
+ return list
+ end
+end
+
+function names.collectspec(askedname,reload,all)
+ local name, weight, style, width, variant = names.splitspec(askedname)
+ return names.collect(name,weight,style,width,variant,reload,all)
+end
+
+function names.resolvespec(askedname,sub) -- redefined later
+ local found = names.specification(names.splitspec(askedname))
+ if found then
+ return found.filename, found.subfont and found.rawname
+ end
+end
+
+function names.collectfiles(askedname,reload) -- no all
+ if askedname and askedname ~= "" and names.enabled then
+ askedname = cleanname(askedname) -- or cleanname
+ names.load(reload)
+ local list = { }
+ local specifications = names.data.specifications
+ for i=1,#specifications do
+ local s = specifications[i]
+ if find(cleanname(basename(s.filename)),askedname) then
+ list[#list+1] = s
+ end
+ end
+ return list
+ end
+end
+
+-- todo:
+--
+-- blacklisted = {
+-- ["cmr10.ttf"] = "completely messed up",
+-- }
+
+function names.exists(name)
+ local found = false
+ local list = filters.list
+ for k=1,#list do
+ local v = list[k]
+ found = (findfile(name,v) or "") ~= ""
+ if found then
+ return found
+ end
+ end
+ return (findfile(name,"tfm") or "") ~= "" or (names.resolve(name) or "") ~= ""
+end
+
+local lastlookups, lastpattern = { }, ""
+
+-- function names.lookup(pattern,name,reload) -- todo: find
+-- if lastpattern ~= pattern then
+-- names.load(reload)
+-- local specifications = names.data.specifications
+-- local families = names.data.families
+-- local lookups = specifications
+-- if name then
+-- lookups = families[name]
+-- elseif not find(pattern,"=",1,true) then
+-- lookups = families[pattern]
+-- end
+-- if trace_names then
+-- report_names("starting with %s lookups for %a",#lookups,pattern)
+-- end
+-- if lookups then
+-- for key, value in gmatch(pattern,"([^=,]+)=([^=,]+)") do
+-- local t, n = { }, 0
+-- if find(value,"*",1,true) then
+-- value = topattern(value)
+-- for i=1,#lookups do
+-- local s = lookups[i]
+-- if find(s[key],value) then
+-- n = n + 1
+-- t[n] = lookups[i]
+-- end
+-- end
+-- else
+-- for i=1,#lookups do
+-- local s = lookups[i]
+-- if s[key] == value then
+-- n = n + 1
+-- t[n] = lookups[i]
+-- end
+-- end
+-- end
+-- if trace_names then
+-- report_names("%s matches for key %a with value %a",#t,key,value)
+-- end
+-- lookups = t
+-- end
+-- end
+-- lastpattern = pattern
+-- lastlookups = lookups or { }
+-- end
+-- return #lastlookups
+-- end
+
+local function look_them_up(lookups,specification)
+ for key, value in sortedhash(specification) do
+ local t, n = { }, 0
+ if find(value,"*",1,true) then
+ value = topattern(value)
+ for i=1,#lookups do
+ local s = lookups[i]
+ if find(s[key],value) then
+ n = n + 1
+ t[n] = lookups[i]
+ end
+ end
+ else
+ for i=1,#lookups do
+ local s = lookups[i]
+ if s[key] == value then
+ n = n + 1
+ t[n] = lookups[i]
+ end
+ end
+ end
+ if trace_names then
+ report_names("%s matches for key %a with value %a",#t,key,value)
+ end
+ lookups = t
+ end
+ return lookups
+end
+
+local function first_look(name,reload)
+ names.load(reload)
+ local data = names.data
+ local specifications = data.specifications
+ local families = data.families
+ if name then
+ return families[name]
+ else
+ return specifications
+ end
+end
+
+function names.lookup(pattern,name,reload) -- todo: find
+ names.load(reload)
+ local data = names.data
+ local specifications = data.specifications
+ local families = data.families
+ local lookups = specifications
+ if name then
+ name = cleanname(name)
+ end
+ if type(pattern) == "table" then
+ local familyname = pattern.familyname
+ if familyname then
+ familyname = cleanname(familyname)
+ pattern.familyname = familyname
+ end
+ local lookups = first_look(name or familyname,reload)
+ if lookups then
+ if trace_names then
+ report_names("starting with %s lookups for '%T'",#lookups,pattern)
+ end
+ lookups = look_them_up(lookups,pattern)
+ end
+ lastpattern = false
+ lastlookups = lookups or { }
+ elseif lastpattern ~= pattern then
+ local lookups = first_look(name or (not find(pattern,"=",1,true) and pattern),reload)
+ if lookups then
+ if trace_names then
+ report_names("starting with %s lookups for %a",#lookups,pattern)
+ end
+ local specification = settings_to_hash(pattern)
+ local familyname = specification.familyname
+ if familyname then
+ familyname = cleanname(familyname)
+ specification.familyname = familyname
+ end
+ lookups = look_them_up(lookups,specification)
+ end
+ lastpattern = pattern
+ lastlookups = lookups or { }
+ end
+ return #lastlookups
+end
+
+function names.getlookupkey(key,n)
+ local l = lastlookups[n or 1]
+ return (l and l[key]) or ""
+end
+
+function names.noflookups()
+ return #lastlookups
+end
+
+function names.getlookups(pattern,name,reload)
+ if pattern then
+ names.lookup(pattern,name,reload)
+ end
+ return lastlookups
+end
+
+-- The following is new ... watch the overload!
+
+local specifications = allocate()
+names.specifications = specifications
+
+-- files = {
+-- name = "antykwapoltawskiego",
+-- list = {
+-- ["AntPoltLtCond-Regular.otf"] = {
+-- -- name = "antykwapoltawskiego",
+-- style = "regular",
+-- weight = "light",
+-- width = "condensed",
+-- },
+-- },
+-- }
+
+function names.register(files)
+ if files then
+ local list, commonname = files.list, files.name
+ if list then
+ local n, m = 0, 0
+ for filename, filespec in sortedhash(list) do
+ local name = lower(filespec.name or commonname)
+ if name and name ~= "" then
+ local style = normalized_styles [lower(filespec.style or "normal")]
+ local width = normalized_widths [lower(filespec.width or "normal")]
+ local weight = normalized_weights [lower(filespec.weight or "normal")]
+ local variant = normalized_variants[lower(filespec.variant or "normal")]
+ local weights = specifications[name ] if not weights then weights = { } specifications[name ] = weights end
+ local styles = weights [weight] if not styles then styles = { } weights [weight] = styles end
+ local widths = styles [style ] if not widths then widths = { } styles [style ] = widths end
+ local variants = widths [width ] if not variants then variants = { } widths [width ] = variants end
+ variants[variant] = filename
+ n = n + 1
+ else
+ m = m + 1
+ end
+ end
+ if trace_specifications then
+ report_names("%s filenames registered, %s filenames rejected",n,m)
+ end
+ end
+ end
+end
+
+function names.registered(name,weight,style,width,variant)
+ local ok = specifications[name]
+ ok = ok and (ok[(weight and weight ~= "" and weight ) or "normal"] or ok.normal)
+ ok = ok and (ok[(style and style ~= "" and style ) or "normal"] or ok.normal)
+ ok = ok and (ok[(width and width ~= "" and width ) or "normal"] or ok.normal)
+ ok = ok and (ok[(variant and variant ~= "" and variant) or "normal"] or ok.normal)
+ --
+ -- todo: same fallbacks as with database
+ --
+ if ok then
+ return {
+ filename = ok,
+ subname = "",
+ -- rawname = nil,
+ }
+ end
+end
+
+function names.resolvespec(askedname,sub) -- overloads previous definition
+ local name, weight, style, width, variant = names.splitspec(askedname)
+ if trace_specifications then
+ report_names("resolving specification: %a to name=%s, weight=%s, style=%s, width=%s, variant=%s",askedname,name,weight,style,width,variant)
+ end
+ local found = names.registered(name,weight,style,width,variant)
+ if found and found.filename then
+ if trace_specifications then
+ report_names("resolved by registered names: %a to %s",askedname,found.filename)
+ end
+ return found.filename, found.subname, found.rawname
+ else
+ found = names.specification(name,weight,style,width,variant)
+ if found and found.filename then
+ if trace_specifications then
+ report_names("resolved by font database: %a to %s",askedname,found.filename)
+ end
+ return found.filename, found.subfont and found.rawname
+ end
+ end
+ if trace_specifications then
+ report_names("unresolved: %s",askedname)
+ end
+end
+
+-- We could generate typescripts with designsize info from the name database but
+-- it's not worth the trouble as font names remain a mess: for instance how do we
+-- idenfity a font? Names, families, subfamilies or whatever snippet can contain
+-- a number related to the design size and so we end up with fuzzy logic again. So,
+-- instead it's easier to make a few goody files.
+--
+-- local hash = { }
+--
+-- for i=1,#specifications do
+-- local s = specifications[i]
+-- local min = s.minsize or 0
+-- local max = s.maxsize or 0
+-- if min ~= 0 or max ~= 0 then
+-- -- the usual name mess:
+-- -- antykwa has modifiers so we need to take these into account, otherwise we get weird combinations
+-- -- ebgaramond has modifiers with the size encoded, so we need to strip this in order to recognized similar styles
+-- -- lm has 'slanted appended in some names so how to choose that one
+-- --
+-- local modifier = string.gsub(s.modifiers or "normal","%d","")
+-- -- print funny modifier
+-- local instance = string.formatters["%s-%s-%s-%s-%s-%s"](s.familyname,s.width,s.style,s.weight,s.variant,modifier)
+-- local h = hash[instance]
+-- if not h then
+-- h = { }
+-- hash[instance] = h
+-- end
+-- size = string.formatters["%0.1fpt"]((min)/10)
+-- h[size] = s.filename
+-- end
+-- end
+--
+-- local newhash = { }
+--
+-- for k, v in next, hash do
+-- if next(v,next(v)) then
+-- -- local instance = string.match(k,"(.+)%-.+%-.+%-.+$")
+-- local instance = string.match(k,"(.+)%-.+%-.+$")
+-- local instance = string.gsub(instance,"%-normal$","")
+-- if not newhash[instance] then
+-- newhash[instance] = v
+-- end
+-- end
+-- end
+--
+-- inspect(newhash)
+
+-- example made for luatex list (unlikely to be used):
+--
+-- local command = [[reg QUERY "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"]]
+-- local pattern = ".-[\n\r]+%s+(.-)%s%(([^%)]+)%)%s+REG_SZ%s+(%S+)%s+"
+--
+-- local function getnamesfromregistry()
+-- local data = os.resultof(command)
+-- local list = { }
+-- for name, format, filename in string.gmatch(data,pattern) do
+-- list[name] = filename
+-- end
+-- return list
+-- end