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" } ------------------------------------------------------------------------------- -- Define a new private environment into which to load "font-syn.lua". -- In 5.3, setting this means 'free' variables get translated to -- . -- This means, I think, that both assignments and references use . -- But luaotfload.fontloader has a metatable with __index where __index is a -- reference to _G. local _ENV = table.copy(luaotfload.fontloader) -- Define some functions required by "font-syn.lua". local split = "^(.-)([^/]-)([^/]-)$" ---@function dowithfilesintree() -- {{{ local function dowithfilesintree(pattern, handle) local files = luaotfload.aux.font_index().files.full local match = string.match for i = 1, #files do local filename = files[i] if match(filename, pattern) then local root, path, name = match(filename, split) -- Path is always empty. handle("file", root, path, name) end end end -- }}} ---@function setmetatableindex() -- {{{ local function setmetatableindex(t, k) if k == "self" then return table.setmetatableindex(t, function(tt, kk) tt[kk] = kk return kk end) else return table.setmetatableindex(t, k) end end -- }}} -- Define some dummy functions. -- Todo: replace or exterminate. local function datastate() return {} end ------------------------------------------------------------------------------- local next, tonumber = next, tonumber local isfile, modificationtime = lfs.isfile, lfs.modification local P, Cs, lpegmatch = lpeg.P, lpeg.Cs, lpeg.match local sub, gsub, find, lower, upper, formatters = string.sub, string.gsub, string.find, string.lower, string.upper, string.formatters local sort, fastcopy, serialize, sortedhash = table.sort, table.fastcopy, table.serialize, table.sortedhash local allocate = utilities.storage.allocate local removesuffix = file.removesuffix local splitbase = file.splitbase local splitname = file.splitname local basename = file.basename 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 resolveprefix = resolvers.resolve local report_names = logs.reporter("fonts","names") -- This module implements a name to filename resolver. Names are resolved using a -- table that has keys filtered from the font related files. fonts = fonts or { } -- also used elsewhere fonts.names = fonts.names or allocate { } local 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.131 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 ------------------------------------------------------------------------------- -- -- 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") -- / "semibold" + P("semi") -- / "semibold" + P("light") + P("medium") + P("heavy") + P("ultra") + P("black") --+ P("bol") / "bold" -- blocks + P("bol") + P("regular") / "normal" ) -- local weights = { -- [100] = "thin", -- [200] = "extralight", -- [300] = "light", -- [400] = "normal", -- [500] = "medium", -- [600] = "semibold", -- demi demibold -- [700] = "bold", -- [800] = "extrabold", -- [900] = "black", -- } local styles = Cs ( P("reverseoblique") / "reverseitalic" + P("regular") / "normal" + P("italic") + P("oblique") / "italic" -- cfr: "slanted"?? + P("slanted") + P("slant") / "slanted" + P("roman") / "normal" + P("ital") / "italic" -- might be tricky + P("ita") / "italic" -- might be tricky --+ P("obli") / "oblique" ) local widths = Cs( P("condensed") + P("thin") + P("expanded") + P("cond") / "condensed" + P("normal") + P("book") / "normal" -- cfr: ??? ) local variants = Cs( -- fax casual P("smallcaps") + P("oldstyle") + P("caps") / "smallcaps" ) names.knownweights = { -- cfr: book?? "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 not weight or not style or not width or not variant then weight, style, width, variant = weight or "normal", style or "normal", width or "normal", variant or "normal" 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 -- It would make sense to implement the filters in the related modules, but to keep -- the overview, we define them here. filters.afm = fonts.handlers.afm.readers.getinfo filters.otf = fonts.handlers.otf.readers.getinfo filters.ttf = filters.otf filters.ttc = filters.otf -- 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. 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 names.extrafontsvariable = "EXTRAFONTS" -- the official way, in minimals etc names.runtimefontsvariable = "RUNTIMEFONTS" -- the official way, in minimals etc filters.paths = { } filters.names = { } function names.getpaths() -- {{{ local hash, result, r = { }, { }, 0 local function collect(t,where) for i=1,#t do local v = t[i] v = gsub(v,"/+$","") -- not needed any more local key = lower(v) report_names("variable %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 local path = names.extrafontsvariable 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 isfile(name) then name = "" -- force quit end end if name ~= "" and isfile(name) then 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 isfile(incname) then return io.loaddata(incname) end end) -- end of untested mess local fontdirs = xml.collect_texts(xmldata,"dir",true) collect(fontdirs,"fontconfig file") end end end sort(result) 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) if fullname then local path, name, suffix = splitname(fullname) if name then 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 end return "badfontname" end local sorter = function(a,b) return a > b -- longest first end names.cleanname = cleanname names.cleanfilename = cleanfilename local function check_name(data,result,filename,modification,suffix,subfont) -- shortcuts local specifications = data.specifications -- fetch local fullname = result.fullname local fontname = result.fontname local family = result.family local subfamily = result.subfamily local familyname = result.familyname local subfamilyname = result.subfamilyname local weight = result.weight local width = result.width 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 fullname = fullname and cleanname(fullname) fontname = fontname and cleanname(fontname) family = family and cleanname(family) subfamily = subfamily and cleanname(subfamily) familyname = familyname and cleanname(familyname) subfamilyname = subfamilyname and cleanname(subfamilyname) weight = weight and cleanname(weight) width = width and cleanname(width) 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 = width or a_width local variant = a_variant local style = subfamilyname or subfamily -- can re really trust subfamilyname? if style then style = gsub(style,"[^%a]","") elseif 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.minsize or 0 local maxsize = result.maxsize or 0 local angle = result.italicangle or 0 local pfmwidth = result.pfmwidth or 0 local pfmweight = result.pfmweight or 0 -- local instancenames = result.instancenames -- specifications[#specifications+1] = { filename = filename, -- unresolved cleanfilename = cleanfilename, -- subfontindex = subfont, format = lower(suffix), subfont = subfont, rawname = rawname, fullname = fullname, fontname = fontname, family = family, subfamily = subfamily, familyname = familyname, subfamilyname = subfamilyname, 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, instancenames = instancenames 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 statistics = data.statistics statistics.used_weights = weights statistics.used_styles = styles statistics.used_widths = widths statistics.used_variants = variants end end local function collectstatistics(runtime) local data = names.data local specifications = data.specifications local statistics = data.statistics 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 -- statistics.weights = weights statistics.styles = styles statistics.widths = widths statistics.variants = variants statistics.angles = angles statistics.pfmweights = pfmweights statistics.pfmwidths = pfmwidths statistics.fonts = #specifications -- setmetatableindex(pfmweights,nil) setmetatableindex(pfmwidths, nil) -- report_names("") report_names("statistics: ") 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("") report_names("registered fonts : %i", statistics.fonts) report_names("read files : %i", statistics.readfiles) report_names("skipped files : %i", statistics.skippedfiles) report_names("duplicate files : %i", statistics.duplicatefiles) if runtime then report_names("total scan time : %0.3f seconds",runtime) end 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 the compatible and cffnames so that they have less preference) local conflicts = setmetatableindex("table") for index=1,#specifications do local specification = specifications[index] local format = specification.format local fullname = specification.fullname local fontname = specification.fontname local familyname = specification.familyname or specification.family local subfamilyname = specification.subfamilyname local subfamily = specification.subfamily local weight = specification.weight local mapping = mappings[format] local fallback = fallbacks[format] local instancenames = specification.instancenames if fullname and not mapping[fullname] then mapping[fullname] = index nofmappings = nofmappings + 1 end if fontname and not mapping[fontname] then mapping[fontname] = index nofmappings = nofmappings + 1 end if instancenames then for i=1,#instancenames do local instance = fullname .. instancenames[i] mapping[instance] = index nofmappings = nofmappings + 1 end end if familyname then if weight and weight ~= sub(familyname,#familyname-#weight+1,#familyname) then local madename = familyname .. weight if not mapping[madename] and not fallback[madename] then fallback[madename] = index noffallbacks = noffallbacks + 1 end end if subfamily and subfamily ~= sub(familyname,#familyname-#subfamily+1,#familyname) then local extraname = familyname .. subfamily if not mapping[extraname] and not fallback[extraname] then fallback[extraname] = index noffallbacks = noffallbacks + 1 end end if subfamilyname and subfamilyname ~= sub(familyname,#familyname-#subfamilyname+1,#familyname) then local extraname = familyname .. subfamilyname if not mapping[extraname] and not fallback[extraname] then fallback[extraname] = index noffallbacks = noffallbacks + 1 end end -- dangerous ... first match takes slot if not mapping[familyname] and not fallback[familyname] then fallback[familyname] = index noffallbacks = noffallbacks + 1 end local conflict = conflicts[format] conflict[familyname] = (conflict[familyname] or 0) + 1 end end for format, conflict in next, conflicts do local fallback = fallbacks[format] for familyname, n in next, conflict do if n > 1 then fallback[familyname] = nil noffallbacks = noffallbacks - n end 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 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 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 local sorted_families = table.keys(data.families) data.sorted_families = sorted_families sort(sorted_families,sorter) end local function unpackreferences() local data = names.data local specifications = data.specifications if specifications then 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 sortedhash(mappings) 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 sortedhash(fallbacks) do for k, v in sortedhash(f) do f[k] = specifications[v] end end end end end local function analyzefiles(olddata) 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 nofskipped = nofskipped + 1 elseif done[name] then if lower(completename) ~= lower(done[name]) then -- already done (avoid otf afm clash) nofduplicates = nofduplicates + 1 nofskipped = nofskipped + 1 end elseif not exists(completename) then -- weird error nofskipped = nofskipped + 1 elseif not is_qualified_path(completename) and findfile(completename,suffix) == "" then -- not locatable by backend anyway nofskipped = nofskipped + 1 else if #skip_paths > 0 then for i=1,#skip_paths do if find(pathpart,skip_paths[i]) then 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 nofskipped = nofskipped + 1 return end end end -- needs checking with ttc / ttx : date not updated ? local result = nil local modification = modificationtime(completename) 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 end 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 check_name(data,result[r],storedname,modification,suffix,r) -- subfonts start at zero end else check_name(data,result,storedname,modification,suffix) end end end done[name] = completename end 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 end -- problem .. this will not take care of duplicates local function withtree(suffix) 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 ) end -- This include system fonts, I think, because dowithfilesintree() gets the -- list from luaotfload's db and then augments that db. -- So I think the comment on the next line doesn't apply here. traverse("tree",withtree) -- TEXTREE only 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 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 = datastate(), } end function names.identify(force) -- Print a message while generating our font name database so that users -- don't get confused by the long pause. texio.write_nl("Generating font name database...") local starttime = os.gettimeofday() -- use elapser resetdata() analyzefiles(not force and names.readdata(names.basename)) rejectclashes() collectfamilies() cleanupkeywords() collecthashes() checkduplicates() addfilenames() -- sorthashes() -- will be resorted when saved collectstatistics(os.gettimeofday()-starttime) texio.write(" done.\n") 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(datastate()) local f_status = serialize(data.datastate) if c_status == f_status then return else report_names("font database has mismatching configuration and file hashes") end else report_names("font database is regenerated") end names.loaded = false reloaded = true names.load(true) end end -- 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). local function fuzzy(mapping,sorted,name,sub) -- no need for reverse sorted here local condensed = gsub(name,"[^%a%d]","") local pattern = condensed .. "$" local matches = false for k=1,#sorted do local v = sorted[k] if v == condensed then return mapping[v], v elseif find(v,pattern) then return mapping[v], v elseif find(v,condensed) then if matches then matches[#matches+1] = v else matches = { v } end end end if matches then if #matches > 1 then sort(matches,function(a,b) return #a < #b end) end matches = matches[1] return mapping[matches], matches end end -- we could cache a lookup .. maybe some day ... (only when auto loaded!) local function checkinstance(found,askedname) local instancenames = found.instancenames if instancenames then local fullname = found.fullname for i=1,#instancenames do local instancename = instancenames[i] if fullname .. instancename == askedname then local f = fastcopy(found) f.instances = nil f.instance = instancename return f end end end return found end 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 return checkinstance(found,name) 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 return checkinstance(found,name) end end for i=1,#list do local l = list[i] local found = fallbacks[l][name] if found then return checkinstance(found,name) 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 return checkinstance(found,name) end 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, found.instance end end local specifications = allocate() return { cache = names.cache, cleanfilename = names.cleanfilename, get_data = function() return names.data end, -- data = names.data, list = names.list, load = names.load, resolve = names.resolve, writable = names.cache.writable, -- ? -- -- filters = names.filters, } -- vim: et:foldmethod=marker:tw=4:sw=2: