font-syn.lua /size: 77 Kb    last modification: 2021-10-28 13:50
1if not modules then modules = { } end modules ['font-syn'] = {
2    version   = 1.001,
3    comment   = "companion to font-ini.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- todo: subs in lookups requests
10-- todo: see if the (experimental) lua reader (on my machine) be used (it's a bit slower so maybe wait till lua 5.3)
11
12-- identifying ttf/otf/ttc/afm : 2200 fonts:
13--
14-- old ff  loader: 140 sec
15-- new lua loader:   5 sec
16
17-- maybe find(...,strictname,1,true)
18
19local next, tonumber, type, tostring = next, tonumber, type, tostring
20local sub, gsub, match, find, lower, upper = string.sub, string.gsub, string.match, string.find, string.lower, string.upper
21local concat, sort, fastcopy, tohash = table.concat, table.sort, table.fastcopy, table.tohash
22local serialize, sortedhash = table.serialize, table.sortedhash
23local lpegmatch = lpeg.match
24local unpack = unpack or table.unpack
25local formatters, topattern = string.formatters, string.topattern
26local round = math.round
27local P, R, S, C, Cc, Ct, Cs = lpeg.P, lpeg.R, lpeg.S, lpeg.C, lpeg.Cc, lpeg.Ct, lpeg.Cs
28local lpegmatch, lpegpatterns = lpeg.match, lpeg.patterns
29local isfile, modificationtime = lfs.isfile, lfs.modification
30
31local allocate             = utilities.storage.allocate
32local sparse               = utilities.storage.sparse
33local setmetatableindex    = table.setmetatableindex
34
35local removesuffix         = file.removesuffix
36local splitbase            = file.splitbase
37local splitname            = file.splitname
38local basename             = file.basename
39local nameonly             = file.nameonly
40local pathpart             = file.pathpart
41local suffixonly           = file.suffix
42local filejoin             = file.join
43local is_qualified_path    = file.is_qualified_path
44local exists               = io.exists
45
46local findfile             = resolvers.findfile
47local cleanpath            = resolvers.cleanpath
48local resolveprefix        = resolvers.resolve
49
50local settings_to_hash     = utilities.parsers.settings_to_hash_tolerant
51
52local trace_names          = false  trackers.register("fonts.names",          function(v) trace_names          = v end)
53local trace_warnings       = false  trackers.register("fonts.warnings",       function(v) trace_warnings       = v end)
54local trace_specifications = false  trackers.register("fonts.specifications", function(v) trace_specifications = v end)
55local trace_rejections     = false  trackers.register("fonts.rejections",     function(v) trace_rejections     = v end)
56
57local report_names         = logs.reporter("fonts","names")
58
59--[[ldx--
60<p>This module implements a name to filename resolver. Names are resolved
61using a table that has keys filtered from the font related files.</p>
62--ldx]]--
63
64fonts                      = fonts or { } -- also used elsewhere
65
66local names                = fonts.names or allocate { }
67fonts.names                = names
68
69local filters              = names.filters or { }
70names.filters              = filters
71
72local treatments           = fonts.treatments or { }
73fonts.treatments           = treatments
74
75names.data                 = names.data or allocate { }
76
77names.version              = 1.131
78names.basename             = "names"
79names.saved                = false
80names.loaded               = false
81names.be_clever            = true
82names.enabled              = true
83names.cache                = containers.define("fonts","data",names.version,true)
84
85local usesystemfonts       = true
86local autoreload           = true
87
88directives.register("fonts.autoreload",     function(v) autoreload     = toboolean(v) end)
89directives.register("fonts.usesystemfonts", function(v) usesystemfonts = toboolean(v) end)
90
91--[[ldx--
92<p>A few helpers.</p>
93--ldx]]--
94
95-- -- what to do with these -- --
96--
97-- thin -> thin
98--
99-- regu -> regular  -> normal
100-- norm -> normal   -> normal
101-- stan -> standard -> normal
102-- medi -> medium
103-- ultr -> ultra
104-- ligh -> light
105-- heav -> heavy
106-- blac -> black
107-- thin
108-- book
109-- verylight
110--
111-- buch        -> book
112-- buchschrift -> book
113-- halb        -> demi
114-- halbfett    -> demi
115-- mitt        -> medium
116-- mittel      -> medium
117-- fett        -> bold
118-- mage        -> light
119-- mager       -> light
120-- nord        -> normal
121-- gras        -> normal
122
123local weights = Cs ( -- not extra
124    P("demibold")
125  + P("semibold")
126  + P("mediumbold")
127  + P("ultrabold")
128  + P("extrabold")
129  + P("ultralight")
130  + P("extralight")
131  + P("bold")
132  + P("demi")  -- / "semibold"
133  + P("semi")  -- / "semibold"
134  + P("light")
135  + P("medium")
136  + P("heavy")
137  + P("ultra")
138  + P("black")
139--+ P("bol")      / "bold" -- blocks
140  + P("bol")
141  + P("regular")  / "normal"
142)
143
144-- local weights = {
145--     [100] = "thin",
146--     [200] = "extralight",
147--     [300] = "light",
148--     [400] = "normal",
149--     [500] = "medium",
150--     [600] = "semibold", -- demi demibold
151--     [700] = "bold",
152--     [800] = "extrabold",
153--     [900] = "black",
154-- }
155
156local normalized_weights = sparse {
157    regular = "normal",
158}
159
160local styles = Cs (
161    P("reverseoblique") / "reverseitalic"
162  + P("regular")        / "normal"
163  + P("italic")
164  + P("oblique")        / "italic"
165  + P("slanted")
166  + P("roman")          / "normal"
167  + P("ital")           / "italic" -- might be tricky
168  + P("ita")            / "italic" -- might be tricky
169--+ P("obli")           / "oblique"
170)
171
172local normalized_styles = sparse {
173    reverseoblique = "reverseitalic",
174    regular        = "normal",
175    oblique        = "italic",
176}
177
178local widths = Cs(
179    P("condensed")
180  + P("thin")
181  + P("expanded")
182  + P("cond")     / "condensed"
183--+ P("expa")     / "expanded"
184  + P("normal")
185  + P("book")     / "normal"
186)
187
188local normalized_widths = sparse()
189
190local variants = Cs( -- fax casual
191    P("smallcaps")
192  + P("oldstyle")
193  + P("caps")      / "smallcaps"
194)
195
196local normalized_variants = sparse()
197
198names.knownweights = {
199    "black",
200    "bold",
201    "demi",
202    "demibold",
203    "extrabold",
204    "heavy",
205    "light",
206    "medium",
207    "mediumbold",
208    "normal",
209    "regular",
210    "semi",
211    "semibold",
212    "ultra",
213    "ultrabold",
214    "ultralight",
215}
216
217names.knownstyles = {
218    "italic",
219    "normal",
220    "oblique",
221    "regular",
222    "reverseitalic",
223    "reverseoblique",
224    "roman",
225    "slanted",
226}
227
228names.knownwidths = {
229    "book",
230    "condensed",
231    "expanded",
232    "normal",
233    "thin",
234}
235
236names.knownvariants = {
237    "normal",
238    "oldstyle",
239    "smallcaps",
240}
241
242local remappedweights = {
243    [""]    = "normal",
244    ["bol"] = "bold",
245}
246
247local remappedstyles = {
248    [""]    = "normal",
249}
250
251local remappedwidths = {
252    [""]    = "normal",
253}
254
255local remappedvariants = {
256    [""]    = "normal",
257}
258
259names.remappedweights  = remappedweights   setmetatableindex(remappedweights ,"self")
260names.remappedstyles   = remappedstyles    setmetatableindex(remappedstyles  ,"self")
261names.remappedwidths   = remappedwidths    setmetatableindex(remappedwidths  ,"self")
262names.remappedvariants = remappedvariants  setmetatableindex(remappedvariants,"self")
263
264local any = P(1)
265
266local analyzed_table
267
268local analyzer = Cs (
269    (
270        weights  / function(s) analyzed_table[1] = s return "" end
271      + styles   / function(s) analyzed_table[2] = s return "" end
272      + widths   / function(s) analyzed_table[3] = s return "" end
273      + variants / function(s) analyzed_table[4] = s return "" end
274      + any
275    )^0
276)
277
278local splitter = lpeg.splitat("-")
279
280function names.splitspec(askedname)
281    local name, weight, style, width, variant = lpegmatch(splitter,askedname)
282    weight  = weight  and lpegmatch(weights, weight)  or weight
283    style   = style   and lpegmatch(styles,  style)   or style
284    width   = width   and lpegmatch(widths,  width)   or width
285    variant = variant and lpegmatch(variants,variant) or variant
286    if trace_names then
287        report_names("requested name %a split in name %a, weight %a, style %a, width %a and variant %a",
288            askedname,name,weight,style,width,variant)
289    end
290    if not weight or not weight or not width or not variant then
291        weight, style, width, variant = weight or "normal", style or "normal", width or "normal", variant or "normal"
292        if trace_names then
293            report_names("request %a normalized to '%s-%s-%s-%s-%s'",
294                askedname,name,weight,style,width,variant)
295        end
296    end
297    return name or askedname, weight, style, width, variant
298end
299
300local function analyzespec(somename)
301    if somename then
302        analyzed_table = { }
303        local name = lpegmatch(analyzer,somename)
304        return name, analyzed_table[1], analyzed_table[2], analyzed_table[3], analyzed_table[4]
305    end
306end
307
308--[[ldx--
309<p>It would make sense to implement the filters in the related modules,
310but to keep the overview, we define them here.</p>
311--ldx]]--
312
313filters.afm = fonts.handlers.afm.readers.getinfo
314filters.otf = fonts.handlers.otf.readers.getinfo
315filters.ttf = filters.otf
316filters.ttc = filters.otf
317-------.ttx = filters.otf
318
319-- local function normalize(t) -- only for afm parsing
320--     local boundingbox = t.boundingbox or t.fontbbox
321--     if boundingbox then
322--         for i=1,#boundingbox do
323--             boundingbox[i] = tonumber(boundingbox[i])
324--         end
325--     else
326--         boundingbox = { 0, 0, 0, 0 }
327--     end
328--     return {
329--         copyright     = t.copyright,
330--         fontname      = t.fontname,
331--         fullname      = t.fullname,
332--         familyname    = t.familyname,
333--         weight        = t.weight,
334--         widtht        = t.width,
335--         italicangle   = tonumber(t.italicangle) or 0,
336--         monospaced    = t.monospaced or toboolean(t.isfixedpitch) or false,
337--         boundingbox   = boundingbox,
338--         version       = t.version, -- not used
339--         capheight     = tonumber(t.capheight),
340--         xheight       = tonumber(t.xheight),
341--         ascender      = tonumber(t.ascender),
342--         descender     = tonumber(t.descender),
343--     }
344-- end
345--
346-- function filters.afm(name)
347--     -- we could parse the afm file as well, and then report an error but
348--     -- it's not worth the trouble
349--     local pfbname = findfile(removesuffix(name)..".pfb","pfb") or ""
350--     if pfbname == "" then
351--         pfbname = findfile(nameonly(name)..".pfb","pfb") or ""
352--     end
353--     if pfbname ~= "" then
354--         local f = io.open(name)
355--         if f then
356--             local hash = { }
357--             local okay = false
358--             for line in f:lines() do -- slow but only a few lines at the beginning
359--                 if find(line,"StartCharMetrics",1,true) then
360--                     break
361--                 else
362--                     local key, value = match(line,"^(.+)%s+(.+)%s*$")
363--                     if key and #key > 0 then
364--                         hash[lower(key)] = value
365--                     end
366--                 end
367--             end
368--             f:close()
369--             return normalize(hash)
370--         end
371--     end
372--     return nil, "no matching pfb file"
373-- end
374
375-- local p_spaces  = lpegpatterns.whitespace
376-- local p_number  = (R("09")+S(".-+"))^1 / tonumber
377-- local p_boolean = P("false") * Cc(false)
378--                 + P("false") * Cc(false)
379-- local p_string  = P("(") * C((lpegpatterns.nestedparents + 1 - P(")"))^1) * P(")")
380-- local p_array   = P("[") * Ct((p_number + p_boolean + p_string + p_spaces^1)^1) * P("]")
381--                 + P("{") * Ct((p_number + p_boolean + p_string + p_spaces^1)^1) * P("}")
382--
383-- local p_key     = P("/") * C(R("AZ","az")^1)
384-- local p_value   = p_string
385--                 + p_number
386--                 + p_boolean
387--                 + p_array
388--
389-- local p_entry   = p_key * p_spaces^0 * p_value
390--
391-- function filters.pfb(name)
392--     local f = io.open(name)
393--     if f then
394--         local hash = { }
395--         local okay = false
396--         for line in f:lines() do -- slow but only a few lines at the beginning
397--             if find(line,"dict begin",1,true) then
398--                 okay = true
399--             elseif not okay then
400--                 -- go on
401--             elseif find(line,"currentdict end",1,true) then
402--                 break
403--             else
404--                 local key, value = lpegmatch(p_entry,line)
405--                 if key and value then
406--                     hash[lower(key)] = value
407--                 end
408--             end
409--         end
410--         f:close()
411--         return normalize(hash)
412--     end
413-- end
414
415--[[ldx--
416<p>The scanner loops over the filters using the information stored in
417the file databases. Watch how we check not only for the names, but also
418for combination with the weight of a font.</p>
419--ldx]]--
420
421filters.list = {
422    "otf", "ttf", "ttc", "afm", -- no longer dfont support (for now)
423}
424
425-- to be considered: loop over paths per list entry (so first all otf ttf etc)
426
427names.fontconfigfile       = "fonts.conf"   -- a bit weird format, bonus feature
428names.osfontdirvariable    = "OSFONTDIR"    -- the official way, in minimals etc
429names.extrafontsvariable   = "EXTRAFONTS"   -- the official way, in minimals etc
430names.runtimefontsvariable = "RUNTIMEFONTS" -- the official way, in minimals etc
431
432filters.paths = { }
433filters.names = { }
434
435function names.getpaths(trace)
436    local hash, result, r = { }, { }, 0
437    local function collect(t,where)
438        for i=1,#t do
439            local v = cleanpath(t[i])
440            v = gsub(v,"/+$","") -- not needed any more
441            local key = lower(v)
442            report_names("variable %a specifies path %a",where,v)
443            if not hash[key] then
444                r = r + 1
445                result[r] = v
446                hash[key] = true
447            end
448        end
449    end
450    local path = names.osfontdirvariable or ""
451    if path ~= "" then
452        collect(resolvers.expandedpathlist(path),path)
453    end
454    local path = names.extrafontsvariable or ""
455    if path ~= "" then
456        collect(resolvers.expandedpathlist(path),path)
457    end
458    if xml then
459        local confname = resolvers.expansion("FONTCONFIG_FILE") or ""
460        if confname == "" then
461            confname = names.fontconfigfile or ""
462        end
463        if confname ~= "" then
464            -- first look in the tex tree
465            local name = findfile(confname,"fontconfig files") or ""
466            if name == "" then
467                -- after all, fontconfig is a unix thing
468                name = filejoin("/etc",confname)
469                if not isfile(name) then
470                    name = "" -- force quit
471                end
472            end
473            if name ~= "" and isfile(name) then
474                if trace_names then
475                    report_names("%s fontconfig file %a","loading",name)
476                end
477                local xmldata = xml.load(name)
478                -- begin of untested mess
479                xml.include(xmldata,"include","",true,function(incname)
480                    if not is_qualified_path(incname) then
481                        local path = pathpart(name) -- main name
482                        if path ~= "" then
483                            incname = filejoin(path,incname)
484                        end
485                    end
486                    if isfile(incname) then
487                        if trace_names then
488                            report_names("%s fontconfig file %a","merging included",incname)
489                        end
490                        return io.loaddata(incname)
491                    elseif trace_names then
492                        report_names("%s fontconfig file: %a","ignoring included",incname)
493                    end
494                end)
495                -- end of untested mess
496                local fontdirs = xml.collect_texts(xmldata,"dir",true)
497                if trace_names then
498                    report_names("%s dirs found in fontconfig",#fontdirs)
499                end
500                collect(fontdirs,"fontconfig file")
501            end
502        end
503    end
504    sort(result)
505    function names.getpaths()
506        return result
507    end
508    return result
509end
510
511local function cleanname(name)
512    return (gsub(lower(name),"[^%a%d]",""))
513end
514
515local function cleanfilename(fullname,defaultsuffix)
516    if fullname then
517        local path, name, suffix = splitname(fullname)
518        if name then
519            name = gsub(lower(name),"[^%a%d]","")
520            if suffix and suffix ~= "" then
521                return name .. ".".. suffix
522            elseif defaultsuffix and defaultsuffix ~= "" then
523                return name .. ".".. defaultsuffix
524            else
525                return name
526            end
527        end
528    end
529    return "badfontname"
530end
531
532local sorter = function(a,b)
533    return a > b -- longest first
534end
535
536-- local sorter = nil
537
538names.cleanname     = cleanname
539names.cleanfilename = cleanfilename
540
541-- local function check_names(result)
542--     local names = result.names
543--     if names then
544--         for i=1,#names do
545--             local name = names[i]
546--             if name.lang == "English (US)" then
547--                 return name.names
548--             end
549--         end
550--     end
551--     return result
552-- end
553
554
555local function check_name(data,result,filename,modification,suffix,subfont)
556    -- shortcuts
557    local specifications = data.specifications
558    -- fetch
559    local fullname       = result.fullname
560    local fontname       = result.fontname
561    local family         = result.family
562    local subfamily      = result.subfamily
563    local familyname     = result.familyname
564    local subfamilyname  = result.subfamilyname
565 -- local compatiblename = result.compatiblename
566 -- local cfffullname    = result.cfffullname
567    local weight         = result.weight
568    local width          = result.width
569    local italicangle    = tonumber(result.italicangle)
570    local subfont        = subfont
571    local rawname        = fullname or fontname or familyname
572    local filebase       = removesuffix(basename(filename))
573    local cleanfilename  = cleanname(filebase) -- for WS
574    -- normalize
575    fullname       = fullname       and cleanname(fullname)
576    fontname       = fontname       and cleanname(fontname)
577    family         = family         and cleanname(family)
578    subfamily      = subfamily      and cleanname(subfamily)
579    familyname     = familyname     and cleanname(familyname)
580    subfamilyname  = subfamilyname  and cleanname(subfamilyname)
581 -- compatiblename = compatiblename and cleanname(compatiblename)
582 -- cfffullname    = cfffullname    and cleanname(cfffullname)
583    weight         = weight         and cleanname(weight)
584    width          = width          and cleanname(width)
585    italicangle    = italicangle == 0 and nil
586    -- analyze
587    local a_name, a_weight, a_style, a_width, a_variant = analyzespec(fullname or fontname or familyname)
588    -- check
589    local width   = width or a_width
590    local variant = a_variant
591    local style   = subfamilyname or subfamily -- can re really trust subfamilyname?
592    if style then
593        style = gsub(style,"[^%a]","")
594    elseif italicangle then
595        style = "italic"
596    end
597    if not variant or variant == "" then
598        variant = "normal"
599    end
600    if not weight or weight == "" then
601        weight = a_weight
602    end
603    if not style or style == ""  then
604        style = a_style
605    end
606    if not familyname then
607        familyname = a_name
608    end
609    fontname   = fontname   or fullname or familyname or filebase -- maybe cleanfilename
610    fullname   = fullname   or fontname
611    familyname = familyname or fontname
612    -- we do these sparse -- todo: check table type or change names in ff loader
613    local units      = result.units       or 1000 -- can be zero too
614    local designsize = result.designsize  or 0
615    local minsize    = result.minsize     or 0
616    local maxsize    = result.maxsize     or 0
617    local angle      = result.italicangle or 0
618    local pfmwidth   = result.pfmwidth    or 0
619    local pfmweight  = result.pfmweight   or 0
620    --
621    local instancenames = result.instancenames
622    --
623    specifications[#specifications+1] = {
624        filename       = filename, -- unresolved
625        cleanfilename  = cleanfilename,
626     -- subfontindex   = subfont,
627        format         = lower(suffix),
628        subfont        = subfont,
629        rawname        = rawname,
630        fullname       = fullname,
631        fontname       = fontname,
632        family         = family,
633        subfamily      = subfamily,
634        familyname     = familyname,
635        subfamilyname  = subfamilyname,
636     -- compatiblename = compatiblename,  -- nor used / needed
637     -- cfffullname    = cfffullname,
638        weight         = weight,
639        style          = style,
640        width          = width,
641        variant        = variant,
642        units          = units        ~= 1000 and units        or nil,
643        pfmwidth       = pfmwidth     ~=    0 and pfmwidth     or nil,
644        pfmweight      = pfmweight    ~=    0 and pfmweight    or nil,
645        angle          = angle        ~=    0 and angle        or nil,
646        minsize        = minsize      ~=    0 and minsize      or nil,
647        maxsize        = maxsize      ~=    0 and maxsize      or nil,
648        designsize     = designsize   ~=    0 and designsize   or nil,
649        modification   = modification ~=    0 and modification or nil,
650        instancenames  = instancenames or nil,
651    }
652end
653
654local function cleanupkeywords()
655    local data           = names.data
656    local specifications = names.data.specifications
657    if specifications then
658        local weights  = { }
659        local styles   = { }
660        local widths   = { }
661        local variants = { }
662        for i=1,#specifications do
663            local s = specifications[i]
664            -- fix (sofar styles are taken from the name, and widths from the specification)
665            local _, b_weight, b_style, b_width, b_variant = analyzespec(s.weight)
666            local _, c_weight, c_style, c_width, c_variant = analyzespec(s.style)
667            local _, d_weight, d_style, d_width, d_variant = analyzespec(s.width)
668            local _, e_weight, e_style, e_width, e_variant = analyzespec(s.variant)
669            local _, f_weight, f_style, f_width, f_variant = analyzespec(s.fullname or "")
670            local weight  = b_weight  or c_weight  or d_weight  or e_weight  or f_weight  or "normal"
671            local style   = b_style   or c_style   or d_style   or e_style   or f_style   or "normal"
672            local width   = b_width   or c_width   or d_width   or e_width   or f_width   or "normal"
673            local variant = b_variant or c_variant or d_variant or e_variant or f_variant or "normal"
674            weight  = remappedweights [weight  or ""]
675            style   = remappedstyles  [style   or ""]
676            width   = remappedwidths  [width   or ""]
677            variant = remappedvariants[variant or ""]
678            weights [weight ] = (weights [weight ] or 0) + 1
679            styles  [style  ] = (styles  [style  ] or 0) + 1
680            widths  [width  ] = (widths  [width  ] or 0) + 1
681            variants[variant] = (variants[variant] or 0) + 1
682            if weight ~= s.weight then
683                s.fontweight = s.weight
684            end
685            s.weight, s.style, s.width, s.variant = weight, style, width, variant
686        end
687        local statistics = data.statistics
688        statistics.used_weights  = weights
689        statistics.used_styles   = styles
690        statistics.used_widths   = widths
691        statistics.used_variants = variants
692    end
693end
694
695local function collectstatistics(runtime)
696    local data           = names.data
697    local specifications = data.specifications
698    local statistics     = data.statistics
699    if specifications then
700        local f_w = formatters["%i"]
701        local f_a = formatters["%0.2f"]
702        -- normal stuff
703        local weights    = { }
704        local styles     = { }
705        local widths     = { }
706        local variants   = { }
707        -- weird stuff
708        local angles     = { }
709        -- extra stuff
710        local pfmweights = { } setmetatableindex(pfmweights,"table")
711        local pfmwidths  = { } setmetatableindex(pfmwidths, "table")
712        -- main loop
713        for i=1,#specifications do
714            local s = specifications[i]
715            -- normal stuff
716            local weight  = s.weight
717            local style   = s.style
718            local width   = s.width
719            local variant = s.variant
720            if weight  then weights [weight ] = (weights [weight ] or 0) + 1 end
721            if style   then styles  [style  ] = (styles  [style  ] or 0) + 1 end
722            if width   then widths  [width  ] = (widths  [width  ] or 0) + 1 end
723            if variant then variants[variant] = (variants[variant] or 0) + 1 end
724            -- weird stuff
725            local angle   = f_a(tonumber(s.angle) or 0)
726            angles[angle] = (angles[angles] or 0) + 1
727            -- extra stuff
728            local pfmweight     = f_w(s.pfmweight or 0)
729            local pfmwidth      = f_w(s.pfmwidth  or 0)
730            local tweights      = pfmweights[pfmweight]
731            local twidths       = pfmwidths [pfmwidth]
732            tweights[pfmweight] = (tweights[pfmweight] or 0) + 1
733            twidths[pfmwidth]   = (twidths [pfmwidth]  or 0) + 1
734        end
735        --
736        statistics.weights    = weights
737        statistics.styles     = styles
738        statistics.widths     = widths
739        statistics.variants   = variants
740        statistics.angles     = angles
741        statistics.pfmweights = pfmweights
742        statistics.pfmwidths  = pfmwidths
743        statistics.fonts      = #specifications
744        --
745        setmetatableindex(pfmweights,nil)
746        setmetatableindex(pfmwidths, nil)
747        --
748        report_names("")
749        report_names("statistics: ")
750        report_names("")
751        report_names("weights")
752        report_names("")
753        report_names(formatters["  %T"](weights))
754        report_names("")
755        report_names("styles")
756        report_names("")
757        report_names(formatters["  %T"](styles))
758        report_names("")
759        report_names("widths")
760        report_names("")
761        report_names(formatters["  %T"](widths))
762        report_names("")
763        report_names("variants")
764        report_names("")
765        report_names(formatters["  %T"](variants))
766        report_names("")
767        report_names("angles")
768        report_names("")
769        report_names(formatters["  %T"](angles))
770        report_names("")
771        report_names("pfmweights")
772        report_names("")
773        for k, v in sortedhash(pfmweights) do
774            report_names(formatters["  %-10s: %T"](k,v))
775        end
776        report_names("")
777        report_names("pfmwidths")
778        report_names("")
779        for k, v in sortedhash(pfmwidths) do
780            report_names(formatters["  %-10s: %T"](k,v))
781        end
782        report_names("")
783        report_names("registered fonts : %i", statistics.fonts)
784        report_names("read files       : %i", statistics.readfiles)
785        report_names("skipped files    : %i", statistics.skippedfiles)
786        report_names("duplicate files  : %i", statistics.duplicatefiles)
787            if runtime then
788        report_names("total scan time  : %0.3f seconds",runtime)
789            end
790    end
791end
792
793local function collecthashes()
794    local data           = names.data
795    local mappings       = data.mappings
796    local fallbacks      = data.fallbacks
797    local specifications = data.specifications
798    local nofmappings    = 0
799    local noffallbacks   = 0
800    if specifications then
801        -- maybe multiple passes (for the compatible and cffnames so that they have less preference)
802        local conflicts = setmetatableindex("table")
803        for index=1,#specifications do
804            local specification  = specifications[index]
805            local format         = specification.format
806            local fullname       = specification.fullname
807            local fontname       = specification.fontname
808         -- local rawname        = specification.rawname
809         -- local compatiblename = specification.compatiblename
810         -- local cfffullname    = specification.cfffullname
811            local familyname     = specification.familyname or specification.family
812            local subfamilyname  = specification.subfamilyname
813            local subfamily      = specification.subfamily
814            local weight         = specification.weight
815            local mapping        = mappings[format]
816            local fallback       = fallbacks[format]
817            local instancenames  = specification.instancenames
818            if fullname and not mapping[fullname] then
819                mapping[fullname] = index
820                nofmappings       = nofmappings + 1
821            end
822            if fontname and not mapping[fontname] then
823                mapping[fontname] = index
824                nofmappings       = nofmappings + 1
825            end
826            if instancenames then
827                for i=1,#instancenames do
828                    local instance = fullname .. instancenames[i]
829                    mapping[instance] = index
830                    nofmappings       = nofmappings + 1
831                end
832            end
833         -- if compatiblename and not mapping[compatiblename] then
834         --     mapping[compatiblename] = index
835         --     nofmappings             = nofmappings + 1
836         -- end
837         -- if cfffullname and not mapping[cfffullname] then
838         --     mapping[cfffullname] = index
839         --     nofmappings          = nofmappings + 1
840         -- end
841            if familyname then
842                if weight and weight ~= sub(familyname,#familyname-#weight+1,#familyname) then
843                    local madename = familyname .. weight
844                    if not mapping[madename] and not fallback[madename] then
845                        fallback[madename] = index
846                        noffallbacks       = noffallbacks + 1
847                    end
848                end
849                if subfamily and subfamily ~= sub(familyname,#familyname-#subfamily+1,#familyname) then
850                    local extraname = familyname .. subfamily
851                    if not mapping[extraname] and not fallback[extraname] then
852                        fallback[extraname] = index
853                        noffallbacks        = noffallbacks + 1
854                    end
855                end
856                if subfamilyname and subfamilyname ~= sub(familyname,#familyname-#subfamilyname+1,#familyname) then
857                    local extraname = familyname .. subfamilyname
858                    if not mapping[extraname] and not fallback[extraname] then
859                        fallback[extraname] = index
860                        noffallbacks        = noffallbacks + 1
861                    end
862                end
863                -- dangerous ... first match takes slot
864                if not mapping[familyname] and not fallback[familyname] then
865                    fallback[familyname] = index
866                    noffallbacks         = noffallbacks + 1
867                end
868                local conflict = conflicts[format]
869                conflict[familyname] = (conflict[familyname] or 0) + 1
870            end
871        end
872        for format, conflict in next, conflicts do
873            local fallback = fallbacks[format]
874            for familyname, n in next, conflict do
875                if n > 1 then
876                    fallback[familyname] = nil
877                    noffallbacks = noffallbacks - n
878                end
879            end
880        end
881    end
882    return nofmappings, noffallbacks
883end
884
885local function collectfamilies()
886    local data           = names.data
887    local specifications = data.specifications
888    local families       = data.families
889    for index=1,#specifications do
890        local familyname = specifications[index].familyname
891        local family     = families[familyname]
892        if not family then
893            families[familyname] = { index }
894        else
895            family[#family+1] = index
896        end
897    end
898end
899
900local function checkduplicate(where) -- fails on "Romantik" but that's a border case anyway
901    local data           = names.data
902    local mapping        = data[where]
903    local specifications = data.specifications
904    local loaded         = { }
905    if specifications and mapping then
906     -- was: for _, m in sortedhash(mapping) do
907        local order = filters.list
908        for i=1,#order do
909            local m = mapping[order[i]]
910            for k, v in sortedhash(m) do
911                local s = specifications[v]
912                local hash = formatters["%s-%s-%s-%s-%s"](s.familyname,s.weight or "*",s.style or "*",s.width or "*",s.variant or "*")
913                local h = loaded[hash]
914                if h then
915                    local ok = true
916                    local fn = s.filename
917                    for i=1,#h do
918                        if h[i] == fn then
919                            ok = false
920                            break
921                        end
922                    end
923                    if ok then
924                        h[#h+1] = fn
925                    end
926                else
927                    loaded[hash] = { s.filename }
928                end
929            end
930        end
931    end
932    local n = 0
933    for k, v in sortedhash(loaded) do
934        local nv = #v
935        if nv > 1 then
936            if trace_warnings then
937                report_names("lookup %a clashes with %a",k,v)
938            end
939            n = n + nv
940        end
941    end
942    report_names("%a double lookups in %a",n,where)
943end
944
945local function checkduplicates()
946    checkduplicate("mappings")
947    checkduplicate("fallbacks")
948end
949
950local function sorthashes()
951    local data             = names.data
952    local list             = filters.list
953    local mappings         = data.mappings
954    local fallbacks        = data.fallbacks
955    local sorted_mappings  = { }
956    local sorted_fallbacks = { }
957    data.sorted_mappings   = sorted_mappings
958    data.sorted_fallbacks  = sorted_fallbacks
959    for i=1,#list do
960        local l = list[i]
961        sorted_mappings [l] = table.keys(mappings[l])
962        sorted_fallbacks[l] = table.keys(fallbacks[l])
963        sort(sorted_mappings [l],sorter)
964        sort(sorted_fallbacks[l],sorter)
965    end
966    local sorted_families = table.keys(data.families)
967    data.sorted_families  = sorted_families
968    sort(sorted_families,sorter)
969end
970
971local function unpackreferences()
972    local data           = names.data
973    local specifications = data.specifications
974    if specifications then
975        for k, v in sortedhash(data.families) do
976            for i=1,#v do
977                v[i] = specifications[v[i]]
978            end
979        end
980        local mappings = data.mappings
981        if mappings then
982            for _, m in sortedhash(mappings) do
983                for k, v in sortedhash(m) do
984                    m[k] = specifications[v]
985                end
986            end
987        end
988        local fallbacks = data.fallbacks
989        if fallbacks then
990            for _, f in sortedhash(fallbacks) do
991                for k, v in sortedhash(f) do
992                    f[k] = specifications[v]
993                end
994            end
995        end
996    end
997end
998
999local function analyzefiles(olddata)
1000
1001    if not trace_warnings then
1002        report_names("warnings are disabled (tracker 'fonts.warnings')")
1003    end
1004
1005    local data               = names.data
1006    local done               = { }
1007    local totalnofread       = 0
1008    local totalnofskipped    = 0
1009    local totalnofduplicates = 0
1010    local nofread            = 0
1011    local nofskipped         = 0
1012    local nofduplicates      = 0
1013    local skip_paths         = filters.paths
1014    local skip_names         = filters.names
1015    local specifications     = data.specifications
1016    local oldindices         = olddata and olddata.indices        or { }
1017    local oldspecifications  = olddata and olddata.specifications or { }
1018    local oldrejected        = olddata and olddata.rejected       or { }
1019    local treatmentdata      = treatments.data or { } -- when used outside context
1020    ----- walked             = setmetatableindex("number")
1021
1022    local function walk_tree(pathlist,suffix,identify)
1023        if pathlist then
1024            for i=1,#pathlist do
1025                local path = pathlist[i]
1026                path = cleanpath(path .. "/")
1027                path = gsub(path,"/+","/")
1028                local pattern = path .. "**." .. suffix -- ** forces recurse
1029                report_names("globbing path %a",pattern)
1030                local t = dir.glob(pattern)
1031                sort(t,sorter)
1032                for j=1,#t do
1033                    local completename = t[j]
1034                    identify(completename,basename(completename),suffix,completename)
1035                end
1036             -- walked[path] = walked[path] + #t
1037            end
1038        end
1039    end
1040
1041    local function identify(completename,name,suffix,storedname)
1042        local pathpart, basepart = splitbase(completename)
1043        nofread = nofread + 1
1044        local treatment = treatmentdata[completename] or treatmentdata[basepart]
1045        if treatment and treatment.ignored then
1046            if trace_names or trace_rejections then
1047                report_names("%s font %a is ignored, reason %a",suffix,completename,treatment.comment or "unknown")
1048            end
1049            nofskipped = nofskipped + 1
1050        elseif done[name] then
1051            if lower(completename) ~= lower(done[name]) then
1052                -- already done (avoid otf afm clash)
1053                if trace_names or trace_rejections then
1054                    report_names("%s font %a already done as %a",suffix,completename,done[name])
1055                end
1056                nofduplicates = nofduplicates + 1
1057                nofskipped = nofskipped + 1
1058            end
1059        elseif not exists(completename) then
1060            -- weird error
1061            if trace_names or trace_rejections then
1062                report_names("%s font %a does not really exist",suffix,completename)
1063            end
1064            nofskipped = nofskipped + 1
1065        elseif not is_qualified_path(completename) and findfile(completename,suffix) == "" then
1066            -- not locatable by backend anyway
1067            if trace_names or trace_rejections then
1068                report_names("%s font %a cannot be found by backend",suffix,completename)
1069            end
1070            nofskipped = nofskipped + 1
1071        else
1072            if #skip_paths > 0 then
1073                for i=1,#skip_paths do
1074                    if find(pathpart,skip_paths[i]) then
1075                        if trace_names or trace_rejections then
1076                            report_names("rejecting path of %s font %a",suffix,completename)
1077                        end
1078                        nofskipped = nofskipped + 1
1079                        return
1080                    end
1081                end
1082            end
1083            if #skip_names > 0 then
1084                for i=1,#skip_paths do
1085                    if find(basepart,skip_names[i]) then
1086                        done[name] = true
1087                        if trace_names or trace_rejections then
1088                            report_names("rejecting name of %s font %a",suffix,completename)
1089                        end
1090                        nofskipped = nofskipped + 1
1091                        return
1092                    end
1093                end
1094            end
1095            if trace_names then
1096                report_names("identifying %s font %a",suffix,completename)
1097            end
1098            -- needs checking with ttc / ttx : date not updated ?
1099            local result = nil
1100            local modification = modificationtime(completename)
1101            if olddata and modification and modification > 0 then
1102                local oldindex = oldindices[storedname] -- index into specifications
1103                if oldindex then
1104                    local oldspecification = oldspecifications[oldindex]
1105                    if oldspecification and oldspecification.filename == storedname then -- double check for out of sync
1106                        local oldmodification = oldspecification.modification
1107                        if oldmodification == modification then
1108                            result = oldspecification
1109                            specifications[#specifications + 1] = result
1110                        else
1111                            -- ??
1112                        end
1113                    else
1114                         -- ??
1115                    end
1116                elseif oldrejected[storedname] == modification then
1117                    result = false
1118                end
1119            end
1120            if result == nil then
1121                local lsuffix = lower(suffix)
1122                local result, message = filters[lsuffix](completename)
1123                if result then
1124                    if #result > 0 then
1125                        for r=1,#result do
1126                            check_name(data,result[r],storedname,modification,suffix,r) -- subfonts start at zero
1127                        end
1128                    else
1129                        check_name(data,result,storedname,modification,suffix)
1130                    end
1131                    if trace_warnings and message and message ~= "" then
1132                        report_names("warning when identifying %s font %a, %s",suffix,completename,message)
1133                    end
1134                elseif trace_warnings then
1135                    nofskipped = nofskipped + 1
1136                    report_names("error when identifying %s font %a, %s",suffix,completename,message or "unknown")
1137                end
1138            end
1139            done[name] = completename
1140        end
1141        logs.flush() --  a bit overkill for each font, maybe not needed here
1142    end
1143
1144    local function traverse(what, method)
1145        local list = filters.list
1146        for n=1,#list do
1147            local suffix = list[n]
1148            local t = os.gettimeofday() -- use elapser
1149            nofread, nofskipped, nofduplicates = 0, 0, 0
1150            suffix = lower(suffix)
1151            report_names("identifying %s font files with suffix %a",what,suffix)
1152            method(suffix)
1153            suffix = upper(suffix)
1154            report_names("identifying %s font files with suffix %a",what,suffix)
1155            method(suffix)
1156            totalnofread, totalnofskipped, totalnofduplicates = totalnofread + nofread, totalnofskipped + nofskipped, totalnofduplicates + nofduplicates
1157            local elapsed = os.gettimeofday() - t
1158            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)
1159        end
1160        logs.flush()
1161    end
1162
1163    -- problem .. this will not take care of duplicates
1164
1165    local function withtree(suffix)
1166        resolvers.dowithfilesintree(".*%." .. suffix .. "$", function(method,root,path,name)
1167            if method == "file" or method == "tree" then
1168                local completename = root .."/" .. path .. "/" .. name
1169                completename = resolveprefix(completename) -- no shortcut
1170                identify(completename,name,suffix,name)
1171                return true
1172            end
1173        end, function(blobtype,blobpath,pattern)
1174            blobpath = resolveprefix(blobpath) -- no shortcut
1175            report_names("scanning path %a for %s files",blobpath,suffix)
1176        end, function(blobtype,blobpath,pattern,total,checked,done)
1177            blobpath = resolveprefix(blobpath) -- no shortcut
1178            report_names("%s %s files checked, %s okay",checked,suffix,done)
1179        end)
1180    end
1181
1182    local function withlsr(suffix) -- all trees
1183        -- we do this only for a stupid names run, not used for context itself,
1184        -- using the vars is too clumsy so we just stick to a full scan instead
1185        local pathlist = resolvers.splitpath(resolvers.showpath("ls-R") or "")
1186        walk_tree(pathlist,suffix,identify)
1187    end
1188
1189    local function withsystem(suffix) -- OSFONTDIR cum suis
1190        walk_tree(names.getpaths(trace),suffix,identify)
1191    end
1192
1193    traverse("tree",withtree) -- TEXTREE only
1194
1195    if not usesystemfonts then
1196        report_names("ignoring system fonts")
1197    elseif texconfig.kpse_init then
1198        traverse("lsr", withlsr)
1199    else
1200        traverse("system", withsystem)
1201    end
1202
1203    data.statistics.readfiles      = totalnofread
1204    data.statistics.skippedfiles   = totalnofskipped
1205    data.statistics.duplicatefiles = totalnofduplicates
1206
1207 -- for k, v in sortedhash(walked) do
1208 --     report_names("%s : %i",k,v)
1209 -- end
1210
1211end
1212
1213local function addfilenames()
1214    local data           = names.data
1215    local specifications = data.specifications
1216    local indices        = { }
1217    local files          = { }
1218    for i=1,#specifications do
1219        local fullname = specifications[i].filename
1220        files[cleanfilename(fullname)] = fullname
1221        indices[fullname] = i
1222    end
1223    data.files   = files
1224    data.indices = indices
1225end
1226
1227local function rejectclashes() -- just to be sure, so no explicit afm will be found then
1228    local specifications  = names.data.specifications
1229    local used            = { }
1230    local okay            = { }
1231    local rejected        = { } -- only keep modification
1232    local o               = 0
1233    for i=1,#specifications do
1234        local s = specifications[i]
1235        local f = s.fontname
1236        if f then
1237            local fnd = used[f]
1238            local fnm = s.filename
1239            if fnd then
1240                if trace_warnings then
1241                    report_names("fontname %a clashes, %a rejected in favor of %a",f,fnm,fnd)
1242                end
1243                rejected[f] = s.modification
1244            else
1245                used[f] = fnm
1246                o = o + 1
1247                okay[o] = s
1248            end
1249        else
1250            o = o + 1
1251            okay[o] = s
1252        end
1253    end
1254    local d = #specifications - #okay
1255    if d > 0 then
1256        report_names("%s files rejected due to clashes",d)
1257    end
1258    names.data.specifications = okay
1259    names.data.rejected       = rejected
1260end
1261
1262local function resetdata()
1263    local mappings  = { }
1264    local fallbacks = { }
1265    for _, k in next, filters.list do
1266        mappings [k] = { }
1267        fallbacks[k] = { }
1268    end
1269    names.data = {
1270        version        = names.version,
1271        mappings       = mappings,
1272        fallbacks      = fallbacks,
1273        specifications = { },
1274        families       = { },
1275        statistics     = { },
1276        names          = { },
1277        indices        = { },
1278        rejected       = { },
1279        datastate      = resolvers.datastate(),
1280    }
1281end
1282
1283function names.identify(force)
1284    local starttime = os.gettimeofday() -- use elapser
1285    resetdata()
1286    analyzefiles(not force and names.readdata(names.basename))
1287    rejectclashes()
1288    collectfamilies()
1289    cleanupkeywords()
1290    collecthashes()
1291    checkduplicates()
1292    addfilenames()
1293 -- sorthashes() -- will be resorted when saved
1294    collectstatistics(os.gettimeofday()-starttime)
1295end
1296
1297function names.is_permitted(name)
1298    return containers.is_usable(names.cache, name)
1299end
1300function names.writedata(name,data)
1301    containers.write(names.cache,name,data)
1302end
1303function names.readdata(name)
1304    return containers.read(names.cache,name)
1305end
1306
1307function names.load(reload,force)
1308    if not names.loaded then
1309        if reload then
1310            if names.is_permitted(names.basename) then
1311                names.identify(force)
1312                names.writedata(names.basename,names.data)
1313            else
1314                report_names("unable to access database cache")
1315            end
1316            names.saved = true
1317        end
1318        local data = names.readdata(names.basename)
1319        names.data = data
1320        if not names.saved then
1321            if not data or not next(data) or not data.specifications or not next(data.specifications) then
1322               names.load(true)
1323            end
1324            names.saved = true
1325        end
1326        if not data then
1327            report_names("accessing the data table failed")
1328        else
1329            unpackreferences()
1330            sorthashes()
1331        end
1332        names.loaded = true
1333    end
1334end
1335
1336local function list_them(mapping,sorted,pattern,t,all)
1337    if mapping[pattern] then
1338        t[pattern] = mapping[pattern]
1339    else
1340        for k=1,#sorted do
1341            local v = sorted[k]
1342            if not t[v] and find(v,pattern) then
1343                t[v] = mapping[v]
1344                if not all then
1345                    return
1346                end
1347            end
1348        end
1349    end
1350end
1351
1352function names.list(pattern,reload,all) -- here?
1353    names.load() -- todo reload
1354    if names.loaded then
1355        local t = { }
1356        local data = names.data
1357        if data then
1358            local list             = filters.list
1359            local mappings         = data.mappings
1360            local sorted_mappings  = data.sorted_mappings
1361            local fallbacks        = data.fallbacks
1362            local sorted_fallbacks = data.sorted_fallbacks
1363            for i=1,#list do
1364                local format = list[i]
1365                list_them(mappings[format],sorted_mappings[format],pattern,t,all)
1366                if next(t) and not all then
1367                    return t
1368                end
1369                list_them(fallbacks[format],sorted_fallbacks[format],pattern,t,all)
1370                if next(t) and not all then
1371                    return t
1372                end
1373            end
1374        end
1375        return t
1376    end
1377end
1378
1379local reloaded = false
1380
1381local function is_reloaded()
1382    if not reloaded then
1383        local data = names.data
1384        if autoreload then
1385            local c_status = serialize(resolvers.datastate())
1386            local f_status = serialize(data.datastate)
1387            if c_status == f_status then
1388                if trace_names then
1389                    report_names("font database has matching configuration and file hashes")
1390                end
1391                return
1392            else
1393                report_names("font database has mismatching configuration and file hashes")
1394            end
1395        else
1396            report_names("font database is regenerated (controlled by directive 'fonts.autoreload')")
1397        end
1398        names.loaded = false
1399        reloaded = true
1400        logs.flush()
1401        names.load(true)
1402    end
1403end
1404
1405--[[ldx--
1406<p>The resolver also checks if the cached names are loaded. Being clever
1407here is for testing purposes only (it deals with names prefixed by an
1408encoding name).</p>
1409--ldx]]--
1410
1411local function fuzzy(mapping,sorted,name,sub) -- no need for reverse sorted here
1412    local condensed = gsub(name,"[^%a%d]","")
1413    local pattern   = condensed .. "$"
1414    local matches   = false
1415    for k=1,#sorted do
1416        local v = sorted[k]
1417        if v == condensed then
1418            return mapping[v], v
1419        elseif find(v,pattern) then
1420            return mapping[v], v
1421        elseif find(v,condensed) then
1422            if matches then
1423                matches[#matches+1] = v
1424            else
1425                matches = { v }
1426            end
1427        end
1428    end
1429    if matches then
1430        if #matches > 1 then
1431            sort(matches,function(a,b) return #a < #b end)
1432        end
1433        matches = matches[1]
1434        return mapping[matches], matches
1435    end
1436end
1437
1438-- we could cache a lookup .. maybe some day ... (only when auto loaded!)
1439
1440local function checkinstance(found,askedname)
1441    local instancenames = found.instancenames
1442    if instancenames then
1443        local fullname = found.fullname
1444        for i=1,#instancenames do
1445            local instancename = instancenames[i]
1446            if fullname .. instancename == askedname then
1447                local f = fastcopy(found)
1448                f.instances = nil
1449                f.instance  = instancename
1450                return f
1451            end
1452        end
1453    end
1454    return found
1455end
1456
1457local function foundname(name,sub) -- sub is not used currently
1458    local data             = names.data
1459    local mappings         = data.mappings
1460    local sorted_mappings  = data.sorted_mappings
1461    local fallbacks        = data.fallbacks
1462    local sorted_fallbacks = data.sorted_fallbacks
1463    local list             = filters.list
1464    -- dilemma: we lookup in the order otf ttf ttc ... afm but now an otf fallback
1465    -- can come after an afm match ... well, one should provide nice names anyway
1466    -- and having two lists is not an option
1467    for i=1,#list do
1468        local l = list[i]
1469        local found = mappings[l][name]
1470        if found then
1471            if trace_names then
1472                report_names("resolved via direct name match: %a",name)
1473            end
1474            return checkinstance(found,name)
1475        end
1476    end
1477    for i=1,#list do
1478        local l = list[i]
1479        local found, fname = fuzzy(mappings[l],sorted_mappings[l],name,sub)
1480        if found then
1481            if trace_names then
1482                report_names("resolved via fuzzy name match: %a onto %a",name,fname)
1483            end
1484            return checkinstance(found,name)
1485        end
1486    end
1487    for i=1,#list do
1488        local l = list[i]
1489        local found = fallbacks[l][name]
1490        if found then
1491            if trace_names then
1492                report_names("resolved via direct fallback match: %a",name)
1493            end
1494            return checkinstance(found,name)
1495        end
1496    end
1497    for i=1,#list do
1498        local l = list[i]
1499        local found, fname = fuzzy(sorted_mappings[l],sorted_fallbacks[l],name,sub)
1500        if found then
1501            if trace_names then
1502                report_names("resolved via fuzzy fallback match: %a onto %a",name,fname)
1503            end
1504            return checkinstance(found,name)
1505        end
1506    end
1507    if trace_names then
1508        report_names("font with name %a cannot be found",name)
1509    end
1510end
1511
1512function names.resolvedspecification(askedname,sub)
1513    if askedname and askedname ~= "" and names.enabled then
1514        askedname = cleanname(askedname)
1515        names.load()
1516        local found = foundname(askedname,sub)
1517        if not found and is_reloaded() then
1518            found = foundname(askedname,sub)
1519        end
1520        return found
1521    end
1522end
1523
1524function names.resolve(askedname,sub)
1525    local found = names.resolvedspecification(askedname,sub)
1526    if found then
1527        return found.filename, found.subfont and found.rawname, found.subfont, found.instance
1528    end
1529end
1530
1531-- function names.getfilename(askedname,suffix) -- last resort, strip funny chars
1532--     names.load()
1533--     local files = names.data.files
1534--     askedname = files and files[cleanfilename(askedname,suffix)] or ""
1535--     if askedname == "" then
1536--         return ""
1537--     else -- never entered
1538--         return resolvers.findbinfile(askedname,suffix) or ""
1539--     end
1540-- end
1541
1542local runtimefiles = { }
1543local runtimedone  = false
1544
1545local function addruntimepath(path)
1546    names.load()
1547    local paths    = type(path) == "table" and path or { path }
1548    local suffixes = tohash(filters.list)
1549    for i=1,#paths do
1550        local path = resolveprefix(paths[i])
1551        if path ~= "" then
1552            local list = dir.glob(path.."/*")
1553            for i=1,#list do
1554                local fullname = list[i]
1555                local suffix   = lower(suffixonly(fullname))
1556                if suffixes[suffix] then
1557                    local c = cleanfilename(fullname)
1558                    runtimefiles[c] = fullname
1559                    if trace_names then
1560                        report_names("adding runtime filename %a for %a",c,fullname)
1561                    end
1562                end
1563            end
1564        end
1565    end
1566end
1567
1568local function addruntimefiles(variable)
1569    local paths = variable and resolvers.expandedpathlistfromvariable(variable)
1570    if paths and #paths > 0 then
1571        addruntimepath(paths)
1572    end
1573end
1574
1575names.addruntimepath  = addruntimepath
1576names.addruntimefiles = addruntimefiles
1577
1578function names.getfilename(askedname,suffix) -- last resort, strip funny chars
1579    if not runtimedone then
1580        addruntimefiles(names.runtimefontsvariable)
1581        runtimedone = true
1582    end
1583    local cleanname = cleanfilename(askedname,suffix)
1584    local found     = runtimefiles[cleanname]
1585    if found then
1586        return found
1587    end
1588    names.load()
1589    local files = names.data.files
1590    local found = files and files[cleanname] or ""
1591    if found == "" and is_reloaded() then
1592        files = names.data.files
1593        found = files and files[cleanname] or ""
1594    end
1595    if found and found ~= "" then
1596        return resolvers.findbinfile(found,suffix) or "" -- we still need to locate it
1597    end
1598end
1599
1600-- specified search
1601
1602local function s_collect_weight_style_width_variant(found,done,all,weight,style,width,variant,family)
1603    if family then
1604        for i=1,#family do
1605            local f = family[i]
1606            if f and weight == f.weight and style == f.style and width == f.width and variant == f.variant then
1607                found[#found+1], done[f] = f, true
1608                if not all then return end
1609            end
1610        end
1611    end
1612end
1613local function m_collect_weight_style_width_variant(found,done,all,weight,style,width,variant,families,sorted,strictname)
1614    for i=1,#sorted do
1615        local k = sorted[i]
1616        local family = families[k]
1617        for i=1,#family do
1618            local f = family[i]
1619            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
1620                found[#found+1], done[f] = f, true
1621                if not all then return end
1622            end
1623        end
1624    end
1625end
1626
1627local function s_collect_weight_style_width(found,done,all,weight,style,width,family)
1628    if family then
1629        for i=1,#family do
1630            local f = family[i]
1631            if f and weight == f.weight and style == f.style and width == f.width then
1632                found[#found+1], done[f] = f, true
1633                if not all then return end
1634            end
1635        end
1636    end
1637end
1638local function m_collect_weight_style_width(found,done,all,weight,style,width,families,sorted,strictname)
1639    for i=1,#sorted do
1640        local k = sorted[i]
1641        local family = families[k]
1642        for i=1,#family do
1643            local f = family[i]
1644            if not done[f] and weight == f.weight and style == f.style and width == f.width and find(f.fontname,strictname) then
1645                found[#found+1], done[f] = f, true
1646                if not all then return end
1647            end
1648        end
1649    end
1650end
1651
1652local function s_collect_weight_style(found,done,all,weight,style,family)
1653    if family then
1654        for i=1,#family do local f = family[i]
1655            if f and weight == f.weight and style == f.style then
1656                found[#found+1], done[f] = f, true
1657                if not all then return end
1658            end
1659        end
1660    end
1661end
1662local function m_collect_weight_style(found,done,all,weight,style,families,sorted,strictname)
1663    for i=1,#sorted do
1664        local k = sorted[i]
1665        local family = families[k]
1666        for i=1,#family do
1667            local f = family[i]
1668            if not done[f] and weight == f.weight and style == f.style and find(f.fontname,strictname) then
1669                found[#found+1], done[f] = f, true
1670                if not all then return end
1671            end
1672        end
1673    end
1674end
1675
1676local function s_collect_style_width(found,done,all,style,width,family)
1677    if family then
1678        for i=1,#family do local f = family[i]
1679            if f and style == f.style and width == f.width then
1680                found[#found+1], done[f] = f, true
1681                if not all then return end
1682            end
1683        end
1684    end
1685end
1686local function m_collect_style_width(found,done,all,style,width,families,sorted,strictname)
1687    for i=1,#sorted do
1688        local k = sorted[i]
1689        local family = families[k]
1690        for i=1,#family do
1691            local f = family[i]
1692            if not done[f] and style == f.style and width == f.width and find(f.fontname,strictname) then
1693                found[#found+1], done[f] = f, true
1694                if not all then return end
1695            end
1696        end
1697    end
1698end
1699
1700local function s_collect_weight(found,done,all,weight,family)
1701    if family then
1702        for i=1,#family do local f = family[i]
1703            if f and weight == f.weight then
1704                found[#found+1], done[f] = f, true
1705                if not all then return end
1706            end
1707        end
1708    end
1709end
1710local function m_collect_weight(found,done,all,weight,families,sorted,strictname)
1711    for i=1,#sorted do
1712        local k = sorted[i]
1713        local family = families[k]
1714        for i=1,#family do
1715            local f = family[i]
1716            if not done[f] and weight == f.weight and find(f.fontname,strictname) then
1717                found[#found+1], done[f] = f, true
1718                if not all then return end
1719            end
1720        end
1721    end
1722end
1723
1724local function s_collect_style(found,done,all,style,family)
1725    if family then
1726        for i=1,#family do local f = family[i]
1727            if f and style == f.style then
1728                found[#found+1], done[f] = f, true
1729                if not all then return end
1730            end
1731        end
1732    end
1733end
1734local function m_collect_style(found,done,all,style,families,sorted,strictname)
1735    for i=1,#sorted do
1736        local k = sorted[i]
1737        local family = families[k]
1738        for i=1,#family do
1739            local f = family[i]
1740            if not done[f] and style == f.style and find(f.fontname,strictname) then
1741                found[#found+1], done[f] = f, true
1742                if not all then return end
1743            end
1744        end
1745    end
1746end
1747
1748local function s_collect_width(found,done,all,width,family)
1749    if family then
1750        for i=1,#family do local f = family[i]
1751            if f and width == f.width then
1752                found[#found+1], done[f] = f, true
1753                if not all then return end
1754            end
1755        end
1756    end
1757end
1758local function m_collect_width(found,done,all,width,families,sorted,strictname)
1759    for i=1,#sorted do
1760        local k = sorted[i]
1761        local family = families[k]
1762        for i=1,#family do
1763            local f = family[i]
1764            if not done[f] and width == f.width and find(f.fontname,strictname) then
1765                found[#found+1], done[f] = f, true
1766                if not all then return end
1767            end
1768        end
1769    end
1770end
1771
1772local function s_collect(found,done,all,family)
1773    if family then
1774        for i=1,#family do local f = family[i]
1775            if f then
1776                found[#found+1], done[f] = f, true
1777                if not all then return end
1778            end
1779        end
1780    end
1781end
1782local function m_collect(found,done,all,families,sorted,strictname)
1783    for i=1,#sorted do
1784        local k = sorted[i]
1785        local family = families[k]
1786        for i=1,#family do
1787            local f = family[i]
1788            if not done[f] and find(f.fontname,strictname) then
1789                found[#found+1], done[f] = f, true
1790                if not all then return end
1791            end
1792        end
1793    end
1794end
1795
1796local function collect(stage,found,done,name,weight,style,width,variant,all)
1797    local data = names.data
1798    local families = data.families
1799    local sorted = data.sorted_families
1800    local strictname = "^".. name -- to be checked
1801    local family = families[name]
1802    if trace_names then
1803        report_names("resolving name %a, weight %a, style %a, width %a, variant %a",name,weight,style,width,variant)
1804    end
1805    if weight and weight ~= "" then
1806        if style and style ~= "" then
1807            if width and width ~= "" then
1808                if variant and variant ~= "" then
1809                    if trace_names then
1810                        report_names("resolving stage %s, name %a, weight %a, style %a, width %a, variant %a",stage,name,weight,style,width,variant)
1811                    end
1812                    s_collect_weight_style_width_variant(found,done,all,weight,style,width,variant,family)
1813                    m_collect_weight_style_width_variant(found,done,all,weight,style,width,variant,families,sorted,strictname)
1814                else
1815                    if trace_names then
1816                        report_names("resolving stage %s, name %a, weight %a, style %a, width %a",stage,name,weight,style,width)
1817                    end
1818                    s_collect_weight_style_width(found,done,all,weight,style,width,family)
1819                    m_collect_weight_style_width(found,done,all,weight,style,width,families,sorted,strictname)
1820                end
1821            else
1822                if trace_names then
1823                    report_names("resolving stage %s, name %a, weight %a, style %a",stage,name,weight,style)
1824                end
1825                s_collect_weight_style(found,done,all,weight,style,family)
1826                m_collect_weight_style(found,done,all,weight,style,families,sorted,strictname)
1827            end
1828        else
1829            if trace_names then
1830                report_names("resolving stage %s, name %a, weight %a",stage,name,weight)
1831            end
1832            s_collect_weight(found,done,all,weight,family)
1833            m_collect_weight(found,done,all,weight,families,sorted,strictname)
1834        end
1835    elseif style and style ~= "" then
1836        if width and width ~= "" then
1837            if trace_names then
1838                report_names("resolving stage %s, name %a, style %a, width %a",stage,name,style,width)
1839            end
1840            s_collect_style_width(found,done,all,style,width,family)
1841            m_collect_style_width(found,done,all,style,width,families,sorted,strictname)
1842        else
1843            if trace_names then
1844                report_names("resolving stage %s, name %a, style %a",stage,name,style)
1845            end
1846            s_collect_style(found,done,all,style,family)
1847            m_collect_style(found,done,all,style,families,sorted,strictname)
1848        end
1849    elseif width and width ~= "" then
1850        if trace_names then
1851            report_names("resolving stage %s, name %a, width %a",stage,name,width)
1852        end
1853        s_collect_width(found,done,all,width,family)
1854        m_collect_width(found,done,all,width,families,sorted,strictname)
1855    else
1856        if trace_names then
1857            report_names("resolving stage %s, name %a",stage,name)
1858        end
1859        s_collect(found,done,all,family)
1860        m_collect(found,done,all,families,sorted,strictname)
1861    end
1862end
1863
1864local function heuristic(name,weight,style,width,variant,all) -- todo: fallbacks
1865    local found, done = { }, { }
1866--~ print(name,weight,style,width,variant)
1867    weight, style, width, variant = weight or "normal", style or "normal", width or "normal", variant or "normal"
1868    name = cleanname(name)
1869    collect(1,found,done,name,weight,style,width,variant,all)
1870    -- still needed ?
1871    if #found == 0 and variant ~= "normal" then -- not weight
1872        variant = "normal"
1873        collect(4,found,done,name,weight,style,width,variant,all)
1874    end
1875    if #found == 0 and width ~= "normal" then
1876        width = "normal"
1877        collect(2,found,done,name,weight,style,width,variant,all)
1878    end
1879    if #found == 0 and weight ~= "normal" then -- not style
1880        weight = "normal"
1881        collect(3,found,done,name,weight,style,width,variant,all)
1882    end
1883    if #found == 0 and style ~= "normal" then -- not weight
1884        style = "normal"
1885        collect(4,found,done,name,weight,style,width,variant,all)
1886    end
1887    --
1888    local nf = #found
1889    if trace_names then
1890        if nf then
1891            local t = { }
1892            for i=1,nf do
1893                t[i] = formatters["%a"](found[i].fontname)
1894            end
1895            report_names("name %a resolved to %s instances: % t",name,nf,t)
1896        else
1897            report_names("name %a unresolved",name)
1898        end
1899    end
1900    if all then
1901        return nf > 0 and found
1902    else
1903        return found[1]
1904    end
1905end
1906
1907function names.specification(askedname,weight,style,width,variant,reload,all)
1908    if askedname and askedname ~= "" and names.enabled then
1909        askedname = cleanname(askedname) -- or cleanname
1910        names.load(reload)
1911        local found = heuristic(askedname,weight,style,width,variant,all)
1912        if not found and is_reloaded() then
1913            found = heuristic(askedname,weight,style,width,variant,all)
1914            if not filename then
1915                found = foundname(askedname) -- old method
1916            end
1917        end
1918        return found
1919    end
1920end
1921
1922function names.collect(askedname,weight,style,width,variant,reload,all)
1923    if askedname and askedname ~= "" and names.enabled then
1924        askedname = cleanname(askedname) -- or cleanname
1925        names.load(reload)
1926        local list = heuristic(askedname,weight,style,width,variant,true)
1927        if not list or #list == 0 and is_reloaded() then
1928            list = heuristic(askedname,weight,style,width,variant,true)
1929        end
1930        return list
1931    end
1932end
1933
1934function names.collectspec(askedname,reload,all)
1935    local name, weight, style, width, variant = names.splitspec(askedname)
1936    return names.collect(name,weight,style,width,variant,reload,all)
1937end
1938
1939function names.resolvespec(askedname,sub) -- redefined later
1940    local found = names.specification(names.splitspec(askedname))
1941    if found then
1942        return found.filename, found.subfont and found.rawname
1943    end
1944end
1945
1946function names.collectfiles(askedname,reload) -- no all
1947    if askedname and askedname ~= "" and names.enabled then
1948        askedname = cleanname(askedname) -- or cleanname
1949        names.load(reload)
1950        local list = { }
1951        local specifications = names.data.specifications
1952        for i=1,#specifications do
1953            local s = specifications[i]
1954            if find(cleanname(basename(s.filename)),askedname) then
1955                list[#list+1] = s
1956            end
1957        end
1958        return list
1959    end
1960end
1961
1962-- todo:
1963--
1964-- blacklisted = {
1965--     ["cmr10.ttf"] = "completely messed up",
1966-- }
1967
1968function names.exists(name)
1969    local found = false
1970    local list = filters.list
1971    for k=1,#list do
1972        local v = list[k]
1973        found = (findfile(name,v) or "") ~= ""
1974        if found then
1975            return found
1976        end
1977    end
1978    return (findfile(name,"tfm") or "") ~= "" or (names.resolve(name) or "") ~= ""
1979end
1980
1981local lastlookups, lastpattern = { }, ""
1982
1983-- function names.lookup(pattern,name,reload) -- todo: find
1984--     if lastpattern ~= pattern then
1985--         names.load(reload)
1986--         local specifications = names.data.specifications
1987--         local families = names.data.families
1988--         local lookups = specifications
1989--         if name then
1990--             lookups = families[name]
1991--         elseif not find(pattern,"=",1,true) then
1992--             lookups = families[pattern]
1993--         end
1994--         if trace_names then
1995--             report_names("starting with %s lookups for %a",#lookups,pattern)
1996--         end
1997--         if lookups then
1998--             for key, value in gmatch(pattern,"([^=,]+)=([^=,]+)") do
1999--                 local t, n = { }, 0
2000--                 if find(value,"*",1,true) then
2001--                     value = topattern(value)
2002--                     for i=1,#lookups do
2003--                         local s = lookups[i]
2004--                         if find(s[key],value) then
2005--                             n = n + 1
2006--                             t[n] = lookups[i]
2007--                         end
2008--                     end
2009--                 else
2010--                     for i=1,#lookups do
2011--                         local s = lookups[i]
2012--                         if s[key] == value then
2013--                             n = n + 1
2014--                             t[n] = lookups[i]
2015--                         end
2016--                     end
2017--                 end
2018--                 if trace_names then
2019--                     report_names("%s matches for key %a with value %a",#t,key,value)
2020--                 end
2021--                 lookups = t
2022--             end
2023--         end
2024--         lastpattern = pattern
2025--         lastlookups = lookups or { }
2026--     end
2027--     return #lastlookups
2028-- end
2029
2030local function look_them_up(lookups,specification)
2031    for key, value in sortedhash(specification) do
2032        local t = { }
2033        local n = 0
2034        if find(value,"*",1,true) then
2035            value = topattern(value)
2036            for i=1,#lookups do
2037                local s = lookups[i]
2038                if find(s[key],value) then
2039                    n = n + 1
2040                    t[n] = lookups[i]
2041                end
2042            end
2043        else
2044            for i=1,#lookups do
2045                local s = lookups[i]
2046                if s[key] == value then
2047                    n = n + 1
2048                    t[n] = lookups[i]
2049                end
2050            end
2051        end
2052        if trace_names then
2053            report_names("%s matches for key %a with value %a",#t,key,value)
2054        end
2055        lookups = t
2056    end
2057    return lookups
2058end
2059
2060local function first_look(name,reload)
2061    names.load(reload)
2062    local data           = names.data
2063    local specifications = data.specifications
2064    local families       = data.families
2065    if name then
2066        return families[name]
2067    else
2068        return specifications
2069    end
2070end
2071
2072function names.lookup(pattern,name,reload) -- todo: find
2073    names.load(reload)
2074    local data           = names.data
2075    local specifications = data.specifications
2076    local families       = data.families
2077    local lookups        = specifications
2078    if name then
2079        name = cleanname(name)
2080    end
2081    if type(pattern) == "table" then
2082        local familyname = pattern.familyname
2083        if familyname then
2084            familyname = cleanname(familyname)
2085            pattern.familyname = familyname
2086        end
2087        local lookups = first_look(name or familyname,reload)
2088        if lookups then
2089            if trace_names then
2090                report_names("starting with %s lookups for '%T'",#lookups,pattern)
2091            end
2092            lookups = look_them_up(lookups,pattern)
2093        end
2094        lastpattern = false
2095        lastlookups = lookups or { }
2096    elseif lastpattern ~= pattern then
2097        local lookups = first_look(name or (not find(pattern,"=",1,true) and pattern),reload)
2098        if lookups then
2099            if trace_names then
2100                report_names("starting with %s lookups for %a",#lookups,pattern)
2101            end
2102            local specification = settings_to_hash(pattern)
2103            local familyname = specification.familyname
2104            if familyname then
2105                familyname = cleanname(familyname)
2106                specification.familyname = familyname
2107            end
2108            lookups = look_them_up(lookups,specification)
2109        end
2110        lastpattern = pattern
2111        lastlookups = lookups or { }
2112    end
2113    return #lastlookups
2114end
2115
2116function names.getlookupkey(key,n)
2117    local l = lastlookups[n or 1]
2118    return (l and l[key]) or ""
2119end
2120
2121function names.noflookups()
2122    return #lastlookups
2123end
2124
2125function names.getlookups(pattern,name,reload)
2126    if pattern then
2127        names.lookup(pattern,name,reload)
2128    end
2129    return lastlookups
2130end
2131
2132-- The following is new ... watch the overload!
2133
2134local specifications = allocate()
2135names.specifications = specifications
2136
2137-- files = {
2138--     name = "antykwapoltawskiego",
2139--     list = {
2140--         ["AntPoltLtCond-Regular.otf"] = {
2141--          -- name   = "antykwapoltawskiego",
2142--             style  = "regular",
2143--             weight = "light",
2144--             width  = "condensed",
2145--         },
2146--     },
2147-- }
2148
2149function names.register(files)
2150    if files then
2151        local list, commonname = files.list, files.name
2152        if list then
2153            local n, m = 0, 0
2154            for filename, filespec in sortedhash(list) do
2155                local name = lower(filespec.name or commonname)
2156                if name and name ~= "" then
2157                    local style    = normalized_styles  [lower(filespec.style   or "normal")]
2158                    local width    = normalized_widths  [lower(filespec.width   or "normal")]
2159                    local weight   = normalized_weights [lower(filespec.weight  or "normal")]
2160                    local variant  = normalized_variants[lower(filespec.variant or "normal")]
2161                    local weights  = specifications[name  ] if not weights  then weights  = { } specifications[name  ] = weights  end
2162                    local styles   = weights       [weight] if not styles   then styles   = { } weights       [weight] = styles   end
2163                    local widths   = styles        [style ] if not widths   then widths   = { } styles        [style ] = widths   end
2164                    local variants = widths        [width ] if not variants then variants = { } widths        [width ] = variants end
2165                    variants[variant] = filename
2166                    n = n + 1
2167                else
2168                    m = m + 1
2169                end
2170            end
2171            if trace_specifications then
2172                report_names("%s filenames registered, %s filenames rejected",n,m)
2173            end
2174        end
2175    end
2176end
2177
2178function names.registered(name,weight,style,width,variant)
2179    local ok = specifications[name]
2180    ok = ok and (ok[(weight  and weight  ~= "" and weight ) or "normal"] or ok.normal)
2181    ok = ok and (ok[(style   and style   ~= "" and style  ) or "normal"] or ok.normal)
2182    ok = ok and (ok[(width   and width   ~= "" and width  ) or "normal"] or ok.normal)
2183    ok = ok and (ok[(variant and variant ~= "" and variant) or "normal"] or ok.normal)
2184    --
2185    -- todo: same fallbacks as with database
2186    --
2187    if ok then
2188        return {
2189            filename = ok,
2190            subname  = "",
2191         -- rawname  = nil,
2192        }
2193    end
2194end
2195
2196function names.resolvespec(askedname,sub) -- overloads previous definition
2197    local name, weight, style, width, variant = names.splitspec(askedname)
2198    if trace_specifications then
2199        report_names("resolving specification: %a to name=%s, weight=%s, style=%s, width=%s, variant=%s",askedname,name,weight,style,width,variant)
2200    end
2201    local found = names.registered(name,weight,style,width,variant)
2202    if found and found.filename then
2203        if trace_specifications then
2204            report_names("resolved by registered names: %a to %s",askedname,found.filename)
2205        end
2206        return found.filename, found.subname, found.rawname
2207    else
2208        found = names.specification(name,weight,style,width,variant)
2209        if found and found.filename then
2210            if trace_specifications then
2211                report_names("resolved by font database: %a to %s",askedname,found.filename)
2212            end
2213            return found.filename, found.subfont and found.rawname
2214        end
2215    end
2216    if trace_specifications then
2217        report_names("unresolved: %s",askedname)
2218    end
2219end
2220
2221function fonts.names.ignoredfile(filename) -- only supported in mkiv
2222    return false -- will be overloaded
2223end
2224
2225-- example made for luatex list (unlikely to be used):
2226--
2227-- local command = [[reg QUERY "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"]]
2228-- local pattern = ".-[\n\r]+%s+(.-)%s%(([^%)]+)%)%s+REG_SZ%s+(%S+)%s+"
2229--
2230-- local function getnamesfromregistry()
2231--     local data = os.resultof(command)
2232--     local list = { }
2233--     for name, format, filename in string.gmatch(data,pattern) do
2234--         list[name] = filename
2235--     end
2236--     return list
2237-- end
2238