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