font-ctx.lua /size: 113 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['font-ctx'] = {
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-- At some point I will clean up the code here so that at the tex end
10-- the table interface is used.
11--
12-- Todo: make a proper 'next id' mechanism (register etc) or wait till 'true'
13-- in virtual fonts indices is implemented.
14
15local tostring, next, type, rawget, tonumber = tostring, next, type, rawget, tonumber
16
17local format, gmatch, match, find, lower, upper, gsub, byte, topattern = string.format, string.gmatch, string.match, string.find, string.lower, string.upper, string.gsub, string.byte, string.topattern
18local concat, serialize, sort, fastcopy, mergedtable = table.concat, table.serialize, table.sort, table.fastcopy, table.merged
19local sortedhash, sortedkeys, sequenced = table.sortedhash, table.sortedkeys, table.sequenced
20local parsers = utilities.parsers
21local settings_to_hash, hash_to_string, settings_to_array = parsers.settings_to_hash, parsers.hash_to_string, parsers.settings_to_array
22local formatcolumns = utilities.formatters.formatcolumns
23local mergehashes = utilities.parsers.mergehashes
24local formatters = string.formatters
25local basename = file.basename
26
27local utfchar, utfbyte = utf.char, utf.byte
28local round = math.round
29
30local context, commands = context, commands
31
32local P, S, C, Cc, Cf, Cg, Ct, lpegmatch = lpeg.P, lpeg.S, lpeg.C, lpeg.Cc, lpeg.Cf, lpeg.Cg, lpeg.Ct, lpeg.match
33
34local trace_features      = false  trackers.register("fonts.features",   function(v) trace_features   = v end)
35local trace_defining      = false  trackers.register("fonts.defining",   function(v) trace_defining   = v end)
36local trace_designsize    = false  trackers.register("fonts.designsize", function(v) trace_designsize = v end)
37local trace_usage         = false  trackers.register("fonts.usage",      function(v) trace_usage      = v end)
38local trace_mapfiles      = false  trackers.register("fonts.mapfiles",   function(v) trace_mapfiles   = v end)
39local trace_automode      = false  trackers.register("fonts.automode",   function(v) trace_automode   = v end)
40local trace_merge         = false  trackers.register("fonts.merge",      function(v) trace_merge      = v end)
41
42local report              = logs.reporter("fonts")
43local report_features     = logs.reporter("fonts","features")
44local report_cummulative  = logs.reporter("fonts","cummulative")
45local report_defining     = logs.reporter("fonts","defining")
46local report_status       = logs.reporter("fonts","status")
47local report_mapfiles     = logs.reporter("fonts","mapfiles")
48
49local setmetatableindex   = table.setmetatableindex
50
51local implement           = interfaces.implement
52
53local chardata            = characters.data
54
55local fonts               = fonts
56local handlers            = fonts.handlers
57local otf                 = handlers.otf -- brrr
58local afm                 = handlers.afm -- brrr
59local tfm                 = handlers.tfm -- brrr
60local names               = fonts.names
61local definers            = fonts.definers
62local specifiers          = fonts.specifiers
63local constructors        = fonts.constructors
64local loggers             = fonts.loggers
65local fontgoodies         = fonts.goodies
66local helpers             = fonts.helpers
67local hashes              = fonts.hashes
68local currentfont         = font.current
69local definefont          = font.define
70
71local getprivateslot      = helpers.getprivateslot
72
73local cleanname           = names.cleanname
74
75local encodings           = fonts.encodings
76----- aglunicodes         = encodings.agl.unicodes
77local aglunicodes         = nil -- delayed loading
78
79local nuts                = nodes.nuts
80local tonut               = nuts.tonut
81
82local nextchar            = nuts.traversers.char
83
84local getattr             = nuts.getattr
85local setattr             = nuts.setattr
86local getstate            = nuts.getstate
87local setsubtype          = nuts.setsubtype
88
89local texgetdimen         = tex.getdimen
90local texsetcount         = tex.setcount
91local texget              = tex.get
92
93local texdefinefont       = tex.definefont
94local texsp               = tex.sp
95
96local fontdata            = hashes.identifiers
97local characters          = hashes.characters
98local descriptions        = hashes.descriptions
99local properties          = hashes.properties
100local resources           = hashes.resources
101local unicodes            = hashes.unicodes
102local csnames             = hashes.csnames
103local lastmathids         = hashes.lastmathids
104local exheights           = hashes.exheights
105local emwidths            = hashes.emwidths
106local parameters          = hashes.parameters
107
108local designsizefilename  = fontgoodies.designsizes.filename
109
110local ctx_char            = context.char
111local ctx_safechar        = context.safechar
112local ctx_getvalue        = context.getvalue
113
114local otffeatures         = otf.features
115local otftables           = otf.tables
116
117local registerotffeature  = otffeatures.register
118
119local sequencers          = utilities.sequencers
120local appendgroup         = sequencers.appendgroup
121local appendaction        = sequencers.appendaction
122
123specifiers.contextsetups  = specifiers.contextsetups  or { }
124specifiers.contextnumbers = specifiers.contextnumbers or { }
125specifiers.contextmerged  = specifiers.contextmerged  or { }
126specifiers.synonyms       = specifiers.synonyms       or { }
127
128local setups   = specifiers.contextsetups
129local numbers  = specifiers.contextnumbers
130local merged   = specifiers.contextmerged
131local synonyms = specifiers.synonyms
132
133storage.register("fonts/setups" ,  setups ,  "fonts.specifiers.contextsetups" )
134storage.register("fonts/numbers",  numbers,  "fonts.specifiers.contextnumbers")
135storage.register("fonts/merged",   merged,   "fonts.specifiers.contextmerged")
136storage.register("fonts/synonyms", synonyms, "fonts.specifiers.synonyms")
137
138-- inspect(setups)
139
140if environment.initex then
141    setmetatableindex(setups,function(t,k)
142        return type(k) == "number" and rawget(t,numbers[k]) or nil
143    end)
144else
145    setmetatableindex(setups,function(t,k)
146        local v = type(k) == "number" and rawget(t,numbers[k])
147        if v then
148            t[k] = v
149            return v
150        end
151    end)
152end
153
154-- this will move elsewhere ...
155
156local function getfontname(tfmdata)
157    return basename(type(tfmdata) == "number" and properties[tfmdata].name or tfmdata.properties.name)
158end
159
160helpers.name = getfontname
161
162local addformatter = utilities.strings.formatters.add
163
164addformatter(formatters,"font:name",    [["'"..fontname(%s).."'"]],          { fontname  = helpers.name })
165addformatter(formatters,"font:features",[["'"..sequenced(%s," ",true).."'"]],{ sequenced = table.sequenced })
166
167-- ... like font-sfm or so
168
169constructors.resolvevirtualtoo = true -- context specific (due to resolver)
170constructors.sharefonts        = true -- experimental
171constructors.nofsharedfonts    = 0
172constructors.nofsharedhashes   = 0
173constructors.nofsharedvectors  = 0
174constructors.noffontsloaded    = 0
175constructors.autocleanup       = true
176
177-- we can get rid of the tfm instance when we have fast access to the
178-- scaled character dimensions at the tex end, e.g. a fontobject.width
179-- actually we already have some of that now as virtual keys in glyphs
180--
181-- flushing the kern and ligature tables from memory saves a lot (only
182-- base mode) but it complicates vf building where the new characters
183-- demand this data .. solution: functions that access them
184
185-- font.getcopy = font.getfont -- we always want the table that context uses
186
187function constructors.cleanuptable(tfmdata)
188    if constructors.autocleanup and tfmdata.properties.virtualized then
189        for k, v in next, tfmdata.characters do
190            if v.commands then v.commands = nil end
191        --  if v.kerns    then v.kerns    = nil end
192        end
193    end
194end
195
196do
197
198    local shares = { }
199    local hashes = { }
200
201
202    local nofinstances = 0
203    local instances    = setmetatableindex(function(t,k)
204        nofinstances = nofinstances + 1
205        t[k] = nofinstances
206        return nofinstances
207    end)
208
209    function constructors.trytosharefont(target,tfmdata)
210        constructors.noffontsloaded = constructors.noffontsloaded + 1
211        if constructors.sharefonts then
212            local fonthash = target.specification.hash
213            if fonthash then
214                local properties = target.properties
215                local fullname   = target.fullname
216                local fontname   = target.fontname
217                local psname     = target.psname
218                -- for the moment here:
219                local instance = properties.instance
220                if instance then
221                    local format = tfmdata.properties.format
222                    if format == "opentype" then
223                        target.streamprovider = 1
224                    elseif format == "truetype" then
225                        target.streamprovider = 2
226                    else
227                        target.streamprovider = 0
228                    end
229                    if target.streamprovider > 0 then
230                        if fullname then
231                            fullname = fullname .. ":" .. instances[instance]
232                            target.fullname = fullname
233                        end
234                        if fontname then
235                            fontname = fontname .. ":" .. instances[instance]
236                            target.fontname = fontname
237                        end
238                        if psname then
239                            -- this one is used for the funny prefix in font names in pdf
240                            -- so it has to be kind of unique in order to avoid subset prefix
241                            -- clashes being reported
242                            psname = psname   .. ":" .. instances[instance]
243                            target.psname = psname
244                        end
245                    end
246                end
247                --
248                local sharedname = hashes[fonthash]
249                if sharedname then
250                    -- this is ok for context as we know that only features can mess with font definitions
251                    -- so a similar hash means that the fonts are similar too
252                    if trace_defining then
253                        report_defining("font %a uses backend resources of font %a (%s)",target.fullname,sharedname,"common hash")
254                    end
255                    target.fullname = sharedname
256                    properties.sharedwith = sharedname
257                    constructors.nofsharedfonts = constructors.nofsharedfonts + 1
258                    constructors.nofsharedhashes = constructors.nofsharedhashes + 1
259                else
260                    -- the one takes more time (in the worst case of many cjk fonts) but it also saves
261                    -- embedding time .. haha, this is interesting: when i got a clash on subset tag
262                    -- collision i saw in the source that these tags are also using a hash like below
263                    -- so maybe we should have an option to pass it from lua
264                    local characters = target.characters
265                    local n = 1
266                    local t = { target.psname }
267                    -- for the moment here:
268                    if instance then
269                        n = n + 1
270                        t[n] = instance
271                    end
272                    --
273                    local u = sortedkeys(characters)
274                    for i=1,#u do
275                        local k = u[i]
276                        n = n + 1 ; t[n] = k
277                        n = n + 1 ; t[n] = characters[k].index or k
278                    end
279                    local checksum   = md5.HEX(concat(t," "))
280                    local sharedname = shares[checksum]
281                    local fullname   = target.fullname
282                    if sharedname then
283                        if trace_defining then
284                            report_defining("font %a uses backend resources of font %a (%s)",fullname,sharedname,"common vector")
285                        end
286                        fullname = sharedname
287                        properties.sharedwith= sharedname
288                        constructors.nofsharedfonts = constructors.nofsharedfonts + 1
289                        constructors.nofsharedvectors = constructors.nofsharedvectors + 1
290                    else
291                        shares[checksum] = fullname
292                    end
293                    target.fullname  = fullname
294                    hashes[fonthash] = fullname
295                end
296            end
297        end
298    end
299
300end
301
302directives.register("fonts.checksharing",function(v)
303    if not v then
304        report_defining("font sharing in backend is disabled")
305    end
306    constructors.sharefonts = v
307end)
308
309function definers.resetnullfont()
310    -- resetting is needed because tikz misuses nullfont
311    local parameters = fonts.nulldata.parameters
312    --
313    parameters.slant         = 0 -- 1
314    parameters.space         = 0 -- 2
315    parameters.space_stretch = 0 -- 3
316    parameters.space_shrink  = 0 -- 4
317    parameters.x_height      = 0 -- 5
318    parameters.quad          = 0 -- 6
319    parameters.extra_space   = 0 -- 7
320    parameters.designsize    = 655360
321    --
322    constructors.enhanceparameters(parameters) -- official copies for us
323    --
324    definers.resetnullfont = function() end
325end
326
327-- This is some initialization code and we don't want (tracing) clutter in lmtx
328-- which is why we follow a different route there. I admit that is sounds freaky.
329-- Watch out: in lmtx the font dimen array is no longer resized automatically.
330
331implement {
332    name      = "resetnullfont",
333    onlyonce  = true,
334    permanent = false,
335    actions   = function()
336        for i=1,7 do
337            context([[\fontdimen%s\nullfont\zeropoint]],i)
338        end
339        definers.resetnullfont()
340    end
341}
342
343-- this cannot be a feature initializer as there is no auto namespace
344-- so we never enter the loop then; we can store the defaults in the tma
345-- file (features.gpos.mkmk = 1 etc)
346
347local needsnodemode = { -- we will have node mode by default anyway
348 -- gsub_single              = true,
349    gsub_multiple            = true,
350 -- gsub_alternate           = true,
351 -- gsub_ligature            = true,
352    gsub_context             = true,
353    gsub_contextchain        = true,
354    gsub_reversecontextchain = true,
355 -- chainsub                 = true,
356 -- reversesub               = true,
357    gpos_mark2base           = true,
358    gpos_mark2ligature       = true,
359    gpos_mark2mark           = true,
360    gpos_cursive             = true,
361 -- gpos_single              = true,
362 -- gpos_pair                = true,
363    gpos_context             = true,
364    gpos_contextchain        = true,
365}
366
367otftables.scripts.auto = "automatic fallback to latn when no dflt present"
368
369-- setmetatableindex(otffeatures.descriptions,otftables.features)
370
371local function checkedscript(tfmdata,resources,features)
372    local latn   = false
373    local script = false
374    if resources.features then
375        for g, list in next, resources.features do
376            for f, scripts in next, list do
377                if scripts.dflt then
378                    script = "dflt"
379                    break
380                elseif scripts.latn then
381                    latn = true
382                end
383            end
384        end
385    end
386    if not script then
387        script = latn and "latn" or "dflt"
388    end
389    if trace_automode then
390        report_defining("auto script mode, using script %a in font %!font:name!",script,tfmdata)
391    end
392    features.script = script
393    return script
394end
395
396-- basemode combined with dynamics is somewhat tricky
397
398local function checkedmode(tfmdata,resources,features)
399    local sequences = resources.sequences
400    if sequences and #sequences > 0 then
401        local script    = features.script   or "dflt"
402        local language  = features.language or "dflt"
403        for feature, value in next, features do
404            if value then
405                local found = false
406                for i=1,#sequences do
407                    local sequence = sequences[i]
408                    local features = sequence.features
409                    if features then
410                        local scripts = features[feature]
411                        if scripts then
412                            local languages = scripts[script]
413                            if languages and languages[language] then
414                                if found then
415                                    -- more than one lookup
416                                    if trace_automode then
417                                        report_defining("forcing mode %a, font %!font:name!, feature %a, script %a, language %a, %s",
418                                            "node",tfmdata,feature,script,language,"multiple lookups")
419                                    end
420                                    features.mode = "node"
421                                    return "node"
422                                elseif needsnodemode[sequence.type] then
423                                    if trace_automode then
424                                        report_defining("forcing mode %a, font %!font:name!, feature %a, script %a, language %a, %s",
425                                            "node",tfmdata,feature,script,language,"no base support")
426                                    end
427                                    features.mode = "node"
428                                    return "node"
429                                else
430                                    -- at least one lookup
431                                    found = true
432                                end
433                            end
434                        end
435                    end
436                end
437            end
438        end
439    end
440    if trace_automode then
441        report_defining("forcing mode base, font %!font:name!",tfmdata)
442    end
443    features.mode = "base" -- new, or is this wrong?
444    return "base"
445end
446
447definers.checkedscript = checkedscript
448definers.checkedmode   = checkedmode
449
450local function modechecker(tfmdata,features,mode) -- we cannot adapt features as they are shared!
451    if trace_features then
452        report_features("fontname %!font:name!, features %!font:features!",tfmdata,features)
453    end
454    local rawdata   = tfmdata.shared.rawdata
455    local resources = rawdata and rawdata.resources
456    local script    = features.script
457    if resources then
458        if script == "auto" then
459            script = checkedscript(tfmdata,resources,features)
460        end
461        if mode == "auto" then
462            mode = checkedmode(tfmdata,resources,features)
463        end
464    else
465        report_features("missing resources for font %!font:name!",tfmdata)
466    end
467    return mode
468end
469
470registerotffeature {
471    -- we only set the checker and leave other settings of the mode
472    -- feature as they are
473    name        = "mode",
474    modechecker = modechecker,
475}
476
477-- -- default = true anyway
478--
479-- local normalinitializer = constructors.getfeatureaction("otf","initializers","node","analyze")
480--
481-- local function analyzeinitializer(tfmdata,value,features) -- attr
482--     if value == "auto" and features then
483--         value = features.init or features.medi or features.fina or features.isol or false
484--     end
485--     return normalinitializer(tfmdata,value,features)
486-- end
487--
488-- registerotffeature {
489--     name         = "analyze",
490--     initializers = {
491--         node = analyzeinitializer,
492--     },
493-- }
494
495do
496
497    local beforecopyingcharacters = sequencers.new {
498        name      = "beforecopyingcharacters",
499        arguments = "target,original",
500    }
501
502    appendgroup(beforecopyingcharacters,"before") -- user
503    appendgroup(beforecopyingcharacters,"system") -- private
504    appendgroup(beforecopyingcharacters,"after" ) -- user
505
506    function constructors.beforecopyingcharacters(original,target)
507        local runner = beforecopyingcharacters.runner
508        if runner then
509            runner(original,target)
510        end
511    end
512
513    local aftercopyingcharacters = sequencers.new {
514        name      = "aftercopyingcharacters",
515        arguments = "target,original",
516    }
517
518    appendgroup(aftercopyingcharacters,"before") -- user
519    appendgroup(aftercopyingcharacters,"system") -- private
520    appendgroup(aftercopyingcharacters,"after" ) -- user
521
522    function constructors.aftercopyingcharacters(original,target)
523        local runner = aftercopyingcharacters.runner
524        if runner then
525            runner(original,target)
526        end
527    end
528
529end
530
531-- So far we haven't really dealt with features (or whatever we want to pass along
532-- with the font definition. We distinguish the following situations:
533--
534--   name:xetex like specs
535--   name@virtual font spec
536--   name*context specification
537--
538-- Currently fonts are scaled while constructing the font, so we have to do scaling
539-- of commands in the vf at that point using e.g. "local scale = g.parameters.factor
540-- or 1" after all, we need to work with copies anyway and scaling needs to be done
541-- at some point; however, when virtual tricks are used as feature (makes more
542-- sense) we scale the commands in fonts.constructors.scale (and set the factor
543-- there).
544
545local loadfont = definers.loadfont
546
547function definers.loadfont(specification,size,id) -- overloads the one in font-def
548    local variants = definers.methods.variants
549    local virtualfeatures = specification.features.virtual
550    if virtualfeatures and virtualfeatures.preset then
551        local variant = variants[virtualfeatures.preset]
552        if variant then
553            return variant(specification,size,id)
554        end
555    else
556        local tfmdata = loadfont(specification,size,id)
557     -- constructors.checkvirtualid(tfmdata,id)
558        return tfmdata
559    end
560end
561
562local function predefined(specification)
563    local variants = definers.methods.variants
564    local detail = specification.detail
565    if detail ~= "" and variants[detail] then
566        specification.features.virtual = { preset = detail }
567    end
568    return specification
569end
570
571definers.registersplit("@", predefined,"virtual")
572
573local normalize_features = otffeatures.normalize     -- should be general
574
575local function definecontext(name,t) -- can be shared
576    local number = setups[name] and setups[name].number or 0 -- hm, numbers[name]
577    if number == 0 then
578        number = #numbers + 1
579        numbers[number] = name
580    end
581    t.number = number
582    setups[name] = t
583    return number, t
584end
585
586-- {a,b,c} as table (so we don' need to parse again when it gets applied)
587-- we will update this ... when we have foo={a,b,c} then we can keep the table
588
589-- \definefontfeature[demo][a={b,c}]
590-- \definefontfeature[demo][a={b=12,c={34,35}}]
591
592local h = setmetatableindex(function(t,k)
593    local v = "," .. k .. ","
594    t[k] = v
595    return v
596end)
597
598-- local function removefromhash(hash,key)
599--     local pattern = h[key]
600--     for k in next, hash do
601--         if k ~= key and find(h[k],pattern) then -- if find(k,",") and ...
602--             hash[k] = nil
603--         end
604--     end
605-- end
606
607local function presetcontext(name,parent,features) -- will go to con and shared
608    if features == "" and find(parent,"=",1,true) then
609        features = parent
610        parent = ""
611    end
612    if not features or features == "" then
613        features = { }
614    elseif type(features) == "string" then
615        features = normalize_features(settings_to_hash(features))
616     -- if type(value) == "string" and find(value,"[=:]") then
617     --     local t = settings_to_hash_colon_too(value) -- clashes with foo=file:bar
618        for key, value in next, features do
619            if type(value) == "string" and find(value,"[=]") then
620                local t = settings_to_hash(value)
621                if next(t) then
622                    features[key] = sequenced(normalize_features(t,true),",")
623                end
624            end
625        end
626    else
627        features = normalize_features(features)
628    end
629    -- todo: synonyms, and not otf bound
630    if parent ~= "" then
631        for p in gmatch(parent,"[^, ]+") do
632            local s = setups[p]
633            if s then
634                for k, v in next, s do
635                    -- no, as then we cannot overload: e.g. math,mathextra
636                    -- reverted, so we only take from parent when not set
637                    if features[k] == nil then
638                        features[k] = v
639                    end
640                end
641            else
642                -- just ignore an undefined one .. i.e. we can refer to not yet defined
643            end
644        end
645    end
646    -- these are auto set so in order to prevent redundant definitions
647    -- we need to preset them (we hash the features and adding a default
648    -- setting during initialization may result in a different hash)
649    --
650    -- for k,v in next, triggers do
651    --     if features[v] == nil then -- not false !
652    --         local vv = default_features[v]
653    --         if vv then features[v] = vv end
654    --     end
655    -- end
656    --
657    for feature,value in next, features do
658        if value == nil then -- not false !
659            local default = default_features[feature]
660            if default ~= nil then
661                features[feature] = default
662            end
663        end
664    end
665    -- sparse 'm so that we get a better hash and less test (experimental
666    -- optimization)
667    local t = { } -- can we avoid t ?
668    for k,v in next, features do
669     -- if v then t[k] = v end
670        t[k] = v
671    end
672    -- the number is needed for dynamic features; maybe number should always be
673    -- renewed as we can redefine features ... i need a test
674    local number = setups[name] and setups[name].number or 0
675    if number == 0 then
676        number = #numbers + 1
677        numbers[number] = name
678    end
679    --
680    t.number = number
681    -- there is the special case of combined features as we have in math but maybe
682    -- this has to change some day ... otherwise we mess up dynamics (ok, we could
683    -- impose a limit there: no combined features)
684    --
685    -- done elsewhere (!)
686    --
687 -- removefromhash(setups,name) -- can have changed (like foo,extramath)
688    --
689    setups[name] = t
690    return number, t
691end
692
693local function adaptcontext(pattern,features)
694    local pattern = topattern(pattern,false,true)
695    for name in next, setups do
696        if find(name,pattern) then
697            presetcontext(name,name,features)
698        end
699    end
700end
701
702-- local function contextnumber(name) -- will be replaced
703--     local t = setups[name]
704--     if not t then
705--         return 0
706--     elseif t.auto then -- check where used, autolanguage / autoscript?
707--         local lng = tonumber(tex.language)
708--         local tag = name .. ":" .. lng
709--         local s = setups[tag]
710--         if s then
711--             return s.number or 0
712--         else
713--             local script, language = languages.association(lng)
714--             if t.script ~= script or t.language ~= language then
715--                 local s = fastcopy(t)
716--                 local n = #numbers + 1
717--                 setups[tag] = s
718--                 numbers[n] = tag
719--                 s.number = n
720--                 s.script = script
721--                 s.language = language
722--                 return n
723--             else
724--                 setups[tag] = t
725--                 return t.number or 0
726--             end
727--         end
728--     else
729--         return t.number or 0
730--     end
731-- end
732
733local function contextnumber(name) -- will be replaced
734    local t = setups[name]
735    return t and t.number or 0
736end
737
738local function mergecontext(currentnumber,extraname,option) -- number string number (used in scrp-ini
739    local extra = setups[extraname]
740    if extra then
741        local current = setups[numbers[currentnumber]]
742        local mergedfeatures = { }
743        local mergedname = nil
744        if option < 0 then
745            if current then
746                for k, v in next, current do
747                    if not extra[k] then
748                        mergedfeatures[k] = v
749                    end
750                end
751            end
752            mergedname = currentnumber .. "-" .. extraname
753        else
754            if current then
755                for k, v in next, current do
756                    mergedfeatures[k] = v
757                end
758            end
759            for k, v in next, extra do
760                mergedfeatures[k] = v
761            end
762            mergedname = currentnumber .. "+" .. extraname
763        end
764        local number = #numbers + 1
765        mergedfeatures.number = number
766        numbers[number] = mergedname
767        merged[number] = option
768        setups[mergedname] = mergedfeatures
769        return number -- contextnumber(mergedname)
770    else
771        return currentnumber
772    end
773end
774
775local extrasets = { }
776
777setmetatableindex(extrasets,function(t,k)
778    local v = mergehashes(setups,k)
779    t[k] = v
780    return v
781end)
782
783local function mergecontextfeatures(currentname,extraname,how,mergedname) -- string string
784    local extra = setups[extraname] or extrasets[extraname]
785    if extra then
786        local current = setups[currentname]
787        local mergedfeatures = { }
788        if how == "+" then
789            if current then
790                for k, v in next, current do
791                    mergedfeatures[k] = v
792                end
793            end
794            for k, v in next, extra do
795                mergedfeatures[k] = v
796            end
797            if trace_merge then
798                report_features("merge %a, method %a, current %|T, extra %|T, result %|T",mergedname,"add",current or { },extra,mergedfeatures)
799            end
800        elseif how == "-" then
801            if current then
802                for k, v in next, current do
803                    mergedfeatures[k] = v
804                end
805            end
806            for k, v in next, extra do
807                -- only boolean features
808                if v == true then
809                    mergedfeatures[k] = false
810                end
811            end
812            if trace_merge then
813                report_features("merge %a, method %a, current %|T, extra %|T, result %|T",mergedname,"subtract",current or { },extra,mergedfeatures)
814            end
815        else -- =
816            for k, v in next, extra do
817                mergedfeatures[k] = v
818            end
819            if trace_merge then
820                report_features("merge %a, method %a, result %|T",mergedname,"replace",mergedfeatures)
821            end
822        end
823        local number = #numbers + 1
824        mergedfeatures.number = number
825        numbers[number] = mergedname
826        merged[number] = option
827        setups[mergedname] = mergedfeatures
828        return number
829    else
830        return numbers[currentname] or 0
831    end
832end
833
834local function registercontext(fontnumber,extraname,option)
835    local extra = setups[extraname]
836    if extra then
837        local mergedfeatures = { }
838        local mergedname     = nil
839        if option < 0 then
840            mergedname = fontnumber .. "-" .. extraname
841        else
842            mergedname = fontnumber .. "+" .. extraname
843        end
844        for k, v in next, extra do
845            mergedfeatures[k] = v
846        end
847        local number = #numbers + 1
848        mergedfeatures.number = number
849        numbers[number] = mergedname
850        merged[number] = option
851        setups[mergedname] = mergedfeatures
852        return number -- contextnumber(mergedname)
853    else
854        return 0
855    end
856end
857
858local function registercontextfeature(mergedname,extraname,how)
859    local extra = setups[extraname]
860    if extra then
861        local mergedfeatures = { }
862        for k, v in next, extra do
863            mergedfeatures[k] = v
864        end
865        local number = #numbers + 1 -- we somehow end up with steps of 2
866        mergedfeatures.number = number
867        numbers[number] = mergedname
868        merged[number] = how == "=" and 1 or 2 -- 1=replace, 2=combine
869        setups[mergedname] = mergedfeatures
870        return number -- contextnumber(mergedname)
871    else
872        report_features("unknown feature %a cannot be merged into %a using method %a",extraname,mergedname,how)
873        return 0
874    end
875end
876
877specifiers.presetcontext   = presetcontext
878specifiers.contextnumber   = contextnumber
879specifiers.mergecontext    = mergecontext
880specifiers.registercontext = registercontext
881specifiers.definecontext   = definecontext
882
883-- we extend the hasher:
884
885constructors.hashmethods.virtual = function(list)
886    local s = { }
887    local n = 0
888    for k, v in next, list do
889        n = n + 1
890     -- if v == true then
891     --     s[n] = k .. '=true'
892     -- elseif v == false then
893     --     s[n] = k .. '=false'
894     -- else
895     --     s[n] = k .. "=" .. v
896     -- end
897        s[n] = k .. "=" .. tostring(v)
898    end
899    if n > 0 then
900        sort(s)
901        return concat(s,"+")
902    end
903end
904
905if not JITSUPPORTED then
906
907    constructors.hashmethods.normal = function(list)
908        local s = { }
909        local n = 0
910        for k, v in next, list do
911            if not k then
912                -- no need to add to hash
913            elseif k == "number" or k == "features" then
914                -- no need to add to hash (maybe we need a skip list)
915            else
916                n = n + 1
917                if type(v) == "table" then
918                    -- table.sequenced
919                    local t = { }
920                    local m = 0
921                    for k, v in next, v do
922                        m = m + 1
923                        t[m] = format("%q=%q",k,v)
924                    end
925                    sort(t)
926                    s[n] = format("%q={%s}",k,concat(t,","))
927                else
928                    s[n] = format("%q=%q",k,v)
929                end
930            end
931        end
932        if n > 0 then
933            sort(s)
934            return concat(s,"+")
935        end
936    end
937
938    constructors.hashmethods.virtual = function(list)
939        local s = { }
940        local n = 0
941        for k, v in next, list do
942            n = n + 1
943            s[n] = format("%q=%q",k,v)
944        end
945        if n > 0 then
946            sort(s)
947            return concat(s,"+")
948        end
949    end
950
951end
952
953-- end of redefine
954
955-- local withcache = { } -- concat might be less efficient than nested tables
956--
957-- local function withset(name,what)
958--     local zero = texgetattribute(0)
959--     local hash = zero .. "+" .. name .. "*" .. what
960--     local done = withcache[hash]
961--     if not done then
962--         done = mergecontext(zero,name,what)
963--         withcache[hash] = done
964--     end
965--     texsetattribute(0,done)
966-- end
967--
968-- local function withfnt(name,what,font)
969--     local font = font or currentfont()
970--     local hash = font .. "*" .. name .. "*" .. what
971--     local done = withcache[hash]
972--     if not done then
973--         done = registercontext(font,name,what)
974--         withcache[hash] = done
975--     end
976--     texsetattribute(0,done)
977-- end
978
979function specifiers.showcontext(name)
980    return setups[name] or setups[numbers[name]] or setups[numbers[tonumber(name)]] or { }
981end
982
983-- we need a copy as we will add (fontclass) goodies to the features and
984-- that is bad for a shared table
985
986-- local function splitcontext(features) -- presetcontext creates dummy here
987--     return fastcopy(setups[features] or (presetcontext(features,"","") and setups[features]))
988-- end
989
990-- local function splitcontext(features) -- presetcontext creates dummy here
991--     local sf = setups[features]
992--     if not sf then
993--         local n -- number
994--         if find(features,",") then
995--             -- let's assume a combination which is not yet defined but just specified (as in math)
996--             n, sf = presetcontext(features,features,"")
997--         else
998--             -- we've run into an unknown feature and or a direct spec so we create a dummy
999--             n, sf = presetcontext(features,"","")
1000--         end
1001--     end
1002--     return fastcopy(sf)
1003-- end
1004
1005local function splitcontext(features) -- presetcontext creates dummy here
1006    local n, sf
1007    -- We can have: "a=yes,b=yes" "a,b" "a" "a=yes" etc.
1008    if find(features,"[,=]") then
1009        --
1010        -- from elsewhere (!)
1011        --
1012        -- this will become:
1013        --
1014     -- if find(features,"^reset," then
1015            setups[features] = nil
1016     -- end
1017        -- let's assume a combination which is not yet defined but just specified (as in math)
1018        n, sf = presetcontext(features,features,"")
1019    else
1020        sf = setups[features]
1021        if not sf then
1022            -- we've run into an unknown feature and or a direct spec so we create a dummy
1023            n, sf = presetcontext(features,"","")
1024        end
1025    end
1026    return fastcopy(sf)
1027end
1028
1029-- local splitter = lpeg.splitat("=")
1030--
1031-- local function splitcontext(features)
1032--     local setup = setups[features]
1033--     if setup then
1034--         return setup
1035--     elseif find(features,",",1,true) then
1036--         -- This is not that efficient but handy anyway for quick and dirty tests
1037--         -- beware, due to the way of caching setups you can get the wrong results
1038--         -- when components change. A safeguard is to nil the cache.
1039--         local merge = nil
1040--         for feature in gmatch(features,"[^, ]+") do
1041--             if find(feature,"=",1,true) then
1042--                 local k, v = lpegmatch(splitter,feature)
1043--                 if k and v then
1044--                     if not merge then
1045--                         merge = { k = v }
1046--                     else
1047--                         merge[k] = v
1048--                     end
1049--                 end
1050--             else
1051--                 local s = setups[feature]
1052--                 if not s then
1053--                     -- skip
1054--                 elseif not merge then
1055--                     merge = s
1056--                 else
1057--                     for k, v in next, s do
1058--                         merge[k] = v
1059--                     end
1060--                 end
1061--             end
1062--         end
1063--         setup = merge and presetcontext(features,"",merge) and setups[features]
1064--         -- actually we have to nil setups[features] in order to permit redefinitions
1065--         setups[features] = nil
1066--     end
1067--     return setup or (presetcontext(features,"","") and setups[features]) -- creates dummy
1068-- end
1069
1070specifiers.splitcontext = splitcontext
1071
1072function specifiers.contexttostring(name,kind,separator,yes,no,strict,omit) -- not used
1073    return hash_to_string(
1074        mergedtable(handlers[kind].features.defaults or {},setups[name] or {}),
1075        separator, yes, no, strict, omit or { "number" }
1076    )
1077end
1078
1079local function starred(features) -- no longer fallbacks here
1080    local detail = features.detail
1081    if detail and detail ~= "" then
1082        features.features.normal = splitcontext(detail)
1083    else
1084        features.features.normal = { }
1085    end
1086    return features
1087end
1088
1089definers.registersplit('*',starred,"featureset")
1090
1091-- sort of xetex mode, but without [] and / as we have file: and name: etc
1092
1093local space      = P(" ")
1094local spaces     = space^0
1095local separator  = S(";,")
1096local equal      = P("=")
1097local sometext   = C((1-equal-space-separator)^1)
1098local truevalue  = P("+") * spaces * sometext                           * Cc(true)
1099local falsevalue = P("-") * spaces * sometext                           * Cc(false)
1100local somevalue  =                   sometext * spaces                  * Cc(true)
1101local keyvalue   =                   sometext * spaces * equal * spaces * sometext
1102local pattern    = Cf(Ct("") * (space + separator + Cg(falsevalue + truevalue + keyvalue + somevalue))^0, rawset)
1103
1104local function colonized(specification)
1105    specification.features.normal = normalize_features(lpegmatch(pattern,specification.detail))
1106    return specification
1107end
1108
1109definers.registersplit(":",colonized,"direct")
1110
1111-- define (two steps)
1112
1113local sizepattern, splitpattern, specialscale  do
1114
1115    ----- space          = P(" ")
1116    ----- spaces         = space^0
1117    local leftparent     = (P"(")
1118    local rightparent    = (P")")
1119    local leftbrace      = (P"{")
1120    local rightbrace     = (P"}")
1121    local withinparents  = leftparent * (1-rightparent)^0 * rightparent
1122    local withinbraces   = leftbrace  * (1-rightbrace )^0 * rightbrace
1123    local value          = C((withinparents + withinbraces + (1-space))^1)
1124    local dimension      = C((space/"" + P(1))^1)
1125    local rest           = C(P(1)^0)
1126    local scale_none     =                     Cc(0)
1127    local scale_at       = (P("at") +P("@")) * Cc(1) * spaces * dimension -- dimension
1128    local scale_sa       = P("sa")           * Cc(2) * spaces * dimension -- number
1129    local scale_mo       = P("mo")           * Cc(3) * spaces * dimension -- number
1130    local scale_scaled   = P("scaled")       * Cc(4) * spaces * dimension -- number
1131    local scale_ht       = P("ht")           * Cc(5) * spaces * dimension -- dimension
1132    local scale_cp       = P("cp")           * Cc(6) * spaces * dimension -- dimension
1133
1134    specialscale = { [5] = "ht", [6] = "cp" }
1135
1136    sizepattern  = spaces * (scale_at + scale_sa + scale_mo + scale_ht + scale_cp + scale_scaled + scale_none)
1137    splitpattern = spaces * value * spaces * rest
1138
1139end
1140
1141function helpers.splitfontpattern(str)
1142    local name, size = lpegmatch(splitpattern,str)
1143    local kind, size = lpegmatch(sizepattern,size)
1144    return name, kind, size
1145end
1146
1147function helpers.fontpatternhassize(str)
1148    local name, size = lpegmatch(splitpattern,str)
1149    local kind, size = lpegmatch(sizepattern,size)
1150    return size or false
1151end
1152
1153local specification -- still needed as local ?
1154
1155local getspecification = definers.getspecification
1156
1157-- we can make helper macros which saves parsing (but normaly not
1158-- that many calls, e.g. in mk a couple of 100 and in metafun 3500)
1159
1160local specifiers = { }
1161
1162do  -- else too many locals
1163
1164    local starttiming = statistics.starttiming
1165    local stoptiming  = statistics.stoptiming
1166
1167    local setmacro    = tokens.setters.macro
1168
1169    implement {
1170        name      = "definefont_one",
1171        arguments = "string",
1172        actions   = function(str)
1173            starttiming(fonts)
1174            if trace_defining then
1175                report_defining("memory usage before: %s",statistics.memused())
1176                report_defining("start stage one: %s",str)
1177            end
1178            local fullname, size = lpegmatch(splitpattern,str)
1179            local lookup, name, sub, method, detail = getspecification(fullname)
1180            if not name then
1181                report_defining("strange definition %a",str)
1182             -- ctx_setdefaultfontname()
1183            elseif name == "unknown" then
1184             -- ctx_setdefaultfontname()
1185            else
1186             -- ctx_setsomefontname(name)
1187                setmacro("somefontname",name,"global")
1188            end
1189            -- we can also use a count for the size
1190            if size and size ~= "" then
1191                local mode, size = lpegmatch(sizepattern,size)
1192                if size and mode then
1193                    texsetcount("scaledfontmode",mode)
1194                 -- ctx_setsomefontsize(size)
1195                    setmacro("somefontsize",size)
1196                else
1197                    texsetcount("scaledfontmode",0)
1198                 -- ctx_setemptyfontsize()
1199                end
1200            elseif true then
1201                -- so we don't need to check in tex
1202                texsetcount("scaledfontmode",2)
1203             -- ctx_setemptyfontsize()
1204            else
1205                texsetcount("scaledfontmode",0)
1206             -- ctx_setemptyfontsize()
1207            end
1208            specification = definers.makespecification(str,lookup,name,sub,method,detail,size)
1209-- specification.original = str
1210            if trace_defining then
1211                report_defining("stop stage one")
1212            end
1213        end
1214    }
1215
1216    local function nice_cs(cs)
1217        return (gsub(cs,".->", ""))
1218    end
1219
1220    local n               = 0
1221    local busy            = false
1222    local combinefeatures = false
1223
1224    directives.register("fonts.features.combine",function(v)
1225        combinefeatures = v
1226    end)
1227
1228    implement {
1229        name      = "definefont_two",
1230        arguments = {
1231            "boolean", "string", "string", "integer", "integer", "string", "string", "string", "string",
1232            "integer", "integer", "integer", "string", "string", "string", "string", "integer",
1233        },
1234        actions   = function (
1235                        global,          -- \ifx\fontclass\empty\s!false\else\s!true\fi
1236                        cs,              -- {#csname}%
1237                        str,             -- \somefontfile
1238                        size,            -- \d_font_scaled_font_size
1239                        inheritancemode, -- \c_font_feature_inheritance_mode
1240                        classfeatures,   -- \m_font_class_features
1241                        fontfeatures,    -- \m_font_features
1242                        classfallbacks,  -- \m_font_class_fallbacks
1243                        fontfallbacks,   -- \m_font_fallbacks
1244                        mathsize,        -- \fontface
1245                        textsize,        -- \d_font_scaled_text_face
1246                        relativeid,      -- \relativefontid
1247                        classgoodies,    -- \m_font_class_goodies
1248                        goodies,         -- \m_font_goodies
1249                        classdesignsize, -- \m_font_class_designsize
1250                        fontdesignsize,  -- \m_font_designsize
1251                        scaledfontmode   -- \scaledfontmode
1252                    )
1253            if trace_defining then
1254                report_defining("start stage two: %s, size %s, features %a & %a, mode %a",str,size,classfeatures,fontfeatures,inheritancemode)
1255            end
1256            -- name is now resolved and size is scaled cf sa/mo
1257            local lookup, name, sub, method, detail = getspecification(str or "")
1258            -- new (todo: inheritancemode)
1259            local designsize = fontdesignsize ~= "" and fontdesignsize or classdesignsize or ""
1260            local designname = designsizefilename(name,designsize,size)
1261            if designname and designname ~= "" then
1262                if trace_defining or trace_designsize then
1263                    report_defining("remapping name %a, specification %a, size %a, designsize %a",name,designsize,size,designname)
1264                end
1265                -- we don't catch detail here
1266                local o_lookup, o_name, o_sub, o_method, o_detail = getspecification(designname)
1267                if o_lookup and o_lookup ~= "" then lookup = o_lookup end
1268                if o_method and o_method ~= "" then method = o_method end
1269                if o_detail and o_detail ~= "" then detail = o_detail end
1270                name = o_name
1271                sub = o_sub
1272            end
1273            -- so far
1274            -- some settings can have been overloaded
1275            if lookup and lookup ~= "" then
1276                specification.lookup = lookup
1277            end
1278            if relativeid and relativeid ~= "" then -- experimental hook
1279                local id = tonumber(relativeid) or 0
1280                specification.relativeid = id > 0 and id
1281            end
1282            --
1283            specification.name      = name
1284            specification.size      = size
1285            specification.sub       = (sub and sub ~= "" and sub) or specification.sub
1286            specification.mathsize  = mathsize
1287            specification.textsize  = textsize
1288            specification.goodies   = goodies
1289            specification.cs        = cs
1290            specification.global    = global
1291            specification.scalemode = scaledfontmode -- context specific
1292            if detail and detail ~= "" then
1293                specification.method = method or "*"
1294                specification.detail = detail
1295            elseif specification.detail and specification.detail ~= "" then
1296                -- already set
1297            elseif inheritancemode == 0 then
1298                -- nothing
1299            elseif inheritancemode == 1 then
1300                -- fontonly
1301                if fontfeatures and fontfeatures ~= "" then
1302                    specification.method = "*"
1303                    specification.detail = fontfeatures
1304                end
1305                if fontfallbacks and fontfallbacks ~= "" then
1306                    specification.fallbacks = fontfallbacks
1307                end
1308            elseif inheritancemode == 2 then
1309                -- classonly
1310                if classfeatures and classfeatures ~= "" then
1311                    specification.method = "*"
1312                    specification.detail = classfeatures
1313                end
1314                if classfallbacks and classfallbacks ~= "" then
1315                    specification.fallbacks = classfallbacks
1316                end
1317            elseif inheritancemode == 3 then
1318                -- fontfirst
1319                if combinefeatures then
1320                    if classfeatures and classfeatures ~= "" then
1321                        specification.method = "*"
1322                        if fontfeatures and fontfeatures ~= "" and fontfeatures ~= classfeatures then
1323                            specification.detail = classfeatures .. "," .. fontfeatures
1324                        else
1325                            specification.detail = classfeatures
1326                        end
1327                    elseif fontfeatures and fontfeatures ~= "" then
1328                        specification.method = "*"
1329                        specification.detail = fontfeatures
1330                    end
1331                else
1332                    if fontfeatures and fontfeatures ~= "" then
1333                        specification.method = "*"
1334                        specification.detail = fontfeatures
1335                    elseif classfeatures and classfeatures ~= "" then
1336                        specification.method = "*"
1337                        specification.detail = classfeatures
1338                    end
1339                end
1340                if fontfallbacks and fontfallbacks ~= "" then
1341                    specification.fallbacks = fontfallbacks
1342                elseif classfallbacks and classfallbacks ~= "" then
1343                    specification.fallbacks = classfallbacks
1344                end
1345            elseif inheritancemode == 4 then
1346                -- classfirst
1347                if combinefeatures then
1348                    if fontfeatures and fontfeatures ~= "" then
1349                        specification.method = "*"
1350                        if classfeatures and classfeatures ~= "" and classfeatures ~= fontfeatures then
1351                            specification.detail = fontfeatures .. "," .. classfeatures
1352                        else
1353                            specification.detail = fontfeatures
1354                        end
1355                    elseif classfeatures and classfeatures ~= "" then
1356                        specification.method = "*"
1357                        specification.detail = classfeatures
1358                    end
1359                else
1360                    if classfeatures and classfeatures ~= "" then
1361                        specification.method = "*"
1362                        specification.detail = classfeatures
1363                    elseif fontfeatures and fontfeatures ~= "" then
1364                        specification.method = "*"
1365                        specification.detail = fontfeatures
1366                    end
1367                end
1368                if classfallbacks and classfallbacks ~= "" then
1369                    specification.fallbacks = classfallbacks
1370                elseif fontfallbacks and fontfallbacks ~= "" then
1371                    specification.fallbacks = fontfallbacks
1372                end
1373            end
1374            --
1375            local tfmdata = definers.read(specification,size) -- id not yet known (size in spec?)
1376            --
1377            local lastfontid = 0
1378            local tfmtype    = type(tfmdata)
1379            if tfmtype == "table" then
1380                -- setting the extra characters will move elsewhere
1381                local characters = tfmdata.characters
1382                local parameters = tfmdata.parameters
1383                local properties = tfmdata.properties
1384                -- we use char0 as signal; cf the spec pdf can handle this (no char in slot)
1385                characters[0] = nil
1386             -- characters[0x00A0] = { width = parameters.space }
1387             -- characters[0x2007] = { width = characters[0x0030] and characters[0x0030].width or parameters.space } -- figure
1388             -- characters[0x2008] = { width = characters[0x002E] and characters[0x002E].width or parameters.space } -- period
1389                --
1390                local fallbacks = specification.fallbacks or ""
1391                local mathsize  = (mathsize == 1 or mathsize == 2 or mathsize == 3) and mathsize or nil -- can be unset so we test 1 2 3
1392                if fallbacks ~= "" and mathsize and not busy then
1393                    busy = true
1394                    -- We need this ugly hack in order to resolve fontnames (at the \TEX end). Originally
1395                    -- math was done in Lua after loading (plugged into aftercopying).
1396                    --
1397                    -- After tl 2017 I'll also do text fallbacks this way (although backups there are done
1398                    -- in a completely different way.)
1399                    if trace_defining then
1400                        report_defining("defining %a, id %a, target %a, features %a / %a, fallbacks %a / %a, step %a",
1401                            name,id,nice_cs(cs),classfeatures,fontfeatures,classfallbacks,fontfallbacks,1)
1402                    end
1403                    mathematics.resolvefallbacks(tfmdata,specification,fallbacks)
1404                    context(function()
1405                        busy = false
1406                        mathematics.finishfallbacks(tfmdata,specification,fallbacks)
1407                        tfmdata.original = specification.specification
1408                        local id = definefont(tfmdata)
1409                        csnames[id] = specification.cs
1410                        properties.id = id
1411                        definers.register(tfmdata,id) -- to be sure, normally already done
1412                        texdefinefont(global,cs,id)
1413                        constructors.cleanuptable(tfmdata)
1414                        constructors.finalize(tfmdata)
1415                        if trace_defining then
1416                            report_defining("defining %a, id %a, target %a, features %a / %a, fallbacks %a / %a, step %a",
1417                                name,id,nice_cs(cs),classfeatures,fontfeatures,classfallbacks,fontfallbacks,2)
1418                        end
1419                        -- resolved (when designsize is used):
1420                        local size = round(tfmdata.parameters.size or 655360)
1421                        setmacro("somefontsize",size.."sp")
1422                     -- ctx_setsomefontsize(size .. "sp")
1423                        texsetcount("scaledfontsize",size)
1424                        lastfontid = id
1425                        --
1426                        if trace_defining then
1427                            report_defining("memory usage after: %s",statistics.memused())
1428                            report_defining("stop stage two")
1429                        end
1430                        --
1431                        texsetcount("global","lastfontid",lastfontid)
1432                        specifiers[lastfontid] = { str, size }
1433                        if not mathsize then
1434                            -- forget about it (can't happen here)
1435                        elseif mathsize == 0 then
1436                            -- can't happen (here)
1437                        else
1438                            -- maybe only 1 2 3 (we already test for this)
1439                            lastmathids[mathsize] = lastfontid
1440                        end
1441                        stoptiming(fonts)
1442                    end)
1443                    return
1444                else
1445                    tfmdata.original = specification.specification
1446                    local id = definefont(tfmdata)
1447                    csnames[id] = specification.cs
1448                    properties.id = id
1449                    definers.register(tfmdata,id) -- to be sure, normally already done
1450                    texdefinefont(global,cs,id)
1451                    constructors.cleanuptable(tfmdata)
1452                    constructors.finalize(tfmdata)
1453                    if trace_defining then
1454                        report_defining("defining %a, id %a, target %a, features %a / %a, fallbacks %a / %a, step %a",
1455                            name,id,nice_cs(cs),classfeatures,fontfeatures,classfallbacks,fontfallbacks,"-")
1456                    end
1457                    -- resolved (when designsize is used):
1458                    local size = round(tfmdata.parameters.size or 655360)
1459                    setmacro("somefontsize",size.."sp")
1460                 -- ctx_setsomefontsize(size .. "sp")
1461                    texsetcount("scaledfontsize",size)
1462                    lastfontid = id
1463                end
1464            elseif tfmtype == "number" then
1465                if trace_defining then
1466                    report_defining("reusing %s, id %a, target %a, features %a / %a, fallbacks %a / %a, goodies %a / %a, designsize %a / %a",
1467                        name,tfmdata,nice_cs(cs),classfeatures,fontfeatures,classfallbacks,fontfallbacks,classgoodies,goodies,classdesignsize,fontdesignsize)
1468                end
1469                csnames[tfmdata] = specification.cs
1470                texdefinefont(global,cs,tfmdata)
1471                -- resolved (when designsize is used):
1472                local size = round(fontdata[tfmdata].parameters.size or 0)
1473             -- ctx_setsomefontsize(size .. "sp")
1474                setmacro("somefontsize",size.."sp")
1475                texsetcount("scaledfontsize",size)
1476                lastfontid = tfmdata
1477            else
1478                report_defining("unable to define %a as %a",name,nice_cs(cs))
1479                lastfontid = -1
1480                texsetcount("scaledfontsize",0)
1481             -- ctx_letvaluerelax(cs) -- otherwise the current definition takes the previous one
1482            end
1483            if trace_defining then
1484                report_defining("memory usage after: %s",statistics.memused())
1485                report_defining("stop stage two")
1486            end
1487            --
1488            texsetcount("global","lastfontid",lastfontid)
1489            specifiers[lastfontid] = { str, size }
1490            if not mathsize then
1491                -- forget about it
1492            elseif mathsize == 0 then
1493                -- can't happen (here)
1494            else
1495                -- maybe only 1 2 3
1496                lastmathids[mathsize] = lastfontid
1497            end
1498            --
1499            stoptiming(fonts)
1500        end
1501    }
1502
1503    implement {
1504         name      = "specifiedfontspec",
1505         arguments = "integer",
1506         actions   = function(id)
1507            local f = specifiers[id]
1508            if f then
1509                context(f[1])
1510            end
1511        end
1512    }
1513
1514    implement {
1515         name      = "specifiedfontsize",
1516         arguments = "integer",
1517         actions   = function(id)
1518            local f = specifiers[id]
1519            if f then
1520                context(f[2])
1521            end
1522        end
1523    }
1524
1525    implement {
1526         name      = "specifiedfont",
1527         arguments = { "integer", "number" },
1528         actions   = function(id,size)
1529            local f = specifiers[id]
1530            if f and size then
1531                context("%s at %0.2p",f[1],size * f[2]) -- we round to 2 decimals (as at the tex end)
1532            end
1533        end
1534    }
1535    --
1536
1537    local function define(specification)
1538        --
1539        local name = specification.name
1540        if not name or name == "" then
1541            return -1
1542        else
1543            starttiming(fonts)
1544            --
1545            -- following calls expect a few properties to be set:
1546            --
1547            local lookup, name, sub, method, detail = getspecification(name or "")
1548            --
1549            specification.name          = (name ~= "" and name) or specification.name
1550            --
1551            specification.lookup        = specification.lookup or (lookup ~= "" and lookup) or "file"
1552            specification.size          = specification.size                                or 655260
1553            specification.sub           = specification.sub    or (sub    ~= "" and sub)    or ""
1554            specification.method        = specification.method or (method ~= "" and method) or "*"
1555            specification.detail        = specification.detail or (detail ~= "" and detail) or ""
1556            --
1557            if type(specification.size) == "string" then
1558                specification.size = texsp(specification.size) or 655260
1559            end
1560            --
1561            specification.specification = "" -- not used
1562            specification.resolved      = ""
1563            specification.forced        = ""
1564            specification.features      = { } -- via detail, maybe some day
1565            --
1566            -- we don't care about mathsize textsize goodies fallbacks
1567            --
1568            local cs = specification.cs
1569            if cs == "" then
1570                cs = nil
1571                specification.cs = nil
1572                specification.global = false
1573            elseif specification.global == nil then
1574                specification.global = false
1575            end
1576            --
1577            local tfmdata = definers.read(specification,specification.size)
1578            if not tfmdata then
1579                return -1, nil
1580            elseif type(tfmdata) == "number" then
1581                if cs then
1582                    texdefinefont(specification.global,cs,tfmdata)
1583                    csnames[tfmdata] = cs
1584                end
1585                stoptiming(fonts)
1586                return tfmdata, fontdata[tfmdata]
1587            else
1588                local id = definefont(tfmdata)
1589                tfmdata.properties.id = id
1590                definers.register(tfmdata,id)
1591                if cs then
1592                    texdefinefont(specification.global,cs,id)
1593                    csnames[id] = cs
1594                end
1595                constructors.cleanuptable(tfmdata)
1596                constructors.finalize(tfmdata)
1597                stoptiming(fonts)
1598                return id, tfmdata
1599            end
1600        end
1601    end
1602
1603    definers.define = define
1604
1605    -- local id, cs = fonts.definers.internal { }
1606    -- local id, cs = fonts.definers.internal { number = 2 }
1607    -- local id, cs = fonts.definers.internal { name = "dejavusans" }
1608
1609    local n = 0
1610
1611    function definers.internal(specification,cs)
1612        specification = specification or { }
1613        local name    = specification.name
1614        local size    = tonumber(specification.size)
1615        local number  = tonumber(specification.number)
1616        local id      = nil
1617        if not size then
1618            size = texgetdimen("bodyfontsize")
1619        end
1620        if number then
1621            id = number
1622        elseif name and name ~= "" then
1623            local cs = cs or specification.cs
1624            if not cs then
1625                n  = n + 1 -- beware ... there can be many and they are often used once
1626             -- cs = formatters["internal font %s"](n)
1627                cs = "internal font " .. n
1628            else
1629                specification.cs = cs
1630            end
1631            id = define {
1632                name = name,
1633                size = size,
1634                cs   = cs,
1635            }
1636        end
1637        if not id then
1638            id = currentfont()
1639        end
1640        return id, csnames[id]
1641    end
1642
1643    callbacks.register('define_font', definers.read, "definition of fonts (tfmdata preparation)")
1644
1645    -- here
1646
1647    local infofont = 0
1648
1649    function fonts.infofont()
1650        if infofont == 0 then
1651            infofont = define { name = "dejavusansmono", size = texsp("6pt") }
1652        end
1653        return infofont
1654    end
1655
1656    -- abstract interfacing
1657
1658    implement { name = "tf", actions = function() setmacro("fontalternative","tf") end }
1659    implement { name = "bf", actions = function() setmacro("fontalternative","bf") end }
1660    implement { name = "it", actions = function() setmacro("fontalternative","it") end }
1661    implement { name = "sl", actions = function() setmacro("fontalternative","sl") end }
1662    implement { name = "bi", actions = function() setmacro("fontalternative","bi") end }
1663    implement { name = "bs", actions = function() setmacro("fontalternative","bs") end }
1664
1665end
1666
1667local enable_auto_r_scale = false
1668
1669experiments.register("fonts.autorscale", function(v)
1670    enable_auto_r_scale = v
1671end)
1672
1673-- Not ok, we can best use a database for this. The problem is that we
1674-- have delayed definitions and so we never know what style is taken
1675-- as start.
1676
1677local calculatescale  = constructors.calculatescale
1678
1679function constructors.calculatescale(tfmdata,scaledpoints,relativeid,specification)
1680    if specification then
1681        local scalemode = specification.scalemode
1682        local special   = scalemode and specialscale[scalemode]
1683        if special then
1684            -- we also have available specification.textsize
1685            local parameters = tfmdata.parameters
1686         -- local designsize = parameters.designsize
1687            if     special == "ht" then
1688                local height = parameters.ascender / parameters.units
1689                scaledpoints = scaledpoints / height
1690            elseif special == "cp" then
1691                local glyph  = tfmdata.descriptions[utfbyte("X")]
1692                local height = (glyph and glyph.height or parameters.ascender) / parameters.units
1693                scaledpoints = scaledpoints / height
1694            end
1695        end
1696    end
1697    local scaledpoints, delta = calculatescale(tfmdata,scaledpoints)
1698 -- if enable_auto_r_scale and relativeid then -- for the moment this is rather context specific (we need to hash rscale then)
1699 --     local relativedata = fontdata[relativeid]
1700 --     local rfmdata = relativedata and relativedata.unscaled and relativedata.unscaled -- just use metadata instead
1701 --     local id_x_height = rfmdata and rfmdata.parameters and rfmdata.parameters.x_height
1702 --     local tf_x_height = tfmdata and tfmdata.parameters and tfmdata.parameters.x_height
1703 --     if id_x_height and tf_x_height then
1704 --         local rscale = id_x_height/tf_x_height
1705 --         delta = rscale * delta
1706 --         scaledpoints = rscale * scaledpoints
1707 --     end
1708 -- end
1709    return round(scaledpoints), round(delta)
1710end
1711
1712local designsizes = constructors.designsizes
1713
1714-- called quite often when in mp labels
1715-- otf.normalizedaxis
1716
1717function constructors.hashinstance(specification,force)
1718    local hash      = specification.hash
1719    local size      = specification.size
1720    local fallbacks = specification.fallbacks
1721    if force or not hash then
1722        hash = constructors.hashfeatures(specification)
1723        specification.hash = hash
1724    end
1725    if size < 1000 and designsizes[hash] then
1726        size = round(constructors.scaled(size,designsizes[hash]))
1727    else
1728        size = round(size)
1729    end
1730    specification.size = size
1731    if fallbacks then
1732        return hash .. ' @ ' .. size .. ' @ ' .. fallbacks
1733    else
1734        local scalemode = specification.scalemode
1735        local special   = scalemode and specialscale[scalemode]
1736        if special then
1737            return hash .. ' @ ' .. size .. ' @ ' .. special
1738        else
1739            return hash .. ' @ ' .. size
1740        end
1741    end
1742end
1743
1744-- We overload the (generic) resolver:
1745
1746local resolvers    = definers.resolvers
1747local hashfeatures = constructors.hashfeatures
1748
1749function definers.resolve(specification) -- overload function in font-con.lua
1750    if not specification.resolved or specification.resolved == "" then -- resolved itself not per se in mapping hash
1751        local r = resolvers[specification.lookup]
1752        if r then
1753            r(specification)
1754        end
1755    end
1756    if specification.forced == "" then
1757        specification.forced = nil
1758    else
1759        specification.forced = specification.forced
1760    end
1761    -- goodies are a context specific thing and are not always defined
1762    -- as feature, so we need to make sure we add them here before
1763    -- hashing because otherwise we get funny goodies applied
1764    local goodies = specification.goodies
1765    if goodies and goodies ~= "" then
1766        -- this adapts the features table so it has best be a copy
1767        local normal = specification.features.normal
1768        if not normal then
1769            specification.features.normal = { goodies = goodies }
1770        elseif not normal.goodies then
1771            local g = normal.goodies
1772            if g and g ~= "" then
1773                normal.goodies = formatters["%s,%s"](g,goodies)
1774            else
1775                normal.goodies = goodies
1776            end
1777        end
1778    end
1779    -- so far for goodie hacks
1780    local hash = hashfeatures(specification)
1781    local name = specification.name or "badfont"
1782    local sub  = specification.sub
1783    if sub and sub ~= "" then
1784        specification.hash = lower(name .. " @ " .. sub .. ' @ ' .. hash)
1785    else
1786        specification.hash = lower(name .. " @ "        .. ' @ ' .. hash)
1787    end
1788    --
1789    return specification
1790end
1791
1792-- soon to be obsolete:
1793
1794local mappings = fonts.mappings
1795
1796local loaded = { -- prevent loading (happens in cont-sys files)
1797 -- ["original-base.map"     ] = true,
1798 -- ["original-ams-base.map" ] = true,
1799 -- ["original-ams-euler.map"] = true,
1800 -- ["original-public-lm.map"] = true,
1801}
1802
1803function mappings.loadfile(name)
1804    name = file.addsuffix(name,"map")
1805    if not loaded[name] then
1806        if trace_mapfiles then
1807            report_mapfiles("loading map file %a",name)
1808        end
1809        lpdf.setmapfile(name) -- bah. will go in lmtx
1810        loaded[name] = true
1811    end
1812end
1813
1814local loaded = { -- prevent double loading
1815}
1816
1817function mappings.loadline(how,line)
1818    if line then
1819        how = how .. " " .. line
1820    elseif how == "" then
1821        how = "= " .. line
1822    end
1823    if not loaded[how] then
1824        if trace_mapfiles then
1825            report_mapfiles("processing map line %a",line)
1826        end
1827        lpdf.setmapline(how) -- bah. will go in lmtx
1828        loaded[how] = true
1829    end
1830end
1831
1832function mappings.reset()
1833    lpdf.setmapfile("") -- tricky ... backend related
1834end
1835
1836implement {
1837    name      = "loadmapfile",
1838    actions   = mappings.loadfile,
1839    arguments = "string"
1840}
1841
1842implement {
1843    name      = "loadmapline",
1844    actions   = mappings.loadline,
1845    arguments = "string"
1846}
1847
1848implement {
1849    name      = "resetmapfiles",
1850    actions   = mappings.reset,
1851    arguments = "string"
1852}
1853
1854-- we need an 'do after the banner hook'
1855
1856-- => commands
1857
1858local pattern = P("P")
1859              * (lpeg.patterns.hexdigit^4 / function(s) return tonumber(s,16) end)
1860              * P(-1)
1861
1862local function nametoslot(name) -- also supports PXXXXX (4+ positions)
1863    local t = type(name)
1864    if t == "string" then
1865        local unic = unicodes[true]
1866        local slot = unic[name]
1867        if slot then
1868            return slot
1869        end
1870        --
1871        local slot = unic[gsub(name,"_"," ")] or unic[gsub(name,"_","-")] or
1872                     unic[gsub(name,"-"," ")] or unic[gsub(name,"-","_")] or
1873                     unic[gsub(name," ","_")] or unic[gsub(name," ","-")]
1874        if slot then
1875            return slot
1876        end
1877        --
1878        if not aglunicodes then
1879            aglunicodes = encodings.agl.unicodes
1880        end
1881        local char = characters[true]
1882        local slot = aglunicodes[name]
1883        if slot and char[slot] then
1884            return slot
1885        end
1886        local slot = lpegmatch(pattern,name)
1887        if slot and char[slot] then
1888            return slot
1889        end
1890        -- not in font
1891    elseif t == "number" then
1892        if characters[true][name] then
1893            return slot
1894        else
1895            -- not in font
1896        end
1897    end
1898end
1899
1900local found = { }
1901
1902local function descriptiontoslot(name)
1903    local t = type(name)
1904    if t == "string" then
1905        -- slow
1906        local list = sortedkeys(chardata) -- can be a cache with weak tables
1907        local slot = found[name]
1908        local char = characters[true]
1909        if slot then
1910            return char[slot] and slot or nil
1911        end
1912        local NAME = upper(name)
1913        for i=1,#list do
1914            slot = list[i]
1915            local c = chardata[slot]
1916            local d = c.description
1917            if d == NAME then
1918                found[name] = slot
1919                return char[slot] and slot or nil
1920            end
1921        end
1922        for i=1,#list do
1923            slot = list[i]
1924            local c = chardata[slot]
1925            local s = c.synonyms
1926            if s then
1927                for i=1,#s do
1928                    local si = s[i]
1929                    if si == name then
1930                        found[name] = si
1931                        return char[slot] and slot or nil
1932                    end
1933                end
1934            end
1935        end
1936        for i=1,#list do
1937            slot = list[i]
1938            local c = chardata[slot]
1939            local d = c.description
1940            if d and find(d,NAME) then
1941                found[name] = slot
1942                return char[slot] and slot or nil
1943            end
1944        end
1945        for i=1,#list do
1946            slot = list[i]
1947            local c = chardata[slot]
1948            local s = c.synonyms
1949            if s then
1950                for i=1,#s do
1951                    local si = s[i]
1952                    if find(s[i],name) then
1953                        found[name] = si
1954                        return char[slot] and slot or nil
1955                    end
1956                end
1957            end
1958        end
1959        -- not in font
1960    elseif t == "number" then
1961        if characters[true][name] then
1962            return slot
1963        else
1964            -- not in font
1965        end
1966    end
1967end
1968
1969local function indextoslot(font,index)
1970    if not index then
1971        index = font
1972        font  = true
1973    end
1974    local r = resources[font]
1975    if r then
1976        local indices = r.indices
1977        if not indices then
1978            indices = { }
1979            local c = characters[font]
1980            for unicode, data in next, c do
1981                local di = data.index
1982                if di then
1983                    indices[di] = unicode
1984                end
1985            end
1986            r.indices = indices
1987        end
1988        return indices[tonumber(index)]
1989    end
1990end
1991
1992do -- else too many locals
1993
1994    local entities = characters.entities
1995    local lowered  = { } -- delayed initialization
1996
1997    setmetatableindex(lowered,function(t,k)
1998        for k, v in next, entities do
1999            local l = lower(k)
2000            if not entities[l] then
2001                lowered[l] = v
2002            end
2003        end
2004        setmetatableindex(lowered,nil)
2005        return lowered[k]
2006    end)
2007
2008    local methods = {
2009        -- entity
2010        e = function(name)
2011                return entities[name] or lowered[name] or name
2012            end,
2013        -- hexadecimal unicode
2014        x = function(name)
2015                local n = tonumber(name,16)
2016                return n and utfchar(n) or name
2017            end,
2018        -- decimal unicode
2019        d = function(name)
2020                local n = tonumber(name)
2021                return n and utfchar(n) or name
2022            end,
2023        -- hexadecimal index (slot)
2024        s = function(name)
2025                local n = tonumber(name,16)
2026                local n = n and indextoslot(n)
2027                return n and utfchar(n) or name
2028            end,
2029        -- decimal index
2030        i = function(name)
2031                local n = tonumber(name)
2032                local n = n and indextoslot(n)
2033                return n and utfchar(n) or name
2034            end,
2035        -- name
2036        n = function(name)
2037                local n = nametoslot(name)
2038                return n and utfchar(n) or name
2039            end,
2040        -- unicode description (synonym)
2041        u = function(name)
2042                local n = descriptiontoslot(name,false)
2043                return n and utfchar(n) or name
2044            end,
2045        -- all
2046        a = function(name)
2047                local n = nametoslot(name) or descriptiontoslot(name)
2048                return n and utfchar(n) or name
2049            end,
2050        -- char
2051        c = function(name)
2052                return name
2053            end,
2054    }
2055
2056    -- -- nicer:
2057    --
2058    -- setmetatableindex(methods,function(t,k) return methods.c end)
2059    --
2060    -- local splitter = (C(1) * P(":") + Cc("c")) * C(P(1)^1) / function(method,name)
2061    --     return methods[method](name)
2062    -- end
2063    --
2064    -- -- more efficient:
2065
2066    local splitter = C(1) * P(":") * C(P(1)^1) / function(method,name)
2067        local action = methods[method]
2068        return action and action(name) or name
2069    end
2070
2071    local function tochar(str)
2072        local t = type(str)
2073        if t == "number" then
2074            return utfchar(str)
2075        elseif t == "string" then
2076            return lpegmatch(splitter,str) or str
2077        else
2078            return str
2079        end
2080    end
2081
2082    helpers.nametoslot        = nametoslot
2083    helpers.descriptiontoslot = descriptiontoslot
2084    helpers.indextoslot       = indextoslot
2085    helpers.tochar            = tochar
2086
2087    -- interfaces:
2088
2089    implement {
2090        name      = "fontchar",
2091        actions   = { nametoslot, ctx_char },
2092        arguments = "string",
2093    }
2094
2095    implement {
2096        name      = "fontcharbyindex",
2097        actions   = { indextoslot, ctx_char },
2098        arguments = "integer",
2099    }
2100
2101    implement {
2102        name      = "tochar",
2103        actions   = { tochar, ctx_safechar },
2104        arguments = "string",
2105    }
2106
2107end
2108
2109-- this will change ...
2110
2111function loggers.reportdefinedfonts()
2112    if trace_usage then
2113        local t, tn = { }, 0
2114        for id, data in sortedhash(fontdata) do
2115            local properties = data.properties or { }
2116            local parameters = data.parameters or { }
2117            tn = tn + 1
2118            t[tn] = {
2119  formatters["%03i"](id                    or 0),
2120  formatters["%p"  ](parameters.size       or 0),
2121                     properties.type       or "real",
2122                     properties.format     or "unknown",
2123                     properties.name       or "",
2124                     properties.psname     or "",
2125                     properties.fullname   or "",
2126                     properties.sharedwith or "",
2127            }
2128        end
2129        formatcolumns(t,"  ")
2130        --
2131        logs.startfilelogging(report,"defined fonts")
2132        for k=1,tn do
2133            report(t[k])
2134        end
2135        logs.stopfilelogging()
2136    end
2137end
2138
2139logs.registerfinalactions(loggers.reportdefinedfonts)
2140
2141function loggers.reportusedfeatures()
2142    -- numbers, setups, merged
2143    if trace_usage then
2144        local t, n = { }, #numbers
2145        for i=1,n do
2146            local name = numbers[i]
2147            local setup = setups[name]
2148            local n = setup.number
2149            setup.number = nil -- we have no reason to show this
2150            t[i] = { i, name, sequenced(setup,false,true) } -- simple mode
2151            setup.number = n -- restore it (normally not needed as we're done anyway)
2152        end
2153        formatcolumns(t,"  ")
2154        logs.startfilelogging(report,"defined featuresets")
2155        for k=1,n do
2156            report(t[k])
2157        end
2158        logs.stopfilelogging()
2159    end
2160end
2161
2162logs.registerfinalactions(loggers.reportusedfeatures)
2163
2164-- maybe move this to font-log.lua:
2165
2166statistics.register("font engine", function()
2167    local elapsed   = statistics.elapsedseconds(fonts)
2168    local nofshared = constructors.nofsharedfonts or 0
2169    local nofloaded = constructors.noffontsloaded or 0
2170    if nofshared > 0 then
2171        return format("otf %0.3f, afm %0.3f, tfm %0.3f, %s instances, %s shared in backend, %s common vectors, %s common hashes, load time %s",
2172            otf.version,afm.version,tfm.version,nofloaded,
2173            nofshared,constructors.nofsharedvectors,constructors.nofsharedhashes,
2174            elapsed)
2175    elseif nofloaded > 0 and elapsed then
2176        return format("otf %0.3f, afm %0.3f, tfm %0.3f, %s instances, load time %s",
2177            otf.version,afm.version,tfm.version,nofloaded,
2178            elapsed)
2179    else
2180        return format("otf %0.3f, afm %0.3f, tfm %0.3f",
2181            otf.version,afm.version,tfm.version)
2182    end
2183end)
2184
2185-- experimental mechanism for Mojca:
2186--
2187-- fonts.definetypeface {
2188--     name         = "mainbodyfont-light",
2189--     preset       = "antykwapoltawskiego-light",
2190-- }
2191--
2192-- fonts.definetypeface {
2193--     name         = "mojcasfavourite",
2194--     preset       = "antykwapoltawskiego",
2195--     normalweight = "light",
2196--     boldweight   = "bold",
2197--     width        = "condensed",
2198-- }
2199
2200local Shapes = {
2201    serif = "Serif",
2202    sans  = "Sans",
2203    mono  = "Mono",
2204}
2205
2206local ctx_startfontclass       = context.startfontclass
2207local ctx_stopfontclass        = context.stopfontclass
2208local ctx_definefontsynonym    = context.definefontsynonym
2209local ctx_dofastdefinetypeface = context.dofastdefinetypeface
2210
2211function fonts.definetypeface(name,t)
2212    if type(name) == "table" then
2213        -- {name=abc,k=v,...}
2214        t = name
2215    elseif t then
2216        if type(t) == "string" then
2217            -- "abc", "k=v,..."
2218            t = settings_to_hash(name)
2219        else
2220            -- "abc", {k=v,...}
2221        end
2222        t.name = t.name or name
2223    else
2224        -- "name=abc,k=v,..."
2225        t = settings_to_hash(name)
2226    end
2227    local p = t.preset and fonts.typefaces[t.preset] or { }
2228    local name         = t.name         or "unknowntypeface"
2229    local shortcut     = t.shortcut     or p.shortcut or "rm"
2230    local size         = t.size         or p.size     or "default"
2231    local shape        = t.shape        or p.shape    or "serif"
2232    local fontname     = t.fontname     or p.fontname or "unknown"
2233    local normalweight = t.normalweight or t.weight or p.normalweight or p.weight or "normal"
2234    local boldweight   = t.boldweight   or t.weight or p.boldweight   or p.weight or "normal"
2235    local normalwidth  = t.normalwidth  or t.width  or p.normalwidth  or p.width  or "normal"
2236    local boldwidth    = t.boldwidth    or t.width  or p.boldwidth    or p.width  or "normal"
2237    Shape = Shapes[shape] or "Serif"
2238    ctx_startfontclass { name }
2239        ctx_definefontsynonym( { formatters["%s"]          (Shape) }, { formatters["spec:%s-%s-regular-%s"] (fontname, normalweight, normalwidth) } )
2240        ctx_definefontsynonym( { formatters["%sBold"]      (Shape) }, { formatters["spec:%s-%s-regular-%s"] (fontname, boldweight,   boldwidth  ) } )
2241        ctx_definefontsynonym( { formatters["%sBoldItalic"](Shape) }, { formatters["spec:%s-%s-italic-%s"]  (fontname, boldweight,   boldwidth  ) } )
2242        ctx_definefontsynonym( { formatters["%sItalic"]    (Shape) }, { formatters["spec:%s-%s-italic-%s"]  (fontname, normalweight, normalwidth) } )
2243    ctx_stopfontclass()
2244    local settings = sequenced({ features= t.features },",")
2245    ctx_dofastdefinetypeface(name, shortcut, shape, size, settings)
2246end
2247
2248implement {
2249    name      = "definetypeface",
2250    actions   = fonts.definetypeface,
2251    arguments = "2 strings"
2252}
2253
2254function fonts.current() -- todo: also handle name
2255    return fontdata[currentfont()] or fontdata[0]
2256end
2257
2258function fonts.currentid()
2259    return currentfont() or 0
2260end
2261
2262-- for the moment here, this will become a chain of extras that is
2263-- hooked into the ctx registration (or scaler or ...)
2264
2265local dimenfactors = number.dimenfactors
2266
2267function helpers.dimenfactor(unit,id)
2268    if unit == "ex" then
2269        return id and exheights[id] or 282460 -- lm 10pt
2270    elseif unit == "em" then
2271        return id and emwidths [id] or 655360 -- lm 10pt
2272    else
2273        local du = dimenfactors[unit]
2274        return du and 1/du or tonumber(unit) or 1
2275    end
2276end
2277
2278local function digitwidth(font) -- max(quad/2,wd(0..9))
2279    local tfmdata = fontdata[font]
2280    local parameters = tfmdata.parameters
2281    local width = parameters.digitwidth
2282    if not width then
2283        width = round(parameters.quad/2) -- maybe tex.scale
2284        local characters = tfmdata.characters
2285        for i=48,57 do
2286            local wd = round(characters[i].width)
2287            if wd > width then
2288                width = wd
2289            end
2290        end
2291        parameters.digitwidth = width
2292    end
2293    return width
2294end
2295
2296helpers.getdigitwidth = digitwidth
2297helpers.setdigitwidth = digitwidth
2298
2299--
2300
2301function helpers.getparameters(tfmdata)
2302    local p = { }
2303    local m = p
2304    local parameters = tfmdata.parameters
2305    while true do
2306        for k, v in next, parameters do
2307            m[k] = v
2308        end
2309        parameters = getmetatable(parameters)
2310        parameters = parameters and parameters.__index
2311        if type(parameters) == "table" then
2312            m = { }
2313            p.metatable = m
2314        else
2315            break
2316        end
2317    end
2318    return p
2319end
2320
2321if environment.initex then
2322
2323    local function names(t)
2324        local nt = #t
2325        if nt > 0 then
2326            local n = { }
2327            for i=1,nt do
2328                n[i] = t[i].name
2329            end
2330            return concat(n," ")
2331        else
2332            return "-"
2333        end
2334    end
2335
2336    statistics.register("font processing", function()
2337        local l = { }
2338        for what, handler in table.sortedpairs(handlers) do
2339            local features = handler and handler.features
2340            if features then
2341                l[#l+1] = format("[%s (base initializers: %s) (base processors: %s) (base manipulators: %s) (node initializers: %s) (node processors: %s) (node manipulators: %s)]",
2342                    what,
2343                    names(features.initializers.base),
2344                    names(features.processors  .base),
2345                    names(features.manipulators.base),
2346                    names(features.initializers.node),
2347                    names(features.processors  .node),
2348                    names(features.manipulators.node)
2349                )
2350            end
2351        end
2352        return concat(l, " | ")
2353    end)
2354
2355end
2356
2357-- redefinition
2358
2359-- local hashes      = fonts.hashes
2360-- local emwidths    = hashes.emwidths
2361-- local exheights   = hashes.exheights
2362
2363setmetatableindex(dimenfactors, function(t,k)
2364    if k == "ex" then
2365        return 1/exheights[currentfont()]
2366    elseif k == "em" then
2367        return 1/emwidths[currentfont()]
2368    elseif k == "pct" or k == "%" then
2369        return 1/(texget("hsize")/100)
2370    else
2371     -- error("wrong dimension: " .. (s or "?")) -- better a message
2372        return false
2373    end
2374end)
2375
2376dimenfactors.ex   = nil
2377dimenfactors.em   = nil
2378dimenfactors["%"] = nil
2379dimenfactors.pct  = nil
2380
2381-- Before a font is passed to TeX we scale it. Here we also need to scale virtual
2382-- characters.
2383
2384do
2385
2386    -- can become luat-tex.lua
2387
2388    local texsetglyphdata = tex.setglyphdata
2389    local texgetglyphdata = tex.getglyphdata
2390
2391    if not texsetglyphdata then
2392
2393        local texsetattribute = tex.setattribute
2394        local texgetattribute = tex.getattribute
2395
2396        texsetglyphdata = function(n) return texsetattribute(0,n) end
2397        texgetglyphdata = function()  return texgetattribute(0)   end
2398
2399        tex.setglyphdata = texsetglyphdata
2400        tex.getglyphdata = texgetglyphdata
2401
2402    end
2403
2404    -- till here
2405
2406    local setmacro = tokens.setters.macro
2407
2408    function constructors.currentfonthasfeature(n)
2409        local f = fontdata[currentfont()]
2410        if not f then return end f = f.shared
2411        if not f then return end f = f.rawdata
2412        if not f then return end f = f.resources
2413        if not f then return end f = f.features
2414        return f and (f.gpos[n] or f.gsub[n])
2415    end
2416
2417    local ctx_doifelse = commands.doifelse
2418    local ctx_doif     = commands.doif
2419
2420    implement {
2421        name      = "doifelsecurrentfonthasfeature",
2422        actions   = { constructors.currentfonthasfeature, ctx_doifelse },
2423        arguments = "string"
2424    }
2425
2426    local f_strip  = formatters["%0.2fpt"] -- normally this value is changed only once
2427    local stripper = lpeg.patterns.stripzeros
2428
2429    local cache = { }
2430
2431    local hows = {
2432        ["+"] = "add",
2433        ["-"] = "subtract",
2434        ["="] = "replace",
2435    }
2436
2437    local function setfeature(how,parent,name,font) -- 0/1 test temporary for testing
2438        if not how or how == 0 then
2439            if trace_features and texgetglyphdata() ~= 0 then
2440                report_cummulative("font %!font:name!, reset",fontdata[font or true])
2441            end
2442            texsetglyphdata(0)
2443        elseif how == true or how == 1 then
2444            local hash = "feature > " .. parent
2445            local done = cache[hash]
2446            if trace_features and done then
2447                report_cummulative("font %!font:name!, revive %a : %!font:features!",fontdata[font or true],parent,setups[numbers[done]])
2448            end
2449            texsetglyphdata(done or 0)
2450        else
2451            local full = parent .. how .. name
2452            local hash = "feature > " .. full
2453            local done = cache[hash]
2454            if not done then
2455                local n = setups[full]
2456                if n then
2457                    -- already defined
2458                else
2459                    n = mergecontextfeatures(parent,name,how,full)
2460                end
2461                done = registercontextfeature(hash,full,how)
2462                cache[hash] = done
2463                if trace_features then
2464                    report_cummulative("font %!font:name!, %s %a : %!font:features!",fontdata[font or true],hows[how],full,setups[numbers[done]])
2465                end
2466            end
2467            texsetglyphdata(done)
2468        end
2469    end
2470
2471    local function resetfeature()
2472        if trace_features and texgetglyphdata() ~= 0 then
2473            report_cummulative("font %!font:name!, reset",fontdata[true])
2474        end
2475        texsetglyphdata(0)
2476    end
2477
2478    local function setfontfeature(tag)
2479        texsetglyphdata(contextnumber(tag))
2480    end
2481
2482    local function resetfontfeature()
2483        texsetglyphdata(0)
2484    end
2485
2486    implement {
2487        name      = "nbfs",
2488        arguments = "dimen",
2489        actions   = function(d)
2490            context(lpegmatch(stripper,f_strip(d/65536)))
2491        end
2492    }
2493
2494    implement {
2495        name      = "featureattribute",
2496        arguments = "string",
2497        actions   = { contextnumber, context }
2498    }
2499
2500    implement {
2501        name      = "setfontfeature",
2502        arguments = "string",
2503        actions   = setfontfeature,
2504    }
2505
2506    implement {
2507        name      = "resetfontfeature",
2508     -- arguments = { 0, 0 },
2509        actions   = resetfontfeature,
2510    }
2511
2512    implement {
2513        name      = "setfontofid",
2514        arguments = "integer",
2515        actions   = function(id)
2516            ctx_getvalue(csnames[id])
2517        end
2518    }
2519
2520    implement {
2521        name      = "definefontfeature",
2522        arguments = "3 strings",
2523        actions   = presetcontext,
2524    }
2525
2526    implement {
2527        name      = "doifelsefontfeature",
2528        arguments = "string",
2529        actions   = function(name) ctx_doifelse(contextnumber(name) > 1) end,
2530    }
2531
2532    implement {
2533        name      = "doifunknownfontfeature",
2534        arguments = "string",
2535        actions   = function(name) ctx_doif(contextnumber(name) == 0) end,
2536    }
2537
2538    implement {
2539        name      = "adaptfontfeature",
2540        arguments = "2 strings",
2541        actions   = adaptcontext
2542    }
2543
2544    local function registerlanguagefeatures()
2545        local specifications = languages.data.specifications
2546        for i=1,#specifications do
2547            local specification = specifications[i]
2548            local language = specification.opentype
2549            if language then
2550                local script = specification.opentypescript or specification.script
2551                if script then
2552                    local context = specification.context
2553                    if type(context) == "table" then
2554                        for i=1,#context do
2555                            definecontext(context[i], { language = language, script = script})
2556                        end
2557                    elseif type(context) == "string" then
2558                       definecontext(context, { language = language, script = script})
2559                    end
2560                end
2561            end
2562        end
2563    end
2564
2565    constructors.setfeature   = setfeature
2566    constructors.resetfeature = resetfeature
2567
2568    implement { name = "resetfeature",    actions = resetfeature }
2569    implement { name = "addfeature",      actions = setfeature, arguments = { "'+'",  "string", "string" } }
2570    implement { name = "subtractfeature", actions = setfeature, arguments = { "'-'",  "string", "string" } }
2571    implement { name = "replacefeature",  actions = setfeature, arguments = { "'='",  "string", "string" } }
2572    implement { name = "revivefeature",   actions = setfeature, arguments = { true, "string" } }
2573
2574    implement {
2575        name      = "featurelist",
2576        actions   = { fonts.specifiers.contexttostring, context },
2577        arguments = { "string", "'otf'", "string", "'yes'", "'no'", true }
2578    }
2579
2580    implement {
2581        name    = "registerlanguagefeatures",
2582        actions = registerlanguagefeatures,
2583    }
2584
2585end
2586
2587-- a fontkern plug:
2588
2589-- nodes.injections.installnewkern(nuts.pool.fontkern)
2590
2591do
2592
2593    local report = logs.reporter("otf","variants")
2594
2595    local function replace(tfmdata,feature,value)
2596        local characters = tfmdata.characters
2597        local variants   = tfmdata.resources.variants
2598        if variants then
2599            local t = { }
2600            for k, v in sortedhash(variants) do
2601                t[#t+1] = formatters["0x%X (%i)"](k,k)
2602            end
2603            value = tonumber(value) or 0xFE00 -- 917762
2604            report("fontname : %s",tfmdata.properties.fontname)
2605            report("available: % t",t)
2606            local v = variants[value]
2607            if v then
2608                report("using    : %X (%i)",value,value)
2609                for k, v in next, v do
2610                    local c = characters[v]
2611                    if c then
2612                        characters[k] = c
2613                    end
2614                end
2615            else
2616                report("unknown  : %X (%i)",value,value)
2617            end
2618        end
2619    end
2620
2621    registerotffeature {
2622        name         = 'variant',
2623        description  = 'unicode variant',
2624        manipulators = {
2625            base = replace,
2626            node = replace,
2627        }
2628    }
2629
2630end
2631
2632-- here (todo: closure)
2633
2634-- make a closure (200 limit):
2635
2636do
2637
2638    local trace_analyzing = false  trackers.register("otf.analyzing", function(v) trace_analyzing = v end)
2639
2640    local analyzers       = fonts.analyzers
2641    local methods         = analyzers.methods
2642
2643    local unsetvalue      = attributes.unsetvalue
2644
2645    local a_color         = attributes.private('color')
2646    local a_colormodel    = attributes.private('colormodel')
2647    local m_color         = attributes.list[a_color] or { }
2648
2649    local glyph_code      = nodes.nodecodes.glyph
2650
2651    local states          = analyzers.states
2652
2653    local colornames = {
2654        [states.init] = "font:1",
2655        [states.medi] = "font:2",
2656        [states.fina] = "font:3",
2657        [states.isol] = "font:4",
2658        [states.mark] = "font:5",
2659        [states.rest] = "font:6",
2660        [states.rphf] = "font:1",
2661        [states.half] = "font:2",
2662        [states.pref] = "font:3",
2663        [states.blwf] = "font:4",
2664        [states.pstf] = "font:5",
2665    }
2666
2667    -- todo: traversers
2668    -- todo: check attr_list so that we can use the same .. helper: setcolorattr
2669
2670    local function markstates(head)
2671        if head then
2672            head = tonut(head)
2673            local model = getattr(head,a_colormodel) or 1
2674            for glyph in nextchar, head do
2675                local a = getstate(glyph)
2676                if a then
2677                    local name = colornames[a]
2678                    if name then
2679                        local color = m_color[name]
2680                        if color then
2681                            setattr(glyph,a_colormodel,model)
2682                            setattr(glyph,a_color,color)
2683                        end
2684                    end
2685                end
2686            end
2687        end
2688    end
2689
2690    local function analyzeprocessor(head,font,attr)
2691        local tfmdata = fontdata[font]
2692        local script, language = otf.scriptandlanguage(tfmdata,attr)
2693        local action = methods[script]
2694        if not action then
2695            return head, false
2696        end
2697        if type(action) == "function" then
2698            local head, done = action(head,font,attr)
2699            if done and trace_analyzing then
2700                markstates(head)
2701            end
2702            return head, done
2703        end
2704        action = action[language]
2705        if action then
2706            local head, done = action(head,font,attr)
2707            if done and trace_analyzing then
2708                markstates(head)
2709            end
2710            return head, done
2711        else
2712            return head, false
2713        end
2714    end
2715
2716    registerotffeature { -- adapts
2717        name       = "analyze",
2718        processors = {
2719            node     = analyzeprocessor,
2720        }
2721    }
2722
2723
2724    function methods.nocolor(head,font,attr)
2725        for n, c, f in nextchar, head do
2726            if not font or f == font then
2727                setattr(n,a_color,unsetvalue)
2728            end
2729        end
2730        return head, true
2731    end
2732
2733end
2734
2735
2736local function purefontname(name)
2737    if type(name) == "number" then
2738        name = getfontname(name)
2739    end
2740    if type(name) == "string" then
2741       return basename(name)
2742    end
2743end
2744
2745implement {
2746    name      = "purefontname",
2747    actions   = { purefontname, context },
2748    arguments = "string",
2749}
2750
2751local sharedstorage = storage.shared
2752
2753local list    = sharedstorage.bodyfontsizes        or { }
2754local unknown = sharedstorage.unknownbodyfontsizes or { }
2755
2756sharedstorage.bodyfontsizes        = list
2757sharedstorage.unknownbodyfontsizes = unknown
2758
2759implement {
2760    name      = "registerbodyfontsize",
2761    arguments = "string",
2762    actions   = function(size)
2763        list[size] = true
2764    end
2765}
2766
2767interfaces.implement {
2768    name      = "registerunknownbodysize",
2769    arguments = "string",
2770    actions   = function(size)
2771        if not unknown[size] then
2772            interfaces.showmessage("fonts",14,size)
2773        end
2774        unknown[size] = true
2775    end,
2776}
2777
2778implement {
2779    name      = "getbodyfontsizes",
2780    arguments = "string",
2781    actions   = function(separator)
2782        context(concat(sortedkeys(list),separator))
2783    end
2784}
2785
2786implement {
2787    name      = "processbodyfontsizes",
2788    arguments = "string",
2789    actions   = function(command)
2790        local keys = sortedkeys(list)
2791        if command then
2792            local action = context[command]
2793            for i=1,#keys do
2794                action(keys[i])
2795            end
2796        else
2797            context(concat(keys,","))
2798        end
2799    end
2800}
2801
2802implement {
2803    name      = "cleanfontname",
2804    actions   = { cleanname, context },
2805    arguments = "string"
2806}
2807
2808implement {
2809    name      = "fontlookupinitialize",
2810    actions   = names.lookup,
2811    arguments = "string",
2812}
2813
2814implement {
2815    name      = "fontlookupnoffound",
2816    actions   = { names.noflookups, context },
2817}
2818
2819implement {
2820    name      = "fontlookupgetkeyofindex",
2821    actions   = { names.getlookupkey, context },
2822    arguments = { "string", "integer"}
2823}
2824
2825implement {
2826    name      = "fontlookupgetkey",
2827    actions   = { names.getlookupkey, context },
2828    arguments = "string"
2829}
2830
2831-- this might move to a runtime module:
2832
2833function commands.showchardata(n)
2834    local tfmdata = fontdata[currentfont()]
2835    if tfmdata then
2836        if type(n) == "string" then
2837            n = utfbyte(n)
2838        end
2839        local chr = tfmdata.characters[n]
2840        if chr then
2841            report_status("%s @ %s => %U => %c => %s",tfmdata.properties.fullname,tfmdata.parameters.size,n,n,serialize(chr,false))
2842        end
2843    end
2844end
2845
2846function commands.showfontparameters(tfmdata)
2847    -- this will become more clever
2848    local tfmdata = tfmdata or fontdata[currentfont()]
2849    if tfmdata then
2850        local parameters        = tfmdata.parameters
2851        local mathparameters    = tfmdata.mathparameters
2852        local properties        = tfmdata.properties
2853        local hasparameters     = parameters     and next(parameters)
2854        local hasmathparameters = mathparameters and next(mathparameters)
2855        if hasparameters then
2856            report_status("%s @ %s => text parameters => %s",properties.fullname,parameters.size,serialize(parameters,false))
2857        end
2858        if hasmathparameters then
2859            report_status("%s @ %s => math parameters => %s",properties.fullname,parameters.size,serialize(mathparameters,false))
2860        end
2861        if not hasparameters and not hasmathparameters then
2862            report_status("%s @ %s => no text parameters and/or math parameters",properties.fullname,parameters.size)
2863        end
2864    end
2865end
2866
2867implement {
2868    name    = "currentdesignsize",
2869    actions = function()
2870        context(parameters[currentfont()].designsize)
2871    end
2872}
2873
2874implement {
2875    name      = "doifelsefontpresent",
2876    actions   = { names.exists, commands.doifelse },
2877    arguments = "string"
2878}
2879
2880-- we use 0xFE000+ and 0xFF000+ in math and for runtime (text) extensions we
2881-- use 0xFD000+
2882
2883constructors.privateslots = constructors.privateslots or { }
2884
2885storage.register("fonts/constructors/privateslots", constructors.privateslots, "fonts.constructors.privateslots")
2886
2887do
2888
2889    local privateslots    = constructors.privateslots
2890    local lastprivateslot = 0xFD000
2891
2892    constructors.privateslots = setmetatableindex(privateslots,function(t,k)
2893        local v = lastprivateslot
2894        lastprivateslot = lastprivateslot + 1
2895        t[k] = v
2896        return v
2897    end)
2898
2899    implement {
2900        name      = "getprivateglyphslot",
2901        actions   = function(name) context(privateslots[name]) end,
2902        arguments = "string",
2903    }
2904
2905end
2906
2907-- an extra helper
2908
2909function helpers.getcoloredglyphs(tfmdata)
2910    if type(tfmdata) == "number" then
2911        tfmdata = fontdata[tfmdata]
2912    end
2913    if not tfmdata then
2914        tfmdata = fontdata[true]
2915    end
2916    local characters   = tfmdata.characters
2917    local descriptions = tfmdata.descriptions
2918    local collected    = { }
2919    for unicode, character in next, characters do
2920        local description = descriptions[unicode]
2921        if description and (description.colors or character.svg) then
2922            collected[#collected+1] = unicode
2923        end
2924    end
2925    table.sort(collected)
2926    return collected
2927end
2928
2929-- for the font manual
2930
2931statistics.register("body font sizes", function()
2932    if next(unknown) then
2933        return formatters["defined: % t, undefined: % t"](sortedkeys(list),sortedkeys(unknown))
2934    end
2935end)
2936
2937statistics.register("used fonts",function()
2938    if trace_usage then
2939        local filename = file.nameonly(environment.jobname) .. "-fonts-usage.lua"
2940        if next(fontdata) then
2941            local files = { }
2942            local list  = { }
2943            for id, tfmdata in sortedhash(fontdata) do
2944                local filename = tfmdata.properties.filename
2945                if filename then
2946                    local filedata = files[filename]
2947                    if filedata then
2948                        filedata.instances = filedata.instances + 1
2949                    else
2950                        local rawdata  = tfmdata.shared and tfmdata.shared.rawdata
2951                        local metadata = rawdata and rawdata.metadata
2952                        files[filename] = {
2953                            instances = 1,
2954                            filename  = filename,
2955                            version   = metadata and metadata.version,
2956                            size      = rawdata and rawdata.size,
2957                        }
2958                    end
2959                else
2960                    -- what to do
2961                end
2962            end
2963            for k, v in sortedhash(files) do
2964                list[#list+1] = v
2965            end
2966            table.save(filename,list)
2967        else
2968            os.remove(filename)
2969        end
2970    end
2971end)
2972
2973-- new
2974
2975do
2976
2977    local settings_to_array    = utilities.parsers.settings_to_array
2978 -- local namedcolorattributes = attributes.colors.namedcolorattributes
2979 -- local colorvalues          = attributes.colors.values
2980
2981 -- implement {
2982 --     name      = "definefontcolorpalette",
2983 --     arguments = "2 strings",
2984 --     actions   = function(name,set)
2985 --         set = settings_to_array(set)
2986 --         for i=1,#set do
2987 --             local name = set[i]
2988 --             local space, color = namedcolorattributes(name)
2989 --             local values = colorvalues[color]
2990 --             if values then
2991 --                 set[i] = { r = values[3], g = values[4],  b = values[5] }
2992 --             else
2993 --                 set[i] = { r = 0, g = 0, b = 0 }
2994 --             end
2995 --         end
2996 --         otf.registerpalette(name,set)
2997 --     end
2998 -- }
2999
3000    implement {
3001        name      = "definefontcolorpalette",
3002        arguments = "2 strings",
3003        actions   = function(name,set)
3004            otf.registerpalette(name,settings_to_array(set))
3005        end
3006    }
3007
3008end
3009
3010do
3011
3012    local pattern = C((1-S("* "))^1) -- strips all after * or ' at'
3013
3014    implement {
3015        name      = "truefontname",
3016        arguments = "string",
3017        actions   = function(s)
3018         -- context(match(s,"[^* ]+") or s)
3019            context(lpegmatch(pattern,s) or s)
3020        end
3021    }
3022
3023end
3024
3025do
3026
3027    local function getinstancespec(id)
3028        local data      = fontdata[id or true]
3029        local shared    = data.shared
3030        local resources = shared and shared.rawdata.resources
3031        if resources then
3032            local instancespec = data.properties.instance
3033            if instancespec then
3034                local variabledata = resources.variabledata
3035                if variabledata then
3036                    local instances = variabledata.instances
3037                    if instances then
3038                        for i=1,#instances do
3039                            local instance = instances[i]
3040                            if cleanname(instance.subfamily)== instancespec then
3041                                local values = table.copy(instance.values)
3042                                local axis = variabledata.axis
3043                                for i=1,#values do
3044                                    for j=1,#axis do
3045                                        if values[i].axis == axis[j].tag then
3046                                            values[i].name = axis[j].name
3047                                            break
3048                                        end
3049                                    end
3050                                end
3051                                return values
3052                            end
3053                        end
3054                    end
3055                end
3056            end
3057        end
3058    end
3059
3060    helpers.getinstancespec = getinstancespec
3061
3062    implement {
3063        name      = "currentfontinstancespec",
3064        actions   = function()
3065            local t = getinstancespec() -- current font
3066            if t then
3067                for i=1,#t do
3068                    if i > 1 then
3069                        context.space()
3070                    end
3071                    local ti = t[i]
3072                    context("%s=%s",ti.name,ti.value)
3073                end
3074            end
3075        end
3076    }
3077
3078end
3079
3080-- for the moment here (and not in font-con.lua):
3081
3082do
3083
3084    local identical     = table.identical
3085    local copy          = table.copy
3086    local fontdata      = fonts.hashes.identifiers
3087    local addcharacters = font.addcharacters
3088
3089    -- This helper is mostly meant to add last-resort (virtual) characters
3090    -- or runtime generated fonts (so we forget about features and such). It
3091    -- will probably take a while before it get used.
3092
3093    local trace_adding  = false
3094    local report_adding = logs.reporter("fonts","add characters")
3095
3096    trackers.register("fonts.addcharacters",function(v) trace_adding = v end)
3097
3098    if addcharacters then
3099
3100        function fonts.constructors.addcharacters(id,list)
3101            local newchar = list.characters
3102            if newchar then
3103                local data    = fontdata[id]
3104                local newfont = list.fonts
3105                local oldchar = data.characters
3106                local oldfont = data.fonts
3107                addcharacters(id, {
3108                    characters = newchar,
3109                    fonts      = newfont,
3110                    nomath     = not data.properties.hasmath,
3111                })
3112                -- this is just for tracing, as the assignment only uses the fonts list
3113                -- and doesn't store it otherwise
3114                if newfont then
3115                    if oldfont then
3116                        local oldn = #oldfont
3117                        local newn = #newfont
3118                        for n=1,newn do
3119                            local ok = false
3120                            local nf = newfont[n]
3121                            for o=1,oldn do
3122                                if identical(nf,oldfont[o]) then
3123                                    ok = true
3124                                    break
3125                                end
3126                            end
3127                            if not ok then
3128                                oldn = oldn + 1
3129                                oldfont[oldn] = newfont[i]
3130                            end
3131                        end
3132                    else
3133                        data.fonts = newfont
3134                    end
3135                end
3136                -- this is because we need to know what goes on and also might
3137                -- want to access character data
3138                for u, c in next, newchar do
3139                    if trace_adding then
3140                        report_adding("adding character %U to font %!font:name!",u,id)
3141                    end
3142                    oldchar[u] = c
3143                end
3144            end
3145        end
3146
3147    else
3148        function fonts.constructors.addcharacters(id,list)
3149            report_adding("adding characters to %!font:name! is not yet supported",id)
3150        end
3151    end
3152
3153    implement {
3154        name      = "addfontpath",
3155        arguments = "string",
3156        actions   = function(list)
3157            names.addruntimepath(settings_to_array(list))
3158        end
3159    }
3160
3161end
3162
3163-- moved here
3164
3165do
3166
3167    local getfontoffamily = font.getfontoffamily
3168    local new_glyph       = nodes.pool.glyph
3169    local fontproperties  = fonts.hashes.properties
3170
3171    local function getprivateslot(id,name)
3172        if not name then
3173            name = id
3174            id   = currentfont()
3175        end
3176        local properties = fontproperties[id]
3177        local privates   = properties and properties.privates
3178        return privates and privates[name]
3179    end
3180
3181    local function getprivatenode(tfmdata,name)
3182        if type(tfmdata) == "number" then
3183            tfmdata = fontdata[tfmdata]
3184        end
3185        local properties = tfmdata.properties
3186        local font = properties.id
3187        local slot = getprivateslot(font,name)
3188        if slot then
3189            -- todo: set current attribibutes
3190            local char   = tfmdata.characters[slot]
3191            local tonode = char.tonode
3192            if tonode then
3193                return tonode(font,char)
3194            else
3195                return new_glyph(font,slot)
3196            end
3197        end
3198    end
3199
3200    local function getprivatecharornode(tfmdata,name)
3201        if type(tfmdata) == "number" then
3202            tfmdata = fontdata[tfmdata]
3203        end
3204        local properties = tfmdata.properties
3205        local font = properties.id
3206        local slot = getprivateslot(font,name)
3207        if slot then
3208            -- todo: set current attributes
3209            local char   = tfmdata.characters[slot]
3210            local tonode = char.tonode
3211            if tonode then
3212                return "node", tonode(tfmdata,char)
3213            else
3214                return "char", slot
3215            end
3216        end
3217    end
3218
3219    helpers.getprivateslot       = getprivateslot
3220    helpers.getprivatenode       = getprivatenode
3221    helpers.getprivatecharornode = getprivatecharornode
3222
3223    implement {
3224        name      = "getprivatechar",
3225        arguments = "string",
3226        actions   = function(name)
3227            local p = getprivateslot(name)
3228            if p then
3229                context(utfchar(p))
3230            end
3231        end
3232    }
3233
3234    implement {
3235        name      = "getprivatemathchar",
3236        arguments = "string",
3237        actions   = function(name)
3238            local p = getprivateslot(getfontoffamily(0),name)
3239            if p then
3240                context(utfchar(p))
3241            end
3242        end
3243    }
3244
3245    implement {
3246        name      = "getprivateslot",
3247        arguments = "string",
3248        actions   = function(name)
3249            local p = getprivateslot(name)
3250            if p then
3251                context(p)
3252            end
3253        end
3254    }
3255
3256end
3257
3258-- handy, for now here:
3259
3260function fonts.helpers.collectanchors(tfmdata)
3261
3262    local resources = tfmdata.resources -- todo: use shared
3263
3264    if not resources or resources.anchors then
3265        return resources.anchors
3266    end
3267
3268    local anchors = { }
3269
3270    local function set(unicode,target,class,anchor)
3271        local a = anchors[unicode]
3272        if not a then
3273            anchors[unicode] = { [target] = { anchor } }
3274            return
3275        end
3276        local t = a[target]
3277        if not t then
3278            a[target] = { anchor }
3279            return
3280        end
3281        local x = anchor[1]
3282        local y = anchor[2]
3283        for k, v in next, t do
3284            if v[1] == x and v[2] == y then
3285                return
3286            end
3287        end
3288        t[#t+1] = anchor
3289    end
3290
3291    local function getanchors(steps,target)
3292        for i=1,#steps do
3293            local step     = steps[i]
3294            local coverage = step.coverage
3295            for unicode, data in next, coverage do
3296                local class  = data[1]
3297                local anchor = data[2]
3298                if anchor[1] ~= 0 or anchor[2] ~= 0 then
3299                    set(unicode,target,class,anchor)
3300                end
3301            end
3302        end
3303    end
3304
3305    local function getcursives(steps)
3306        for i=1,#steps do
3307            local step     = steps[i]
3308            local coverage = step.coverage
3309            for unicode, data in next, coverage do
3310                local class  = data[1]
3311                local en = data[2]
3312                local ex = data[3]
3313                if en then
3314                    set(unicode,"entry",class,en)
3315                end
3316                if ex then
3317                    set(unicode,"exit", class,ex)
3318                end
3319            end
3320        end
3321    end
3322
3323    local function collect(list)
3324        if list then
3325            for i=1,#list do
3326                local entry = list[i]
3327                local steps = entry.steps
3328                local kind  = entry.type
3329                if kind == "gpos_mark2mark" then
3330                    getanchors(steps,"mark")
3331                elseif kind == "gpos_mark2base" then
3332                    getanchors(steps,"base")
3333                elseif kind == "gpos_mark2ligature" then
3334                    getanchors(steps,"ligature")
3335                elseif kind == "gpos_cursive" then
3336                    getcursives(steps)
3337                end
3338            end
3339        end
3340    end
3341
3342    collect(resources.sequences)
3343    collect(resources.sublookups)
3344
3345    local function sorter(a,b)
3346        if a[1] == b[1] then
3347            return a[2] < b[2]
3348        else
3349            return a[1] < b[1]
3350        end
3351    end
3352
3353    for unicode, old in next, anchors do
3354        for target, list in next, old do
3355            sort(list,sorter)
3356        end
3357    end
3358
3359    resources.anchors = anchors
3360
3361    return anchors
3362
3363end
3364