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